From b033d9bc282af1ec42d1d7ba93fdf541d4663584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Sat, 28 Nov 2020 17:53:37 +0000 Subject: [PATCH 01/26] move courseName to shared folder and update references --- src/Contexts/Mooc/Courses/application/CourseCreator.ts | 2 +- .../Mooc/Courses/application/CreateCourseCommandHandler.ts | 2 +- src/Contexts/Mooc/Courses/domain/Course.ts | 2 +- .../{Courses/domain => Shared/domain/Courses}/CourseName.ts | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename src/Contexts/Mooc/{Courses/domain => Shared/domain/Courses}/CourseName.ts (65%) diff --git a/src/Contexts/Mooc/Courses/application/CourseCreator.ts b/src/Contexts/Mooc/Courses/application/CourseCreator.ts index db2b81b..5cee3b3 100644 --- a/src/Contexts/Mooc/Courses/application/CourseCreator.ts +++ b/src/Contexts/Mooc/Courses/application/CourseCreator.ts @@ -2,9 +2,9 @@ import { CourseRepository } from '../domain/CourseRepository'; import { Course } from '../domain/Course'; import { CreateCourseRequest } from './CreateCourseRequest'; import { CourseId } from '../../Shared/domain/Courses/CourseId'; -import { CourseName } from '../domain/CourseName'; import { CourseDuration } from '../domain/CourseDuration'; import { EventBus } from '../../../Shared/domain/EventBus'; +import { CourseName } from '../../Shared/domain/Courses/CourseName'; type Params = { courseId: CourseId; diff --git a/src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler.ts b/src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler.ts index 2c8c60e..c5735f1 100644 --- a/src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler.ts +++ b/src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler.ts @@ -3,8 +3,8 @@ import { CommandHandler } from '../../../Shared/domain/CommandHandler'; import { CourseCreator } from './CourseCreator'; import { Command } from '../../../Shared/domain/Command'; import { CourseId } from '../../Shared/domain/Courses/CourseId'; -import { CourseName } from '../domain/CourseName'; import { CourseDuration } from '../domain/CourseDuration'; +import { CourseName } from '../../Shared/domain/Courses/CourseName'; export class CreateCourseCommandHandler implements CommandHandler { constructor(private courseCreator: CourseCreator) {} diff --git a/src/Contexts/Mooc/Courses/domain/Course.ts b/src/Contexts/Mooc/Courses/domain/Course.ts index 4f2513e..ab7f55f 100644 --- a/src/Contexts/Mooc/Courses/domain/Course.ts +++ b/src/Contexts/Mooc/Courses/domain/Course.ts @@ -1,8 +1,8 @@ import { AggregateRoot } from './AggregateRoot'; import { CourseCreatedDomainEvent } from './CourseCreatedDomainEvent'; -import { CourseName } from './CourseName'; import { CourseDuration } from './CourseDuration'; import { CourseId } from '../../Shared/domain/Courses/CourseId'; +import { CourseName } from '../../Shared/domain/Courses/CourseName'; export class Course extends AggregateRoot { readonly id: CourseId; diff --git a/src/Contexts/Mooc/Courses/domain/CourseName.ts b/src/Contexts/Mooc/Shared/domain/Courses/CourseName.ts similarity index 65% rename from src/Contexts/Mooc/Courses/domain/CourseName.ts rename to src/Contexts/Mooc/Shared/domain/Courses/CourseName.ts index a7d92e4..64a5587 100644 --- a/src/Contexts/Mooc/Courses/domain/CourseName.ts +++ b/src/Contexts/Mooc/Shared/domain/Courses/CourseName.ts @@ -1,5 +1,5 @@ -import { StringValueObject } from '../../../Shared/domain/value-object/StringValueObject'; -import { InvalidArgumentError } from '../../../Shared/domain/value-object/InvalidArgumentError'; +import { InvalidArgumentError } from "../../../../Shared/domain/value-object/InvalidArgumentError"; +import { StringValueObject } from "../../../../Shared/domain/value-object/StringValueObject"; export class CourseName extends StringValueObject { constructor(value: string) { From 20aab3b7633264597a7ae5cebb64108165fe599b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Sat, 28 Nov 2020 17:56:47 +0000 Subject: [PATCH 02/26] update CourseName tests references --- tests/Contexts/Mooc/Courses/domain/CourseMother.ts | 2 +- tests/Contexts/Mooc/Courses/domain/CourseNameMother.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Contexts/Mooc/Courses/domain/CourseMother.ts b/tests/Contexts/Mooc/Courses/domain/CourseMother.ts index 4c388be..c59ab45 100644 --- a/tests/Contexts/Mooc/Courses/domain/CourseMother.ts +++ b/tests/Contexts/Mooc/Courses/domain/CourseMother.ts @@ -1,5 +1,5 @@ import { CourseId } from '../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseId'; -import { CourseName } from '../../../../../src/Contexts/Mooc/Courses/domain/CourseName'; +import { CourseName } from '../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseName'; import { CourseDuration } from '../../../../../src/Contexts/Mooc/Courses/domain/CourseDuration'; import { Course } from '../../../../../src/Contexts/Mooc/Courses/domain/Course'; import { CreateCourseRequest } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourseRequest'; diff --git a/tests/Contexts/Mooc/Courses/domain/CourseNameMother.ts b/tests/Contexts/Mooc/Courses/domain/CourseNameMother.ts index 99f1c11..6f63df6 100644 --- a/tests/Contexts/Mooc/Courses/domain/CourseNameMother.ts +++ b/tests/Contexts/Mooc/Courses/domain/CourseNameMother.ts @@ -1,4 +1,4 @@ -import { CourseName } from '../../../../../src/Contexts/Mooc/Courses/domain/CourseName'; +import { CourseName } from '../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseName'; import { WordMother } from '../../../Shared/domain/WordMother'; export class CourseNameMother { From 62c56b097f5ee20929fba846d3119b8f52a5942d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Sat, 28 Nov 2020 17:57:18 +0000 Subject: [PATCH 03/26] remove unused reference --- tests/Contexts/Mooc/Courses/domain/CourseMother.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Contexts/Mooc/Courses/domain/CourseMother.ts b/tests/Contexts/Mooc/Courses/domain/CourseMother.ts index c59ab45..ae2f2e8 100644 --- a/tests/Contexts/Mooc/Courses/domain/CourseMother.ts +++ b/tests/Contexts/Mooc/Courses/domain/CourseMother.ts @@ -2,7 +2,6 @@ import { CourseId } from '../../../../../src/Contexts/Mooc/Shared/domain/Courses import { CourseName } from '../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseName'; import { CourseDuration } from '../../../../../src/Contexts/Mooc/Courses/domain/CourseDuration'; import { Course } from '../../../../../src/Contexts/Mooc/Courses/domain/Course'; -import { CreateCourseRequest } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourseRequest'; import { CourseIdMother } from '../../Shared/domain/Courses/CourseIdMother'; import { CourseNameMother } from './CourseNameMother'; import { CourseDurationMother } from './CourseDurationMother'; From 144fa1040f9090314556dd73c0b36f0d64e4c20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Sat, 28 Nov 2020 17:58:00 +0000 Subject: [PATCH 04/26] added new twit object with its properties --- .../Mooc/Notifications/domain/Twit.ts | 25 +++++++++++++++++++ .../Mooc/Notifications/domain/TwitId.ts | 3 +++ .../Mooc/Notifications/domain/TwitMessage.ts | 3 +++ 3 files changed, 31 insertions(+) create mode 100644 src/Contexts/Mooc/Notifications/domain/Twit.ts create mode 100644 src/Contexts/Mooc/Notifications/domain/TwitId.ts create mode 100644 src/Contexts/Mooc/Notifications/domain/TwitMessage.ts diff --git a/src/Contexts/Mooc/Notifications/domain/Twit.ts b/src/Contexts/Mooc/Notifications/domain/Twit.ts new file mode 100644 index 0000000..3aa28ae --- /dev/null +++ b/src/Contexts/Mooc/Notifications/domain/Twit.ts @@ -0,0 +1,25 @@ +import { Uuid } from '../../../Shared/domain/value-object/Uuid'; +import { TwitId } from './TwitId'; +import { TwitMessage } from './TwitMessage'; + +type ConstructorParams = { + id?: TwitId; + message: TwitMessage; +}; + +export class Twit { + readonly message: TwitMessage; + readonly id: TwitId; + + constructor(params: ConstructorParams) { + this.id = params.id || new TwitId(Uuid.random().value); + this.message = params.message; + } + + equals(twit: Twit): boolean { + return ( + this.id.value === twit.id.value && + this.message.value === twit.message.value + ); + } +} diff --git a/src/Contexts/Mooc/Notifications/domain/TwitId.ts b/src/Contexts/Mooc/Notifications/domain/TwitId.ts new file mode 100644 index 0000000..603966f --- /dev/null +++ b/src/Contexts/Mooc/Notifications/domain/TwitId.ts @@ -0,0 +1,3 @@ +import { Uuid } from '../../../Shared/domain/value-object/Uuid'; + +export class TwitId extends Uuid {} diff --git a/src/Contexts/Mooc/Notifications/domain/TwitMessage.ts b/src/Contexts/Mooc/Notifications/domain/TwitMessage.ts new file mode 100644 index 0000000..2ab2469 --- /dev/null +++ b/src/Contexts/Mooc/Notifications/domain/TwitMessage.ts @@ -0,0 +1,3 @@ +import { StringValueObject } from '../../../Shared/domain/value-object/StringValueObject'; + +export class TwitMessage extends StringValueObject {} From 6766596ae3ce1554ac5ced9d54c8df80c92e0cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Sat, 28 Nov 2020 18:20:34 +0000 Subject: [PATCH 05/26] added tests about twit object --- .../Mooc/Notifications/domain/Twit.test.ts | 16 ++++++++++++++++ .../Mooc/Notifications/domain/TwitIdMother.ts | 16 ++++++++++++++++ .../Notifications/domain/TwitMessageMother.ts | 12 ++++++++++++ .../Mooc/Notifications/domain/TwitMother.ts | 15 +++++++++++++++ 4 files changed, 59 insertions(+) create mode 100644 tests/Contexts/Mooc/Notifications/domain/Twit.test.ts create mode 100644 tests/Contexts/Mooc/Notifications/domain/TwitIdMother.ts create mode 100644 tests/Contexts/Mooc/Notifications/domain/TwitMessageMother.ts create mode 100644 tests/Contexts/Mooc/Notifications/domain/TwitMother.ts diff --git a/tests/Contexts/Mooc/Notifications/domain/Twit.test.ts b/tests/Contexts/Mooc/Notifications/domain/Twit.test.ts new file mode 100644 index 0000000..fdd80c8 --- /dev/null +++ b/tests/Contexts/Mooc/Notifications/domain/Twit.test.ts @@ -0,0 +1,16 @@ +import { TwitIdMother } from "./TwitIdMother"; +import { TwitMessageMother } from "./TwitMessageMother"; +import { TwitMother } from "./TwitMother"; + + +describe('Twit', () => { + + it('should return a new twit instance', () => { + const id = TwitIdMother.random(); + const message = TwitMessageMother.random(); + const twit = TwitMother.create(id, message); + + expect(twit.id.value).toBe(id.value); + expect(twit.message.value).toBe(message.value); + }); +}); diff --git a/tests/Contexts/Mooc/Notifications/domain/TwitIdMother.ts b/tests/Contexts/Mooc/Notifications/domain/TwitIdMother.ts new file mode 100644 index 0000000..9d5c943 --- /dev/null +++ b/tests/Contexts/Mooc/Notifications/domain/TwitIdMother.ts @@ -0,0 +1,16 @@ +import { UuidMother } from "../../../Shared/domain/UuidMother"; +import { TwitId } from "../../../../../src/Contexts/Mooc/Notifications/domain/TwitId"; + +export class TwitIdMother { + static create(value: string): TwitId { + return new TwitId(value); + } + + static creator() { + return () => TwitIdMother.random(); + } + + static random(): TwitId { + return this.create(UuidMother.random()); + } +} diff --git a/tests/Contexts/Mooc/Notifications/domain/TwitMessageMother.ts b/tests/Contexts/Mooc/Notifications/domain/TwitMessageMother.ts new file mode 100644 index 0000000..6e4af00 --- /dev/null +++ b/tests/Contexts/Mooc/Notifications/domain/TwitMessageMother.ts @@ -0,0 +1,12 @@ +import { WordMother } from '../../../Shared/domain/WordMother'; +import { TwitMessage } from "../../../../../src/Contexts/Mooc/Notifications/domain/TwitMessage"; + +export class TwitMessageMother { + static create(value: string): TwitMessage { + return new TwitMessage(value); + } + + static random(): TwitMessage { + return this.create(WordMother.random()); + } +} diff --git a/tests/Contexts/Mooc/Notifications/domain/TwitMother.ts b/tests/Contexts/Mooc/Notifications/domain/TwitMother.ts new file mode 100644 index 0000000..f396328 --- /dev/null +++ b/tests/Contexts/Mooc/Notifications/domain/TwitMother.ts @@ -0,0 +1,15 @@ +import { TwitIdMother } from "./TwitIdMother"; +import { TwitId } from "../../../../../src/Contexts/Mooc/Notifications/domain/TwitId"; +import { TwitMessage } from "../../../../../src/Contexts/Mooc/Notifications/domain/TwitMessage"; +import { Twit } from "../../../../../src/Contexts/Mooc/Notifications/domain/Twit"; +import { TwitMessageMother } from "./TwitMessageMother"; + +export class TwitMother { + static create(id: TwitId, message: TwitMessage): Twit { + return new Twit({id, message}); + } + + static random(): Twit { + return this.create(TwitIdMother.random(), TwitMessageMother.random()); + } +} From a83a4243d336ff7d71081dbfb8b81613829cbe22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Sun, 29 Nov 2020 16:54:31 +0000 Subject: [PATCH 06/26] added new course twit model and error --- .../Mooc/Notifications/domain/NewCourseTwit.ts | 11 +++++++++++ .../Mooc/Notifications/domain/NewCourseTwitError.ts | 7 +++++++ 2 files changed, 18 insertions(+) create mode 100644 src/Contexts/Mooc/Notifications/domain/NewCourseTwit.ts create mode 100644 src/Contexts/Mooc/Notifications/domain/NewCourseTwitError.ts diff --git a/src/Contexts/Mooc/Notifications/domain/NewCourseTwit.ts b/src/Contexts/Mooc/Notifications/domain/NewCourseTwit.ts new file mode 100644 index 0000000..7388982 --- /dev/null +++ b/src/Contexts/Mooc/Notifications/domain/NewCourseTwit.ts @@ -0,0 +1,11 @@ +import { CourseName } from '../../Shared/domain/Courses/CourseName'; +import { Twit } from './Twit'; +import { TwitMessage } from './TwitMessage'; + +export class NewCourseTwit extends Twit { + constructor(courseName: CourseName) { + super({ + message: new TwitMessage(`New course published. ${courseName}`), + }); + } +} diff --git a/src/Contexts/Mooc/Notifications/domain/NewCourseTwitError.ts b/src/Contexts/Mooc/Notifications/domain/NewCourseTwitError.ts new file mode 100644 index 0000000..617fd3d --- /dev/null +++ b/src/Contexts/Mooc/Notifications/domain/NewCourseTwitError.ts @@ -0,0 +1,7 @@ +import { NewCourseTwit } from "./NewCourseTwit"; + +export class NewCourseTwitError extends Error { + constructor(twit: NewCourseTwit) { + super(`Error twiting new course message ${twit.message}`); + } +} From f3b44343c81713035e22b05cc2abc40df6ae7eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Sun, 29 Nov 2020 16:55:34 +0000 Subject: [PATCH 07/26] added course created event --- .../domain/CourseCreatedDomainEvent.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/Contexts/Mooc/Notifications/domain/CourseCreatedDomainEvent.ts diff --git a/src/Contexts/Mooc/Notifications/domain/CourseCreatedDomainEvent.ts b/src/Contexts/Mooc/Notifications/domain/CourseCreatedDomainEvent.ts new file mode 100644 index 0000000..2745366 --- /dev/null +++ b/src/Contexts/Mooc/Notifications/domain/CourseCreatedDomainEvent.ts @@ -0,0 +1,54 @@ +import { DomainEvent } from '../../../Shared/domain/DomainEvent'; + +type CreateCourseDomainEventBody = { + readonly duration: string; + readonly name: string; +}; + +export class CourseCreatedDomainEvent extends DomainEvent { + static readonly EVENT_NAME = 'course.created'; + + readonly duration: string; + readonly name: string; + + constructor({ + id, + name, + duration, + eventId, + occurredOn + }: { + id: string; + eventId?: string; + duration: string; + name: string; + occurredOn?: Date; + }) { + super(CourseCreatedDomainEvent.EVENT_NAME, id, eventId, occurredOn); + this.duration = duration; + this.name = name; + } + + toPrimitive(): CreateCourseDomainEventBody { + const { name, duration } = this; + return { + name, + duration + }; + } + + static fromPrimitives( + aggregateId: string, + body: CreateCourseDomainEventBody, + eventId: string, + occurredOn: Date + ): DomainEvent { + return new CourseCreatedDomainEvent({ + id: aggregateId, + duration: body.duration, + name: body.name, + eventId, + occurredOn + }); + } +} From 21e77cfa0a38d6a7c9b319d0afdcb64f8bf71a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Sun, 29 Nov 2020 16:56:27 +0000 Subject: [PATCH 08/26] added twit sender interface and fake implementation --- src/Contexts/Mooc/Notifications/domain/TwitSender.ts | 5 +++++ .../Mooc/Notifications/infrastructure/FakeTwitSender.ts | 8 ++++++++ 2 files changed, 13 insertions(+) create mode 100644 src/Contexts/Mooc/Notifications/domain/TwitSender.ts create mode 100644 src/Contexts/Mooc/Notifications/infrastructure/FakeTwitSender.ts diff --git a/src/Contexts/Mooc/Notifications/domain/TwitSender.ts b/src/Contexts/Mooc/Notifications/domain/TwitSender.ts new file mode 100644 index 0000000..32a188b --- /dev/null +++ b/src/Contexts/Mooc/Notifications/domain/TwitSender.ts @@ -0,0 +1,5 @@ +import { Twit } from './Twit'; + +export interface TwitSender { + send(twit: Twit): Promise; +} diff --git a/src/Contexts/Mooc/Notifications/infrastructure/FakeTwitSender.ts b/src/Contexts/Mooc/Notifications/infrastructure/FakeTwitSender.ts new file mode 100644 index 0000000..01806b3 --- /dev/null +++ b/src/Contexts/Mooc/Notifications/infrastructure/FakeTwitSender.ts @@ -0,0 +1,8 @@ +import { Twit } from "../domain/Twit"; +import { TwitSender } from "../domain/TwitSender"; + +export default class FakeTwitSender implements TwitSender { + async send(twit: Twit): Promise { + // do nothing + } +} From 641f4cc6b7f007cd1c991a6cd1ef8d9fed89476e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Sun, 29 Nov 2020 16:57:15 +0000 Subject: [PATCH 09/26] added send new course twit handler and use case --- .../SendNewCourseTwit/SendNewCourseTwit.ts | 17 +++++++++++++++++ .../SendNewCourseTwitOnCourseCreated.ts | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwit.ts create mode 100644 src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated.ts diff --git a/src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwit.ts b/src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwit.ts new file mode 100644 index 0000000..77a90fb --- /dev/null +++ b/src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwit.ts @@ -0,0 +1,17 @@ +import { CourseName } from "../../../Shared/domain/Courses/CourseName"; +import { NewCourseTwit } from "../../domain/NewCourseTwit"; +import { NewCourseTwitError } from "../../domain/NewCourseTwitError"; +import { TwitSender } from "../../domain/TwitSender"; + +export default class SendNewCourseTwit { + constructor(private tweetSender: TwitSender) {} + + async run(courseName: CourseName): Promise { + const newCourseTweet = new NewCourseTwit(courseName); + try { + await this.tweetSender.send(newCourseTweet); + } catch (error) { + throw new NewCourseTwitError(newCourseTweet); + } + } +} diff --git a/src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated.ts b/src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated.ts new file mode 100644 index 0000000..1045b05 --- /dev/null +++ b/src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated.ts @@ -0,0 +1,18 @@ +import { DomainEventSubscriber } from '../../../../Shared/domain/DomainEventSubscriber'; +import { DomainEventClass } from '../../../../Shared/domain/DomainEvent'; +import { CourseCreatedDomainEvent } from '../../domain/CourseCreatedDomainEvent'; +import SendNewCourseTwit from './SendNewCourseTwit'; +import { CourseName } from '../../../Shared/domain/Courses/CourseName'; + +export default class SendNewCourseTweetOnCourseCreated implements DomainEventSubscriber { + constructor(private sendNewCourseTwit: SendNewCourseTwit) {} + + subscribedTo(): DomainEventClass[] { + return [CourseCreatedDomainEvent]; + } + + async on(domainEvent: CourseCreatedDomainEvent): Promise { + const courseName = new CourseName(domainEvent.name); + await this.sendNewCourseTwit.run(courseName); + } +} From 8916b19bc2a0908459c6ccbff4e086da34324b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Sun, 29 Nov 2020 16:57:42 +0000 Subject: [PATCH 10/26] twit sender mock --- .../Notifications/__mocks__/TwitSenderMock.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/Contexts/Mooc/Notifications/__mocks__/TwitSenderMock.ts diff --git a/tests/Contexts/Mooc/Notifications/__mocks__/TwitSenderMock.ts b/tests/Contexts/Mooc/Notifications/__mocks__/TwitSenderMock.ts new file mode 100644 index 0000000..2dc03b8 --- /dev/null +++ b/tests/Contexts/Mooc/Notifications/__mocks__/TwitSenderMock.ts @@ -0,0 +1,22 @@ +import { TwitSender } from '../../../../../src/Contexts/Mooc/Notifications/domain/TwitSender'; +import { Twit } from '../../../../../src/Contexts/Mooc/Notifications/domain/Twit'; + +export class TwitSenderMock implements TwitSender { + private sendSpy = jest.fn(); + + async send(twit: Twit): Promise { + this.sendSpy(twit); + } + + assertSentTimes(times: number): void { + expect(this.sendSpy.mock.calls.length).toBe(times); + } + + lastTwitSent(): Twit { + const sendCalls = this.sendSpy.mock.calls; + const lastSendCall = sendCalls[sendCalls.length - 1] || []; + const lastTwitSent = lastSendCall[0] as Twit; + + return lastTwitSent; + } +} From db95f798e2d620f5d80a115b0a5ff4f50a1ecfe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Sun, 29 Nov 2020 16:57:59 +0000 Subject: [PATCH 11/26] test use case --- .../SendNewCourseTwitOnCourseCreated.test.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated.test.ts diff --git a/tests/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated.test.ts b/tests/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated.test.ts new file mode 100644 index 0000000..4dc3a0e --- /dev/null +++ b/tests/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated.test.ts @@ -0,0 +1,66 @@ +import { TwitSenderMock } from '../../__mocks__/TwitSenderMock'; +import SendNewCourseTwit from '../../../../../../src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwit'; +import SendNewCourseTwitOnCourseCreated from '../../../../../../src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated'; +import { UuidMother } from '../../../../Shared/domain/UuidMother'; +import { CourseCreatedDomainEvent } from '../../../../../../src/Contexts/Mooc/Notifications/domain/CourseCreatedDomainEvent'; +import { NewCourseTwit } from '../../../../../../src/Contexts/Mooc/Notifications/domain/NewCourseTwit'; +import { WordMother } from '../../../../Shared/domain/WordMother'; +import { TwitSender } from '../../../../../../src/Contexts/Mooc/Notifications/domain/TwitSender'; +import { Twit } from '../../../../../../src/Contexts/Mooc/Notifications/domain/Twit'; +import { NewCourseTwitError } from '../../../../../../src/Contexts/Mooc/Notifications/domain/NewCourseTwitError'; + +describe('SendNewCourseTwitOnCourseCreated event handler', () => { + it('Send a new course twit', async () => { + const twitSenderMock = new TwitSenderMock(); + const sendNewCourseTwit = new SendNewCourseTwit(twitSenderMock); + const sendNewCourseTwitOnCourseCreated = new SendNewCourseTwitOnCourseCreated(sendNewCourseTwit); + const courseName = aNewCourseName(); + const domainEvent = aNewCourseDomainEvent(courseName); + + await sendNewCourseTwitOnCourseCreated.on(domainEvent); + + const lastTwitSent = twitSenderMock.lastTwitSent(); + twitSenderMock.assertSentTimes(1); + expect(lastTwitSent).toBeInstanceOf(NewCourseTwit); + expect(lastTwitSent.message.value).toEqual(`New course published. ${courseName}`); + }); + + it('throws a NewCourseTwitError if the twitSender fails', async () => { + const failingEmailSender = aFailingTwitSender(); + const sendNewCourseTwit = new SendNewCourseTwit(failingEmailSender); + const sendNewCourseTwitOnCourseCreated = new SendNewCourseTwitOnCourseCreated(sendNewCourseTwit); + + const domainEvent = aNewCourseDomainEvent(aNewCourseName()); + let error; + + try { + await sendNewCourseTwitOnCourseCreated.on(domainEvent); + } catch (e) { + error = e; + } + + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(NewCourseTwitError); + expect(error.message).toBe(`Error twiting new course message New course published. ${domainEvent.name}`); + }); +}); + +function aFailingTwitSender() { + return { + async send(twit: Twit) { + throw new Error('some error'); + } + } as TwitSender; +} + +function aNewCourseName(): string { + return WordMother.random(); +} + +function aNewCourseDomainEvent(courseName: string) { + return new CourseCreatedDomainEvent({ + id: UuidMother.random(), + duration: WordMother.random(), + name: courseName + }); +} From e235b5aad9b3bbe5b0857541b671a73057ca0a0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Sun, 29 Nov 2020 17:36:17 +0000 Subject: [PATCH 12/26] inject created classes --- .../Notifications/application.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/apps/mooc_backend/config/dependency-injection/Notifications/application.yaml b/src/apps/mooc_backend/config/dependency-injection/Notifications/application.yaml index ec4ae95..2503113 100644 --- a/src/apps/mooc_backend/config/dependency-injection/Notifications/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/Notifications/application.yaml @@ -10,5 +10,19 @@ services: Mooc.notifications.SendWelcomeUserEmailOnUserRegistered: class: ../../../../../Contexts/Mooc/Notifications/application/SendWelcomeUserEmail/SendWelcomeUserEmailOnUserRegistered arguments: ["@Mooc.notifications.SendWelcomeUserEmail"] + tags: + - { name: 'domainEventSubscriber' } + + Mooc.notifications.TwitSender: + class: ../../../../../Contexts/Mooc/Notifications/infrastructure/FakeTwitSender + arguments: [] + + Mooc.notifications.SendNewCourseTwit: + class: ../../../../../Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwit + arguments: ["@Mooc.notifications.TwitSender"] + + Mooc.notifications.SendNewCourseTwitOnCourseCreated: + class: ../../../../../Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated + arguments: ["@Mooc.notifications.SendNewCourseTwit"] tags: - { name: 'domainEventSubscriber' } \ No newline at end of file From d93c973266348e734855d729ac791bc1da3f4bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Mon, 30 Nov 2020 19:35:31 +0000 Subject: [PATCH 13/26] relocate application create course to a proper folder --- .../application/{ => CreateCourse}/CourseCreator.ts | 13 ++++++------- .../{ => CreateCourse}/CreateCourseCommand.ts | 2 +- .../CreateCourseCommandHandler.ts | 10 +++++----- .../{ => CreateCourse}/CreateCourseRequest.ts | 0 .../dependency-injection/Courses/application.yaml | 4 ++-- .../frontend/controllers/CoursesPostController.ts | 2 +- .../dependency-injection/Courses/application.yaml | 4 ++-- .../mooc_backend/controllers/CoursePutController.ts | 2 +- .../Mooc/Courses/application/CourseCreator.test.ts | 4 ++-- .../application/CreateCourseCommandMother.ts | 2 +- tests/Contexts/Mooc/Courses/domain/CourseMother.ts | 2 +- 11 files changed, 22 insertions(+), 23 deletions(-) rename src/Contexts/Mooc/Courses/application/{ => CreateCourse}/CourseCreator.ts (59%) rename src/Contexts/Mooc/Courses/application/{ => CreateCourse}/CreateCourseCommand.ts (84%) rename src/Contexts/Mooc/Courses/application/{ => CreateCourse}/CreateCourseCommandHandler.ts (66%) rename src/Contexts/Mooc/Courses/application/{ => CreateCourse}/CreateCourseRequest.ts (100%) diff --git a/src/Contexts/Mooc/Courses/application/CourseCreator.ts b/src/Contexts/Mooc/Courses/application/CreateCourse/CourseCreator.ts similarity index 59% rename from src/Contexts/Mooc/Courses/application/CourseCreator.ts rename to src/Contexts/Mooc/Courses/application/CreateCourse/CourseCreator.ts index 5cee3b3..fe96952 100644 --- a/src/Contexts/Mooc/Courses/application/CourseCreator.ts +++ b/src/Contexts/Mooc/Courses/application/CreateCourse/CourseCreator.ts @@ -1,10 +1,9 @@ -import { CourseRepository } from '../domain/CourseRepository'; -import { Course } from '../domain/Course'; -import { CreateCourseRequest } from './CreateCourseRequest'; -import { CourseId } from '../../Shared/domain/Courses/CourseId'; -import { CourseDuration } from '../domain/CourseDuration'; -import { EventBus } from '../../../Shared/domain/EventBus'; -import { CourseName } from '../../Shared/domain/Courses/CourseName'; +import { CourseRepository } from '../../domain/CourseRepository'; +import { Course } from '../../domain/Course'; +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { CourseDuration } from '../../domain/CourseDuration'; +import { EventBus } from '../../../../Shared/domain/EventBus'; +import { CourseName } from '../../../Shared/domain/Courses/CourseName'; type Params = { courseId: CourseId; diff --git a/src/Contexts/Mooc/Courses/application/CreateCourseCommand.ts b/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand.ts similarity index 84% rename from src/Contexts/Mooc/Courses/application/CreateCourseCommand.ts rename to src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand.ts index 884cf7d..5da981b 100644 --- a/src/Contexts/Mooc/Courses/application/CreateCourseCommand.ts +++ b/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand.ts @@ -1,4 +1,4 @@ -import { Command } from '../../../Shared/domain/Command'; +import { Command } from '../../../../Shared/domain/Command'; type Params = { id: string; diff --git a/src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler.ts b/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler.ts similarity index 66% rename from src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler.ts rename to src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler.ts index c5735f1..9477207 100644 --- a/src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler.ts +++ b/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler.ts @@ -1,10 +1,10 @@ import { CreateCourseCommand } from './CreateCourseCommand'; -import { CommandHandler } from '../../../Shared/domain/CommandHandler'; +import { CommandHandler } from '../../../../Shared/domain/CommandHandler'; import { CourseCreator } from './CourseCreator'; -import { Command } from '../../../Shared/domain/Command'; -import { CourseId } from '../../Shared/domain/Courses/CourseId'; -import { CourseDuration } from '../domain/CourseDuration'; -import { CourseName } from '../../Shared/domain/Courses/CourseName'; +import { Command } from '../../../../Shared/domain/Command'; +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { CourseDuration } from '../../domain/CourseDuration'; +import { CourseName } from '../../../Shared/domain/Courses/CourseName'; export class CreateCourseCommandHandler implements CommandHandler { constructor(private courseCreator: CourseCreator) {} diff --git a/src/Contexts/Mooc/Courses/application/CreateCourseRequest.ts b/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseRequest.ts similarity index 100% rename from src/Contexts/Mooc/Courses/application/CreateCourseRequest.ts rename to src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseRequest.ts diff --git a/src/apps/backoffice/frontend/config/dependency-injection/Courses/application.yaml b/src/apps/backoffice/frontend/config/dependency-injection/Courses/application.yaml index 40fddc3..07781b1 100644 --- a/src/apps/backoffice/frontend/config/dependency-injection/Courses/application.yaml +++ b/src/apps/backoffice/frontend/config/dependency-injection/Courses/application.yaml @@ -4,11 +4,11 @@ services: arguments: ['@Shared.ConnectionManager'] Mooc.courses.CourseCreator: - class: ../../../../../../Contexts/Mooc/Courses/application/CourseCreator + class: ../../../../../../Contexts/Mooc/Courses/application/CreateCourse/CourseCreator arguments: ['@Mooc.courses.CourseRepository', '@Shared.EventBus'] Mooc.courses.CreateCourseCommandHandler: - class: ../../../../../../Contexts/Mooc/Courses/application/CreateCourseCommandHandler + class: ../../../../../../Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler arguments: ['@Mooc.courses.CourseCreator'] tags: - { name: 'commandHandler' } diff --git a/src/apps/backoffice/frontend/controllers/CoursesPostController.ts b/src/apps/backoffice/frontend/controllers/CoursesPostController.ts index 204c996..b824036 100644 --- a/src/apps/backoffice/frontend/controllers/CoursesPostController.ts +++ b/src/apps/backoffice/frontend/controllers/CoursesPostController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import { CommandBus } from '../../../../Contexts/Shared/domain/CommandBus'; -import { CreateCourseCommand } from '../../../../Contexts/Mooc/Courses/application/CreateCourseCommand'; +import { CreateCourseCommand } from '../../../../Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand'; import { body, ValidationChain } from 'express-validator'; import { WebController } from './WebController'; diff --git a/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml b/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml index b0ab7c8..d538ae4 100644 --- a/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml @@ -5,11 +5,11 @@ services: arguments: ['@Shared.ConnectionManager'] Mooc.courses.CourseCreator: - class: ../../../../../Contexts/Mooc/Courses/application/CourseCreator + class: ../../../../../Contexts/Mooc/Courses/application/CreateCourse/CourseCreator arguments: ['@Mooc.courses.CourseRepository', '@Shared.EventBus'] Mooc.courses.CreateCourseCommandHandler: - class: ../../../../../Contexts/Mooc/Courses/application/CreateCourseCommandHandler + class: ../../../../../Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler arguments: ['@Mooc.courses.CourseCreator'] tags: - { name: 'commandHandler' } diff --git a/src/apps/mooc_backend/controllers/CoursePutController.ts b/src/apps/mooc_backend/controllers/CoursePutController.ts index 1863ab7..49cc70d 100644 --- a/src/apps/mooc_backend/controllers/CoursePutController.ts +++ b/src/apps/mooc_backend/controllers/CoursePutController.ts @@ -3,7 +3,7 @@ import httpStatus from 'http-status'; import { Controller } from './Controller'; import { CourseAlreadyExists } from '../../../Contexts/Mooc/Courses/domain/CourseAlreadyExists'; import { CommandBus } from '../../../Contexts/Shared/domain/CommandBus'; -import { CreateCourseCommand } from '../../../Contexts/Mooc/Courses/application/CreateCourseCommand'; +import { CreateCourseCommand } from '../../../Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand'; export class CoursePutController implements Controller { constructor(private commandBus: CommandBus) {} diff --git a/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts b/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts index a406009..03f1f49 100644 --- a/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts +++ b/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts @@ -1,9 +1,9 @@ -import { CourseCreator } from '../../../../../src/Contexts/Mooc/Courses/application/CourseCreator'; +import { CourseCreator } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourse/CourseCreator'; import { CourseMother } from '../domain/CourseMother'; import { CourseRepositoryMock } from '../__mocks__/CourseRepositoryMock'; import { CreateCourseCommandMother } from './CreateCourseCommandMother'; import EventBusMock from '../__mocks__/EventBusMock'; -import { CreateCourseCommandHandler } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler'; +import { CreateCourseCommandHandler } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler'; let repository: CourseRepositoryMock; let handler: CreateCourseCommandHandler; diff --git a/tests/Contexts/Mooc/Courses/application/CreateCourseCommandMother.ts b/tests/Contexts/Mooc/Courses/application/CreateCourseCommandMother.ts index 02b796d..cdaca2d 100644 --- a/tests/Contexts/Mooc/Courses/application/CreateCourseCommandMother.ts +++ b/tests/Contexts/Mooc/Courses/application/CreateCourseCommandMother.ts @@ -1,7 +1,7 @@ import { CourseDurationMother } from '../domain/CourseDurationMother'; import { CourseIdMother } from '../../Shared/domain/Courses/CourseIdMother'; import { CourseNameMother } from '../domain/CourseNameMother'; -import { CreateCourseCommand } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourseCommand'; +import { CreateCourseCommand } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand'; export class CreateCourseCommandMother { static create(id: string, name: string, duration: string): CreateCourseCommand { diff --git a/tests/Contexts/Mooc/Courses/domain/CourseMother.ts b/tests/Contexts/Mooc/Courses/domain/CourseMother.ts index ae2f2e8..1167441 100644 --- a/tests/Contexts/Mooc/Courses/domain/CourseMother.ts +++ b/tests/Contexts/Mooc/Courses/domain/CourseMother.ts @@ -5,7 +5,7 @@ import { Course } from '../../../../../src/Contexts/Mooc/Courses/domain/Course'; import { CourseIdMother } from '../../Shared/domain/Courses/CourseIdMother'; import { CourseNameMother } from './CourseNameMother'; import { CourseDurationMother } from './CourseDurationMother'; -import { CreateCourseCommand } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourseCommand'; +import { CreateCourseCommand } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand'; export class CourseMother { static create(id: CourseId, name: CourseName, duration: CourseDuration): Course { From c41d634acff73715d5d8842a5433d531225a3289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Mon, 30 Nov 2020 20:25:28 +0000 Subject: [PATCH 14/26] added finder use case --- .../application/GetCourse/CourseFinder.ts | 26 +++++++++++++++++++ .../application/GetCourse/GetCourseQuery.ts | 11 ++++++++ .../GetCourse/GetCourseQueryHandler.ts | 19 ++++++++++++++ .../application/GetCourse/GetCourseRequest.ts | 3 +++ .../GetCourse/GetCourseResponse.ts | 17 ++++++++++++ .../Mooc/Courses/domain/CourseNotFound.ts | 5 ++++ 6 files changed, 81 insertions(+) create mode 100644 src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts create mode 100644 src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQuery.ts create mode 100644 src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.ts create mode 100644 src/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequest.ts create mode 100644 src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse.ts create mode 100644 src/Contexts/Mooc/Courses/domain/CourseNotFound.ts diff --git a/src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts b/src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts new file mode 100644 index 0000000..82327f9 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts @@ -0,0 +1,26 @@ +import { CourseRepository } from '../../domain/CourseRepository'; +import { Course } from '../../domain/Course'; +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { CourseNotFound } from '../../domain/CourseNotFound'; +import { GetCourseResponse } from './GetCourseResponse'; +import { Nullable } from '../../../../Shared/domain/Nullable'; + +type Params = { + courseId: CourseId; +}; + +export class CourseFinder { + private repository: CourseRepository; + + constructor(repository: CourseRepository) { + this.repository = repository; + } + + async run({ courseId }: Params): Promise { + const course : Nullable = await this.repository.search(courseId); + if (!course) { + throw new CourseNotFound(); + } + return new GetCourseResponse(course); + } +} diff --git a/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQuery.ts b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQuery.ts new file mode 100644 index 0000000..0cda0ae --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQuery.ts @@ -0,0 +1,11 @@ +import { GetCourseRequest } from './GetCourseRequest'; +import { Query } from '../../../../Shared/domain/Query'; + +export class GetCourseQuery extends Query { + id: string; + + constructor({ id }: GetCourseRequest) { + super(); + this.id = id; + } +} diff --git a/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.ts b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.ts new file mode 100644 index 0000000..735fb00 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.ts @@ -0,0 +1,19 @@ +import { GetCourseQuery } from './GetCourseQuery'; +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { CourseFinder } from './CourseFinder'; +import { GetCourseResponse } from './GetCourseResponse'; +import { QueryHandler } from '../../../../Shared/domain/QueryHandler'; +import { Query } from '../../../../Shared/domain/Query'; + +export class GetCourseQueryHandler implements QueryHandler { + constructor(private courseFinder: CourseFinder) {} + + subscribedTo(): Query { + return GetCourseQuery; + } + + async handle(query: GetCourseQuery): Promise { + const courseId = new CourseId(query.id); + return this.courseFinder.run({ courseId }); + } +} diff --git a/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequest.ts b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequest.ts new file mode 100644 index 0000000..b3b8015 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequest.ts @@ -0,0 +1,3 @@ +export type GetCourseRequest = { + id: string; +}; diff --git a/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse.ts b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse.ts new file mode 100644 index 0000000..26601d8 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse.ts @@ -0,0 +1,17 @@ +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { CourseName } from '../../../Shared/domain/Courses/CourseName'; +import { Course } from '../../domain/Course'; +import { CourseDuration } from '../../domain/CourseDuration'; + +export class GetCourseResponse { + readonly id: CourseId; + readonly name: CourseName; + readonly duration: CourseDuration; + + constructor(course: Course) { + this.id = course.id; + this.name = course.name; + this.duration = course.duration; + } +} + \ No newline at end of file diff --git a/src/Contexts/Mooc/Courses/domain/CourseNotFound.ts b/src/Contexts/Mooc/Courses/domain/CourseNotFound.ts new file mode 100644 index 0000000..678ce6b --- /dev/null +++ b/src/Contexts/Mooc/Courses/domain/CourseNotFound.ts @@ -0,0 +1,5 @@ +export class CourseNotFound extends Error { + constructor() { + super('The course does not exists'); + } +} From 8fc6dc8d2be3efff651d8543128646e0ca3e94aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Tue, 1 Dec 2020 20:34:45 +0000 Subject: [PATCH 15/26] added get controller --- .../controllers/CourseGetController.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/apps/mooc_backend/controllers/CourseGetController.ts diff --git a/src/apps/mooc_backend/controllers/CourseGetController.ts b/src/apps/mooc_backend/controllers/CourseGetController.ts new file mode 100644 index 0000000..edf64de --- /dev/null +++ b/src/apps/mooc_backend/controllers/CourseGetController.ts @@ -0,0 +1,26 @@ +import { Controller } from './Controller'; +import { Request, Response } from 'express'; +import httpStatus = require('http-status'); + +import { QueryBus } from '../../../Contexts/Shared/domain/QueryBus'; +import { GetCourseQuery } from '../../../Contexts/Mooc/Courses/application/GetCourse/GetCourseQuery'; +import { GetCourseResponse } from '../../../Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse'; +import { CourseNotFound } from '../../../Contexts/Mooc/Courses/domain/CourseNotFound'; + +export class CourseGetController implements Controller { + constructor(private queryBus: QueryBus) {} + async run(req: Request, res: Response): Promise { + try { + const id: string = req.params.id; + const query = new GetCourseQuery({id}); + const course = await this.queryBus.ask(query); + res.status(httpStatus.OK).send(course); + } catch (e) { + if (e instanceof CourseNotFound) { + res.status(httpStatus.NOT_FOUND).send(); + } else { + res.status(httpStatus.INTERNAL_SERVER_ERROR).send(); + } + } + } +} From 707e9b44f731762cbe72982cbee48a04ae2de0f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Tue, 1 Dec 2020 20:34:58 +0000 Subject: [PATCH 16/26] added get course route --- src/apps/mooc_backend/routes/courses.route.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/apps/mooc_backend/routes/courses.route.ts b/src/apps/mooc_backend/routes/courses.route.ts index c49fce9..c7e3de5 100644 --- a/src/apps/mooc_backend/routes/courses.route.ts +++ b/src/apps/mooc_backend/routes/courses.route.ts @@ -5,6 +5,9 @@ export const register = (router: Router) => { const coursePutController = container.get('Apps.mooc.controllers.CoursePutController'); router.put('/courses/:id', (req: Request, res: Response) => coursePutController.run(req, res)); + const courseGetController = container.get('Apps.mooc.controllers.CourseGetController'); + router.get('/courses/:id', (req: Request, res: Response) => courseGetController.run(req, res)); + const coursesCounterGetController = container.get('Apps.mooc.controllers.CoursesCounterGetController'); router.get('/courses-counter', (req: Request, res: Response) => coursesCounterGetController.run(req, res)); }; From 816952d9b3b47ca56b6307ee3bbd1d5218c887e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Tue, 1 Dec 2020 20:35:31 +0000 Subject: [PATCH 17/26] change get course response object in order to return primitive values --- .../application/GetCourse/GetCourseResponse.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse.ts b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse.ts index 26601d8..744d46a 100644 --- a/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse.ts +++ b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse.ts @@ -1,17 +1,13 @@ -import { CourseId } from '../../../Shared/domain/Courses/CourseId'; -import { CourseName } from '../../../Shared/domain/Courses/CourseName'; import { Course } from '../../domain/Course'; -import { CourseDuration } from '../../domain/CourseDuration'; - export class GetCourseResponse { - readonly id: CourseId; - readonly name: CourseName; - readonly duration: CourseDuration; + readonly id: string; + readonly name: string; + readonly duration: string; constructor(course: Course) { - this.id = course.id; - this.name = course.name; - this.duration = course.duration; + this.id = course.id.value; + this.name = course.name.value; + this.duration = course.duration.value; } } \ No newline at end of file From c10e21b7def794740705ee76ddff5dcd279ffdda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Tue, 1 Dec 2020 20:35:59 +0000 Subject: [PATCH 18/26] added dependency injection configuration --- .../dependency-injection/Courses/application.yaml | 10 ++++++++++ .../config/dependency-injection/apps/application.yaml | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml b/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml index d538ae4..9df8fde 100644 --- a/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml @@ -13,3 +13,13 @@ services: arguments: ['@Mooc.courses.CourseCreator'] tags: - { name: 'commandHandler' } + + Mooc.courses.CourseFinder: + class: ../../../../../Contexts/Mooc/Courses/application/GetCourse/CourseFinder + arguments: ["@Mooc.courses.CourseRepository"] + + Mooc.courses.GetCourseQueryHandler: + class: ../../../../../Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler + arguments: ["@Mooc.courses.CourseFinder"] + tags: + - { name: 'queryHandler' } \ No newline at end of file diff --git a/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml b/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml index 05e0acd..b8ab424 100644 --- a/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml @@ -11,3 +11,7 @@ services: Apps.mooc.controllers.CoursesCounterGetController: class: ../../../controllers/CoursesCounterGetController arguments: ["@Shared.QueryBus"] + + Apps.mooc.controllers.CourseGetController: + class: ../../../controllers/CourseGetController + arguments: ["@Shared.QueryBus"] \ No newline at end of file From 51434cf47a01913ecd5683edaef147e20186fbf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Tue, 1 Dec 2020 20:55:37 +0000 Subject: [PATCH 19/26] added get course query handler tests --- .../Courses/__mocks__/CourseRepositoryMock.ts | 13 +++++- .../GetCourse/GetCourseQueryHandler.test.ts | 42 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.test.ts diff --git a/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts b/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts index f7e8205..638bb6e 100644 --- a/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts +++ b/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts @@ -6,6 +6,7 @@ import { Nullable } from '../../../../../src/Contexts/Shared/domain/Nullable'; export class CourseRepositoryMock implements CourseRepository { private mockSave = jest.fn(); private mockSearch = jest.fn(); + private course: Nullable = null; async save(course: Course): Promise { this.mockSave(course); @@ -19,7 +20,8 @@ export class CourseRepositoryMock implements CourseRepository { } async search(id: CourseId): Promise> { - return this.mockSearch(id); + this.mockSearch(id); + return this.course; } whenSearchThenReturn(value: Nullable): void { @@ -29,4 +31,13 @@ export class CourseRepositoryMock implements CourseRepository { assertLastSearchedCourseIs(expected: CourseId): void { expect(this.mockSearch).toHaveBeenCalledWith(expected); } + + assertSearch(expected: CourseId) { + expect(this.mockSearch).toHaveBeenCalled(); + expect(this.mockSearch).toHaveBeenCalledWith(expected); + } + + returnOnSearch(course: Course) { + this.course = course; + } } diff --git a/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.test.ts b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.test.ts new file mode 100644 index 0000000..e52b4c0 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.test.ts @@ -0,0 +1,42 @@ +import { CourseFinder } from './../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder'; +import { CourseRepositoryMock } from '../../__mocks__/CourseRepositoryMock'; +import { CourseMother } from '../../domain/CourseMother'; +import { GetCourseQuery } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQuery'; +import { CourseIdMother } from '../../../Shared/domain/Courses/CourseIdMother'; +import { GetCourseQueryHandler } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler'; +import { CourseNotFound } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseNotFound'; +import { GetCourseResponse } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse'; + +describe('GetCourse QueryHandler', () => { + let repository: CourseRepositoryMock; + + beforeEach(() => { + repository = new CourseRepositoryMock(); + }); + + + it('should find an existing courses counter', async () => { + const course = CourseMother.random(); + const id = course.id; + repository.returnOnSearch(course); + + const handler = new GetCourseQueryHandler(new CourseFinder(repository)); + + const query = new GetCourseQuery({ id: id.value }); + const response = await handler.handle(query); + + repository.assertSearch(id); + + const expected = new GetCourseResponse(course); + expect(expected).toEqual(response); + }); + + it('should throw an exception when courses counter does not exists', async () => { + const handler = new GetCourseQueryHandler(new CourseFinder(repository)); + + const id = CourseIdMother.random(); + const query = new GetCourseQuery({id : id.value}); + + await expect(handler.handle(query)).rejects.toBeInstanceOf(CourseNotFound); + }); +}); From fc1f07cdf14c2fa0f7a370858045a2bfc2c65f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Wed, 2 Dec 2020 19:36:09 +0000 Subject: [PATCH 20/26] added course finder tests --- .../application/GetCourse/CourseFinder.ts | 2 +- .../GetCourse/CourseFinder.test.ts | 34 +++++++++++++++++++ .../GetCourse/GetCourseQueryMother.ts | 14 ++++++++ .../GetCourse/GetCourseRequestMother.ts | 14 ++++++++ .../application/GetCourse/ParamsMother.ts | 8 +++++ 5 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.test.ts create mode 100644 tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryMother.ts create mode 100644 tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequestMother.ts create mode 100644 tests/Contexts/Mooc/Courses/application/GetCourse/ParamsMother.ts diff --git a/src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts b/src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts index 82327f9..34d765e 100644 --- a/src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts +++ b/src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts @@ -5,7 +5,7 @@ import { CourseNotFound } from '../../domain/CourseNotFound'; import { GetCourseResponse } from './GetCourseResponse'; import { Nullable } from '../../../../Shared/domain/Nullable'; -type Params = { +export type Params = { courseId: CourseId; }; diff --git a/tests/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.test.ts b/tests/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.test.ts new file mode 100644 index 0000000..f995ad9 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.test.ts @@ -0,0 +1,34 @@ +import { CourseFinder } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder'; +import { CourseRepositoryMock } from '../../__mocks__/CourseRepositoryMock'; +import { CourseMother } from '../../domain/CourseMother'; +import { GetCourseResponse } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse'; +import { ParamsMother } from './ParamsMother'; +import { CourseIdMother } from '../../../Shared/domain/Courses/CourseIdMother'; +import { CourseNotFound } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseNotFound'; + +let repository: CourseRepositoryMock; +let finder: CourseFinder; + + +beforeEach(() => { + repository = new CourseRepositoryMock(); + finder = new CourseFinder(repository); +}); + +it('should get a course', async () => { + const course = CourseMother.random(); + const id = course.id; + repository.returnOnSearch(course); + const params = ParamsMother.create(id); + const response = await finder.run(params); + + repository.assertSearch(id); + const expected = new GetCourseResponse(course); + expect(expected).toEqual(response); +}); + +it('should throw an exception when courses counter does not exists', async () => { + const id = CourseIdMother.random() + const params = ParamsMother.create(id); + await expect(finder.run(params)).rejects.toBeInstanceOf(CourseNotFound); +}); diff --git a/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryMother.ts b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryMother.ts new file mode 100644 index 0000000..00515a7 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryMother.ts @@ -0,0 +1,14 @@ +import { GetCourseQuery } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQuery'; +import { GetCourseRequestMother } from './GetCourseRequestMother'; + +export class GetCourseQueryMother { + static random(): GetCourseQuery { + const getCourseRequest = GetCourseRequestMother.random() + return new GetCourseQuery(getCourseRequest); + } + + static create(id: string): GetCourseQuery { + const getCourseRequest = GetCourseRequestMother.create(id) + return new GetCourseQuery(getCourseRequest) + } +} diff --git a/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequestMother.ts b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequestMother.ts new file mode 100644 index 0000000..09dbf90 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequestMother.ts @@ -0,0 +1,14 @@ +import { GetCourseRequest } from "../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequest"; +import { UuidMother } from '../../../../Shared/domain/UuidMother'; + +export class GetCourseRequestMother { + static random(): GetCourseRequest { + const id = UuidMother.random() + return { id }; + } + + static create(id: string): GetCourseRequest { + return { id }; + } + } + \ No newline at end of file diff --git a/tests/Contexts/Mooc/Courses/application/GetCourse/ParamsMother.ts b/tests/Contexts/Mooc/Courses/application/GetCourse/ParamsMother.ts new file mode 100644 index 0000000..43a2db1 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/GetCourse/ParamsMother.ts @@ -0,0 +1,8 @@ +import { Params } from './../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder'; +import { CourseId } from '../../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseId'; + +export class ParamsMother { + static create(id: CourseId): Params { + return { courseId: id }; + } + } \ No newline at end of file From a1ddf947b3e9ad309539f73484a640f571b78d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Wed, 2 Dec 2020 20:12:20 +0000 Subject: [PATCH 21/26] added api test --- .../mongo/MongoEnvironmentArranger.ts | 6 ++++++ .../features/courses/get-course.feature | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 tests/apps/mooc_backend/features/courses/get-course.feature diff --git a/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts b/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts index 7e52276..4686374 100644 --- a/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts +++ b/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts @@ -8,6 +8,7 @@ export class MongoEnvironmentArranger extends EnvironmentArranger { public async arrange(): Promise { await this.cleanDatabase(); + await this.addCourse(); } protected async cleanDatabase(): Promise { @@ -28,6 +29,11 @@ export class MongoEnvironmentArranger extends EnvironmentArranger { return collections.map(collection => collection.name); } + private async addCourse(): Promise { + const client = await this.client(); + await client.db().collection("courses").insertOne({ id: "ef8ac118-8d7f-49cc-abec-78e0d05af80b", name: 'Test Course!', duration: '1'}) + } + protected client(): Promise { return this._client; } diff --git a/tests/apps/mooc_backend/features/courses/get-course.feature b/tests/apps/mooc_backend/features/courses/get-course.feature new file mode 100644 index 0000000..7e57230 --- /dev/null +++ b/tests/apps/mooc_backend/features/courses/get-course.feature @@ -0,0 +1,21 @@ +Feature: Obtain the total number of courses + In order to have a course info + As a user + I want to see the courses + + # Scenario: Retrieve a course + # When I send a GET request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80b" + # Then the response status code should be 200 + # And the response content should be: + # """ + # { + # "id": "ef8ac118-8d7f-49cc-abec-78e0d05af80b", + # "duration": "1", + # "name": "Test Course!" + # } + # """ + + Scenario: Get a Not found Exception + When I send a GET request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80c" + Then the response status code should be 404 + And the response should be empty \ No newline at end of file From 61ef769ee7405d29352e6c3285ed5c61d4ec17b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Thu, 3 Dec 2020 19:56:46 +0000 Subject: [PATCH 22/26] added api test completed --- .../mongo/MongoEnvironmentArranger.ts | 3 ++- .../features/courses/get-course.feature | 24 +++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts b/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts index 4686374..fd4298a 100644 --- a/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts +++ b/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts @@ -31,7 +31,8 @@ export class MongoEnvironmentArranger extends EnvironmentArranger { private async addCourse(): Promise { const client = await this.client(); - await client.db().collection("courses").insertOne({ id: "ef8ac118-8d7f-49cc-abec-78e0d05af80b", name: 'Test Course!', duration: '1'}) + const id = "ef8ac118-8d7f-49cc-abec-78e0d05af80b"; + await client.db().collection("courses").updateOne({ _id: id } ,{$set: { id, name: 'Test Course!', duration: '1' } }, { upsert: true }); } protected client(): Promise { diff --git a/tests/apps/mooc_backend/features/courses/get-course.feature b/tests/apps/mooc_backend/features/courses/get-course.feature index 7e57230..cb329e8 100644 --- a/tests/apps/mooc_backend/features/courses/get-course.feature +++ b/tests/apps/mooc_backend/features/courses/get-course.feature @@ -1,19 +1,19 @@ -Feature: Obtain the total number of courses +Feature: Obtain a course In order to have a course info As a user I want to see the courses - # Scenario: Retrieve a course - # When I send a GET request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80b" - # Then the response status code should be 200 - # And the response content should be: - # """ - # { - # "id": "ef8ac118-8d7f-49cc-abec-78e0d05af80b", - # "duration": "1", - # "name": "Test Course!" - # } - # """ + Scenario: Retrieve a course + When I send a GET request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80b" + Then the response status code should be 200 + And the response content should be: + """ + { + "id": "ef8ac118-8d7f-49cc-abec-78e0d05af80b", + "duration": "1", + "name": "Test Course!" + } + """ Scenario: Get a Not found Exception When I send a GET request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80c" From 18909094232d46d0905af535029b128ad0c43539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Sat, 5 Dec 2020 09:47:20 +0000 Subject: [PATCH 23/26] Feature/rename course (#1) * course finder refactor to domain layer * missing config * rename course mother function and update references * removed unused class * added rename to domain class and create course renamed event * added test to course rename * added test to course rename domain model * added rename course handler and tests * added controller and route * added dependency injection config * added test scenario --- docker-compose.yml | 2 +- .../CreateCourse/CreateCourseRequest.ts | 5 -- .../application/GetCourse/CourseFinder.ts | 16 ++---- .../application/RenameCourse/CourseRenamer.ts | 30 +++++++++++ .../RenameCourse/RenameCourseCommand.ts | 17 ++++++ .../RenameCourseCommandHandler.ts | 20 +++++++ src/Contexts/Mooc/Courses/domain/Course.ts | 18 ++++++- .../Mooc/Courses/domain/CourseFinder.ts | 21 ++++++++ .../domain/CourseRenamedDomainEvent.ts | 54 +++++++++++++++++++ .../Courses/application.yaml | 19 ++++++- .../apps/application.yaml | 4 ++ .../controllers/CourseRenameController.ts | 28 ++++++++++ src/apps/mooc_backend/routes/courses.route.ts | 3 ++ .../Courses/application/CourseCreator.test.ts | 2 +- .../CourseRename/CourseRenamer.test.ts | 40 ++++++++++++++ .../RenameCourseCommandHandler.test.ts | 43 +++++++++++++++ .../CourseRename/RenameCourseCommandMother.ts | 14 +++++ .../GetCourse/CourseFinder.test.ts | 6 ++- .../GetCourse/GetCourseQueryHandler.test.ts | 7 ++- .../Mooc/Courses/domain/Course.test.ts | 15 +++++- .../Mooc/Courses/domain/CourseFinder.test.ts | 31 +++++++++++ .../Mooc/Courses/domain/CourseMother.ts | 11 +++- .../features/courses/rename-course.feature | 24 +++++++++ 23 files changed, 402 insertions(+), 28 deletions(-) delete mode 100644 src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseRequest.ts create mode 100644 src/Contexts/Mooc/Courses/application/RenameCourse/CourseRenamer.ts create mode 100644 src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand.ts create mode 100644 src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommandHandler.ts create mode 100644 src/Contexts/Mooc/Courses/domain/CourseFinder.ts create mode 100644 src/Contexts/Mooc/Courses/domain/CourseRenamedDomainEvent.ts create mode 100644 src/apps/mooc_backend/controllers/CourseRenameController.ts create mode 100644 tests/Contexts/Mooc/Courses/application/CourseRename/CourseRenamer.test.ts create mode 100644 tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandHandler.test.ts create mode 100644 tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandMother.ts create mode 100644 tests/Contexts/Mooc/Courses/domain/CourseFinder.test.ts create mode 100644 tests/apps/mooc_backend/features/courses/rename-course.feature diff --git a/docker-compose.yml b/docker-compose.yml index 9fe833c..d7dcc89 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ services: mooc-backend: <<: *default-app - command: bash -c "npm run build && npm run start" + command: bash -c "npm run build && npm run dev" ports: - 3000:3000 diff --git a/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseRequest.ts b/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseRequest.ts deleted file mode 100644 index dcae7a6..0000000 --- a/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseRequest.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type CreateCourseRequest = { - id: string; - name: string; - duration: string; -}; diff --git a/src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts b/src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts index 34d765e..80c0548 100644 --- a/src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts +++ b/src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts @@ -1,26 +1,20 @@ -import { CourseRepository } from '../../domain/CourseRepository'; -import { Course } from '../../domain/Course'; +import { CourseFinder as DomainCourseFinder } from '../../domain/CourseFinder'; import { CourseId } from '../../../Shared/domain/Courses/CourseId'; -import { CourseNotFound } from '../../domain/CourseNotFound'; import { GetCourseResponse } from './GetCourseResponse'; -import { Nullable } from '../../../../Shared/domain/Nullable'; export type Params = { courseId: CourseId; }; export class CourseFinder { - private repository: CourseRepository; + private courseFinder: DomainCourseFinder; - constructor(repository: CourseRepository) { - this.repository = repository; + constructor(courseFinder: DomainCourseFinder) { + this.courseFinder = courseFinder; } async run({ courseId }: Params): Promise { - const course : Nullable = await this.repository.search(courseId); - if (!course) { - throw new CourseNotFound(); - } + const course = await this.courseFinder.run(courseId); return new GetCourseResponse(course); } } diff --git a/src/Contexts/Mooc/Courses/application/RenameCourse/CourseRenamer.ts b/src/Contexts/Mooc/Courses/application/RenameCourse/CourseRenamer.ts new file mode 100644 index 0000000..ba8cdd4 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/RenameCourse/CourseRenamer.ts @@ -0,0 +1,30 @@ +import { CourseRepository } from '../../domain/CourseRepository'; +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { EventBus } from '../../../../Shared/domain/EventBus'; +import { CourseName } from '../../../Shared/domain/Courses/CourseName'; +import { CourseFinder } from '../../domain/CourseFinder'; + +type Params = { + courseId: CourseId; + courseName: CourseName; +}; + +export class CourseRenamer { + private courseFinder: CourseFinder; + private eventBus: EventBus; + private repository: CourseRepository; + + constructor(courseFinder: CourseFinder, repository: CourseRepository, eventBus: EventBus) { + this.courseFinder = courseFinder; + this.eventBus = eventBus; + this.repository = repository; + } + + async run({ courseId, courseName }: Params): Promise { + const course = await this.courseFinder.run(courseId); + course.rename(courseName); + + await this.repository.save(course); + await this.eventBus.publish(course.pullDomainEvents()); + } +} \ No newline at end of file diff --git a/src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand.ts b/src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand.ts new file mode 100644 index 0000000..b0f8c1a --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand.ts @@ -0,0 +1,17 @@ +import { Command } from '../../../../Shared/domain/Command'; + +type Params = { + id: string; + name: string; +}; + +export class RenameCourseCommand extends Command { + id: string; + name: string; + + constructor({ id, name }: Params) { + super(); + this.id = id; + this.name = name; + } +} diff --git a/src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommandHandler.ts b/src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommandHandler.ts new file mode 100644 index 0000000..506c3c0 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommandHandler.ts @@ -0,0 +1,20 @@ +import { CommandHandler } from '../../../../Shared/domain/CommandHandler'; +import { Command } from '../../../../Shared/domain/Command'; +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { CourseName } from '../../../Shared/domain/Courses/CourseName'; +import { CourseRenamer } from './CourseRenamer'; +import { RenameCourseCommand } from './RenameCourseCommand'; + +export class RenameCourseCommandHandler implements CommandHandler { + constructor(private courseRenamer: CourseRenamer) {} + + subscribedTo(): Command { + return RenameCourseCommand; + } + + async handle(command: RenameCourseCommand): Promise { + const courseId = new CourseId(command.id); + const courseName = new CourseName(command.name); + await this.courseRenamer.run({ courseId, courseName }); + } +} diff --git a/src/Contexts/Mooc/Courses/domain/Course.ts b/src/Contexts/Mooc/Courses/domain/Course.ts index ab7f55f..ab7e2db 100644 --- a/src/Contexts/Mooc/Courses/domain/Course.ts +++ b/src/Contexts/Mooc/Courses/domain/Course.ts @@ -3,11 +3,12 @@ import { CourseCreatedDomainEvent } from './CourseCreatedDomainEvent'; import { CourseDuration } from './CourseDuration'; import { CourseId } from '../../Shared/domain/Courses/CourseId'; import { CourseName } from '../../Shared/domain/Courses/CourseName'; +import { CourseRenamedDomainEvent } from './CourseRenamedDomainEvent'; export class Course extends AggregateRoot { readonly id: CourseId; - readonly name: CourseName; - readonly duration: CourseDuration; + name: CourseName; + duration: CourseDuration; constructor(id: CourseId, name: CourseName, duration: CourseDuration) { super(); @@ -30,6 +31,19 @@ export class Course extends AggregateRoot { return course; } + rename(name: CourseName) { + const oldName = this.name; + this.name = name; + + this.record( + new CourseRenamedDomainEvent({ + id: this.id.value, + oldName: oldName.value, + newName: name.value + }) + ) + } + static fromPrimitives(plainData: { id: string; name: string; duration: string }): Course { return new Course( new CourseId(plainData.id), diff --git a/src/Contexts/Mooc/Courses/domain/CourseFinder.ts b/src/Contexts/Mooc/Courses/domain/CourseFinder.ts new file mode 100644 index 0000000..ea0c1c0 --- /dev/null +++ b/src/Contexts/Mooc/Courses/domain/CourseFinder.ts @@ -0,0 +1,21 @@ +import { CourseRepository } from './CourseRepository'; +import { CourseId } from '../../Shared/domain/Courses/CourseId'; +import { Course } from './Course'; +import { Nullable } from '../../../Shared/domain/Nullable'; +import { CourseNotFound } from './CourseNotFound'; + +export class CourseFinder { + private repository: CourseRepository; + + constructor(repository: CourseRepository) { + this.repository = repository; + } + + async run(courseId : CourseId): Promise { + const course : Nullable = await this.repository.search(courseId); + if (!course) { + throw new CourseNotFound(); + } + return course; + } + } \ No newline at end of file diff --git a/src/Contexts/Mooc/Courses/domain/CourseRenamedDomainEvent.ts b/src/Contexts/Mooc/Courses/domain/CourseRenamedDomainEvent.ts new file mode 100644 index 0000000..1c5e3a6 --- /dev/null +++ b/src/Contexts/Mooc/Courses/domain/CourseRenamedDomainEvent.ts @@ -0,0 +1,54 @@ +import { DomainEvent } from '../../../Shared/domain/DomainEvent'; + +type CourseRenamedDomainEventBody = { + readonly oldName: string; + readonly newName: string; +}; + +export class CourseRenamedDomainEvent extends DomainEvent { + static readonly EVENT_NAME = 'course.renamed'; + + readonly oldName: string; + readonly newName: string; + + constructor({ + id, + oldName, + newName, + eventId, + occurredOn + }: { + id: string; + eventId?: string; + oldName: string; + newName: string; + occurredOn?: Date; + }) { + super(CourseRenamedDomainEvent.EVENT_NAME, id, eventId, occurredOn); + this.oldName = oldName; + this.newName = newName; + } + + toPrimitive(): CourseRenamedDomainEventBody { + const { newName: oldName, oldName: newName } = this; + return { + oldName, + newName + }; + } + + static fromPrimitives( + aggregateId: string, + body: CourseRenamedDomainEventBody, + eventId: string, + occurredOn: Date + ): DomainEvent { + return new CourseRenamedDomainEvent({ + id: aggregateId, + oldName: body.oldName, + newName: body.newName, + eventId, + occurredOn + }); + } +} diff --git a/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml b/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml index 9df8fde..63fecaa 100644 --- a/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml @@ -14,12 +14,27 @@ services: tags: - { name: 'commandHandler' } + Mooc.courses.DomainCourseFinder: + class: ../../../../../Contexts/Mooc/Courses/domain/CourseFinder + arguments: ["@Mooc.courses.CourseRepository"] + Mooc.courses.CourseFinder: class: ../../../../../Contexts/Mooc/Courses/application/GetCourse/CourseFinder - arguments: ["@Mooc.courses.CourseRepository"] + arguments: ["@Mooc.courses.DomainCourseFinder"] Mooc.courses.GetCourseQueryHandler: class: ../../../../../Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler arguments: ["@Mooc.courses.CourseFinder"] tags: - - { name: 'queryHandler' } \ No newline at end of file + - { name: 'queryHandler' } + + Mooc.courses.CourseRenamer: + class: ../../../../../Contexts/Mooc/Courses/application/RenameCourse/CourseRenamer + arguments: ["@Mooc.courses.DomainCourseFinder", "@Mooc.courses.CourseRepository", "@Shared.EventBus"] + + Mooc.courses.RenameCourseCommandHandler: + class: ../../../../../Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommandHandler + arguments: ['@Mooc.courses.CourseRenamer'] + tags: + - { name: 'commandHandler' } + diff --git a/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml b/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml index b8ab424..7fdec48 100644 --- a/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml @@ -4,6 +4,10 @@ services: class: ../../../controllers/CoursePutController arguments: ["@Shared.CommandBus"] + Apps.mooc.controllers.CourseRenameController: + class: ../../../controllers/CourseRenameController + arguments: ["@Shared.CommandBus"] + Apps.mooc.controllers.StatusGetController: class: ../../../controllers/StatusGetController arguments: [] diff --git a/src/apps/mooc_backend/controllers/CourseRenameController.ts b/src/apps/mooc_backend/controllers/CourseRenameController.ts new file mode 100644 index 0000000..7e01d89 --- /dev/null +++ b/src/apps/mooc_backend/controllers/CourseRenameController.ts @@ -0,0 +1,28 @@ +import { Request, Response } from 'express'; +import { Controller } from './Controller'; +import { CommandBus } from '../../../Contexts/Shared/domain/CommandBus'; +import { RenameCourseCommand } from '../../../Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand'; +import { CourseNotFound } from '../../../Contexts/Mooc/Courses/domain/CourseNotFound'; +import httpStatus from 'http-status'; + +export class CourseRenameController implements Controller { + constructor(private commandBus: CommandBus) {} + + async run(req: Request, res: Response) { + const id: string = req.params.id; + const name: string = req.body.name; + const renameCourseCommand = new RenameCourseCommand({ id, name }); + + try { + await this.commandBus.dispatch(renameCourseCommand); + } catch (error) { + if (error instanceof CourseNotFound) { + res.status(httpStatus.NOT_FOUND).send(error.message); + } else { + res.status(httpStatus.INTERNAL_SERVER_ERROR).json(error); + } + } + + res.status(httpStatus.NO_CONTENT).send(); + } + } \ No newline at end of file diff --git a/src/apps/mooc_backend/routes/courses.route.ts b/src/apps/mooc_backend/routes/courses.route.ts index c7e3de5..03176f4 100644 --- a/src/apps/mooc_backend/routes/courses.route.ts +++ b/src/apps/mooc_backend/routes/courses.route.ts @@ -5,6 +5,9 @@ export const register = (router: Router) => { const coursePutController = container.get('Apps.mooc.controllers.CoursePutController'); router.put('/courses/:id', (req: Request, res: Response) => coursePutController.run(req, res)); + const courseRenameController = container.get('Apps.mooc.controllers.CourseRenameController'); + router.put('/courses/:id/rename', (req: Request, res: Response) => courseRenameController.run(req, res)); + const courseGetController = container.get('Apps.mooc.controllers.CourseGetController'); router.get('/courses/:id', (req: Request, res: Response) => courseGetController.run(req, res)); diff --git a/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts b/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts index 03f1f49..11997e1 100644 --- a/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts +++ b/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts @@ -20,6 +20,6 @@ it('should create a valid course', async () => { const command = CreateCourseCommandMother.random(); await handler.handle(command); - const course = CourseMother.fromCommand(command); + const course = CourseMother.fromCreateCommand(command); repository.assertLastSavedCourseIs(course); }); diff --git a/tests/Contexts/Mooc/Courses/application/CourseRename/CourseRenamer.test.ts b/tests/Contexts/Mooc/Courses/application/CourseRename/CourseRenamer.test.ts new file mode 100644 index 0000000..5aea116 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/CourseRename/CourseRenamer.test.ts @@ -0,0 +1,40 @@ + +import { CourseRenamer } from '../../../../../../src/Contexts/Mooc/Courses/application/RenameCourse/CourseRenamer'; +import { CourseFinder } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseFinder'; +import { CourseNameMother } from '../../domain/CourseNameMother'; +import { CourseMother } from '../../domain/CourseMother'; +import { CourseRepositoryMock } from '../../__mocks__/CourseRepositoryMock'; +import EventBusMock from '../../__mocks__/EventBusMock'; +import { CourseIdMother } from '../../../Shared/domain/Courses/CourseIdMother'; +import { CourseNotFound } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseNotFound'; + +let repository: CourseRepositoryMock; +let finder: CourseFinder; +let renamer: CourseRenamer; + +const eventBus = new EventBusMock(); + +beforeEach(() => { + repository = new CourseRepositoryMock(); + finder = new CourseFinder(repository); + renamer = new CourseRenamer(finder, repository, eventBus); +}); + +it('should rename a valid course', async () => { + const courseBefore = CourseMother.random(); + const { id, duration } = courseBefore; + const newName = CourseNameMother.random(); + const renamedCourse = CourseMother.create(id, newName, duration); + + repository.returnOnSearch(courseBefore); + + await renamer.run({ courseId: id, courseName: newName}) + + repository.assertSearch(id); + repository.assertLastSavedCourseIs(renamedCourse); +}); + +it('should get a not found exception', async () => { + const params = { courseId: CourseIdMother.random(), courseName: CourseNameMother.random()}; + await expect(renamer.run(params)).rejects.toBeInstanceOf(CourseNotFound); +}); diff --git a/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandHandler.test.ts b/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandHandler.test.ts new file mode 100644 index 0000000..928fbc6 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandHandler.test.ts @@ -0,0 +1,43 @@ +import { CourseFinder } from './../../../../../../src/Contexts/Mooc/Courses/domain/CourseFinder'; +import { RenameCourseCommandHandler } from '../../../../../../src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommandHandler'; +import { CourseRepositoryMock } from '../../__mocks__/CourseRepositoryMock'; +import { CourseRenamer } from '../../../../../../src/Contexts/Mooc/Courses/application/RenameCourse/CourseRenamer'; +import EventBusMock from '../../__mocks__/EventBusMock'; +import { RenameCourseCommandMother } from './RenameCourseCommandMother'; +import { CourseMother } from '../../domain/CourseMother'; +import { CourseNameMother } from '../../domain/CourseNameMother'; +import { CourseDurationMother } from '../../domain/CourseDurationMother'; +import { CourseIdMother } from '../../../Shared/domain/Courses/CourseIdMother'; +import { CourseNotFound } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseNotFound'; + +let repository: CourseRepositoryMock; +let handler: RenameCourseCommandHandler; + +const eventBus = new EventBusMock(); + +beforeEach(() => { + repository = new CourseRepositoryMock(); + const finder = new CourseFinder(repository); + const renamer = new CourseRenamer(finder, repository, eventBus); + handler = new RenameCourseCommandHandler(renamer); +}); + +it('should rename a course', async () => { + const command = RenameCourseCommandMother.random(); + const id = CourseIdMother.create(command.id); + const oldName = CourseNameMother.random(); + const name = CourseNameMother.create(command.name); + const duration = CourseDurationMother.random(); + const courseBefore = CourseMother.create(id, oldName, duration); + const renamedCourse = CourseMother.create(id, name, duration); + + repository.returnOnSearch(courseBefore); + await handler.handle(command); + repository.assertSearch(id); + repository.assertLastSavedCourseIs(renamedCourse); +}); + +it('should get an exception', async () => { + const command = RenameCourseCommandMother.random(); + await expect(handler.handle(command)).rejects.toBeInstanceOf(CourseNotFound); +}); \ No newline at end of file diff --git a/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandMother.ts b/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandMother.ts new file mode 100644 index 0000000..0e6c0d9 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandMother.ts @@ -0,0 +1,14 @@ +import { RenameCourseCommand } from '../../../../../../src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand'; +import { CourseIdMother } from '../../../Shared/domain/Courses/CourseIdMother'; +import { CourseNameMother } from '../../domain/CourseNameMother'; + +export class RenameCourseCommandMother { + static create(id: string, name: string): RenameCourseCommand { + return new RenameCourseCommand({ id, name}); + } + + static random(): RenameCourseCommand { + return this.create(CourseIdMother.random().value, CourseNameMother.random().value); + } + } + \ No newline at end of file diff --git a/tests/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.test.ts b/tests/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.test.ts index f995ad9..aa293ba 100644 --- a/tests/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.test.ts +++ b/tests/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.test.ts @@ -1,4 +1,5 @@ import { CourseFinder } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder'; +import { CourseFinder as DomainCourseFinder } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseFinder'; import { CourseRepositoryMock } from '../../__mocks__/CourseRepositoryMock'; import { CourseMother } from '../../domain/CourseMother'; import { GetCourseResponse } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse'; @@ -8,11 +9,12 @@ import { CourseNotFound } from '../../../../../../src/Contexts/Mooc/Courses/doma let repository: CourseRepositoryMock; let finder: CourseFinder; - +let domainFinder: DomainCourseFinder; beforeEach(() => { repository = new CourseRepositoryMock(); - finder = new CourseFinder(repository); + domainFinder = new DomainCourseFinder(repository); + finder = new CourseFinder(domainFinder); }); it('should get a course', async () => { diff --git a/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.test.ts b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.test.ts index e52b4c0..d2c5436 100644 --- a/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.test.ts +++ b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.test.ts @@ -1,4 +1,5 @@ import { CourseFinder } from './../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder'; +import { CourseFinder as DomainCourseFinder } from './../../../../../../src/Contexts/Mooc/Courses/domain/CourseFinder'; import { CourseRepositoryMock } from '../../__mocks__/CourseRepositoryMock'; import { CourseMother } from '../../domain/CourseMother'; import { GetCourseQuery } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQuery'; @@ -9,9 +10,11 @@ import { GetCourseResponse } from '../../../../../../src/Contexts/Mooc/Courses/a describe('GetCourse QueryHandler', () => { let repository: CourseRepositoryMock; + let domainCourseFinder: DomainCourseFinder; beforeEach(() => { repository = new CourseRepositoryMock(); + domainCourseFinder = new DomainCourseFinder(repository); }); @@ -20,7 +23,7 @@ describe('GetCourse QueryHandler', () => { const id = course.id; repository.returnOnSearch(course); - const handler = new GetCourseQueryHandler(new CourseFinder(repository)); + const handler = new GetCourseQueryHandler(new CourseFinder(domainCourseFinder)); const query = new GetCourseQuery({ id: id.value }); const response = await handler.handle(query); @@ -32,7 +35,7 @@ describe('GetCourse QueryHandler', () => { }); it('should throw an exception when courses counter does not exists', async () => { - const handler = new GetCourseQueryHandler(new CourseFinder(repository)); + const handler = new GetCourseQueryHandler(new CourseFinder(domainCourseFinder)); const id = CourseIdMother.random(); const query = new GetCourseQuery({id : id.value}); diff --git a/tests/Contexts/Mooc/Courses/domain/Course.test.ts b/tests/Contexts/Mooc/Courses/domain/Course.test.ts index cdd67c1..9753e29 100644 --- a/tests/Contexts/Mooc/Courses/domain/Course.test.ts +++ b/tests/Contexts/Mooc/Courses/domain/Course.test.ts @@ -10,7 +10,7 @@ describe('Course', () => { it('should return a new course instance', () => { const command = CreateCourseCommandMother.random(); - const course = CourseMother.fromCommand(command); + const course = CourseMother.fromCreateCommand(command); expect(course.id.value).toBe(command.id); expect(course.name.value).toBe(command.name); @@ -25,4 +25,17 @@ describe('Course', () => { expect(events).toHaveLength(1); expect(events[0].eventName).toBe('course.created'); }); + + it('should record a CourseRenamedDomainEvent after rename', () => { + const command = CreateCourseCommandMother.random(); + const course = CourseMother.fromCreateCommand(command); + const oldName = course.name; + const newName = CourseNameMother.random(); + course.rename(newName); + + const events = course.pullDomainEvents(); + + expect(events).toHaveLength(1); + expect(events[0].eventName).toBe('course.renamed'); + }); }); diff --git a/tests/Contexts/Mooc/Courses/domain/CourseFinder.test.ts b/tests/Contexts/Mooc/Courses/domain/CourseFinder.test.ts new file mode 100644 index 0000000..ec10945 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/domain/CourseFinder.test.ts @@ -0,0 +1,31 @@ +import { CourseFinder } from './../../../../../src/Contexts/Mooc/Courses/domain/CourseFinder'; +import { CourseRepositoryMock } from '../__mocks__/CourseRepositoryMock'; +import { CourseMother } from './CourseMother'; +import { GetCourseResponse } from '../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse'; +import { CourseIdMother } from '../../Shared/domain/Courses/CourseIdMother'; +import { CourseNotFound } from '../../../../../src/Contexts/Mooc/Courses/domain/CourseNotFound'; + + +let repository: CourseRepositoryMock; +let finder: CourseFinder; + + +beforeEach(() => { + repository = new CourseRepositoryMock(); + finder = new CourseFinder(repository); +}); + +it('should get a course', async () => { + const course = CourseMother.random(); + const id = course.id; + repository.returnOnSearch(course); + const response = await finder.run(id); + + repository.assertSearch(id); + expect(course).toEqual(response); +}); + +it('should throw an exception when courses counter does not exists', async () => { + const id = CourseIdMother.random() + await expect(finder.run(id)).rejects.toBeInstanceOf(CourseNotFound); +}); diff --git a/tests/Contexts/Mooc/Courses/domain/CourseMother.ts b/tests/Contexts/Mooc/Courses/domain/CourseMother.ts index 1167441..2b950f9 100644 --- a/tests/Contexts/Mooc/Courses/domain/CourseMother.ts +++ b/tests/Contexts/Mooc/Courses/domain/CourseMother.ts @@ -6,13 +6,14 @@ import { CourseIdMother } from '../../Shared/domain/Courses/CourseIdMother'; import { CourseNameMother } from './CourseNameMother'; import { CourseDurationMother } from './CourseDurationMother'; import { CreateCourseCommand } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand'; +import { RenameCourseCommand } from '../../../../../src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand'; export class CourseMother { static create(id: CourseId, name: CourseName, duration: CourseDuration): Course { return new Course(id, name, duration); } - static fromCommand(command: CreateCourseCommand): Course { + static fromCreateCommand(command: CreateCourseCommand): Course { return this.create( CourseIdMother.create(command.id), CourseNameMother.create(command.name), @@ -20,6 +21,14 @@ export class CourseMother { ); } + static fromRenameCommand(command: RenameCourseCommand): Course { + return this.create( + CourseIdMother.create(command.id), + CourseNameMother.create(command.name), + CourseDurationMother.random() + ); + } + static random(): Course { return this.create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random()); } diff --git a/tests/apps/mooc_backend/features/courses/rename-course.feature b/tests/apps/mooc_backend/features/courses/rename-course.feature new file mode 100644 index 0000000..98a771a --- /dev/null +++ b/tests/apps/mooc_backend/features/courses/rename-course.feature @@ -0,0 +1,24 @@ +Feature: Rename a new course + In order to be able to rename courses in the platform + As a user with admin permissions + I want to rename an existing course + + Scenario: A valid existing course + Given I send a PUT request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80b/rename" with body: + """ + { + "name": "New name course" + } + """ + Then the response status code should be 204 + And the response should be empty + + Scenario: A not existing course + Given I send a PUT request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80a/rename" with body: + """ + { + "name": "New name course" + } + """ + Then the response status code should be 404 + And the response should be empty \ No newline at end of file From 03b0e0162abc7f895f92185e421eb3e86ec42c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Fri, 11 Dec 2020 09:14:20 +0000 Subject: [PATCH 24/26] Cqrs/first command (#2) * added some semantic to rename course command handler test * added change description command * added description mother class and import where necessary * added description to course class * modify tests to admit new course property --- .../ChangeDescriptionCourseCommand.ts | 12 +++++++ .../application/CreateCourse/CourseCreator.ts | 7 +++-- .../CreateCourse/CreateCourseCommand.ts | 11 ++++--- .../CreateCourseCommandHandler.ts | 4 ++- .../GetCourse/GetCourseResponse.ts | 2 ++ src/Contexts/Mooc/Courses/domain/Course.ts | 20 +++++++----- .../domain/CourseCreatedDomainEvent.ts | 11 +++++-- .../Mooc/Courses/domain/CourseDescription.ts | 3 ++ .../controllers/CoursesPostController.ts | 3 +- .../controllers/CoursePutController.ts | 4 ++- .../CourseRename/CourseRenamer.test.ts | 4 +-- .../RenameCourseCommandHandler.test.ts | 31 ++++++++++++++----- .../application/CreateCourseCommandMother.ts | 7 +++-- .../Mooc/Courses/domain/Course.test.ts | 5 +-- .../Courses/domain/CourseDescriptionMother.ts | 12 +++++++ .../Mooc/Courses/domain/CourseMother.ts | 14 ++++++--- .../mongo/MongoEnvironmentArranger.ts | 2 +- .../features/courses/create-course.feature | 3 +- .../features/courses/get-course.feature | 3 +- .../step_definitions/controller.steps.ts | 2 +- 20 files changed, 118 insertions(+), 42 deletions(-) create mode 100644 src/Contexts/Mooc/Courses/application/ChangeDescriptionCourse/ChangeDescriptionCourseCommand.ts create mode 100644 src/Contexts/Mooc/Courses/domain/CourseDescription.ts create mode 100644 tests/Contexts/Mooc/Courses/domain/CourseDescriptionMother.ts diff --git a/src/Contexts/Mooc/Courses/application/ChangeDescriptionCourse/ChangeDescriptionCourseCommand.ts b/src/Contexts/Mooc/Courses/application/ChangeDescriptionCourse/ChangeDescriptionCourseCommand.ts new file mode 100644 index 0000000..bce1a4d --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/ChangeDescriptionCourse/ChangeDescriptionCourseCommand.ts @@ -0,0 +1,12 @@ +import { Command } from '../../../../Shared/domain/Command'; + +export class ChangeDescriptionCourseCommand extends Command { + readonly id: string; + readonly description: string; + + constructor(id: string, description: string) { + super(); + this.id = id; + this.description = description; + } +} \ No newline at end of file diff --git a/src/Contexts/Mooc/Courses/application/CreateCourse/CourseCreator.ts b/src/Contexts/Mooc/Courses/application/CreateCourse/CourseCreator.ts index fe96952..c674f2e 100644 --- a/src/Contexts/Mooc/Courses/application/CreateCourse/CourseCreator.ts +++ b/src/Contexts/Mooc/Courses/application/CreateCourse/CourseCreator.ts @@ -4,11 +4,13 @@ import { CourseId } from '../../../Shared/domain/Courses/CourseId'; import { CourseDuration } from '../../domain/CourseDuration'; import { EventBus } from '../../../../Shared/domain/EventBus'; import { CourseName } from '../../../Shared/domain/Courses/CourseName'; +import { CourseDescription } from '../../domain/CourseDescription'; type Params = { courseId: CourseId; courseName: CourseName; courseDuration: CourseDuration; + courseDescription: CourseDescription }; export class CourseCreator { @@ -20,11 +22,12 @@ export class CourseCreator { this.eventBus = eventBus; } - async run({ courseId, courseName, courseDuration }: Params): Promise { + async run({ courseId, courseName, courseDuration, courseDescription }: Params): Promise { const course = Course.create( courseId, courseName, - courseDuration + courseDuration, + courseDescription ); await this.repository.save(course); diff --git a/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand.ts b/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand.ts index 5da981b..0b3edfe 100644 --- a/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand.ts +++ b/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand.ts @@ -4,17 +4,20 @@ type Params = { id: string; name: string; duration: string; + description: string; }; export class CreateCourseCommand extends Command { - id: string; - name: string; - duration: string; + readonly id: string; + readonly name: string; + readonly duration: string; + readonly description: string; - constructor({ id, name, duration }: Params) { + constructor({ id, name, duration, description }: Params) { super(); this.id = id; this.name = name; this.duration = duration; + this.description = description; } } diff --git a/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler.ts b/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler.ts index 9477207..f872ca5 100644 --- a/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler.ts +++ b/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler.ts @@ -5,6 +5,7 @@ import { Command } from '../../../../Shared/domain/Command'; import { CourseId } from '../../../Shared/domain/Courses/CourseId'; import { CourseDuration } from '../../domain/CourseDuration'; import { CourseName } from '../../../Shared/domain/Courses/CourseName'; +import { CourseDescription } from '../../domain/CourseDescription'; export class CreateCourseCommandHandler implements CommandHandler { constructor(private courseCreator: CourseCreator) {} @@ -17,6 +18,7 @@ export class CreateCourseCommandHandler implements CommandHandler { it('should rename a valid course', async () => { const courseBefore = CourseMother.random(); - const { id, duration } = courseBefore; + const { id, duration, description } = courseBefore; const newName = CourseNameMother.random(); - const renamedCourse = CourseMother.create(id, newName, duration); + const renamedCourse = CourseMother.create(id, newName, duration, description); repository.returnOnSearch(courseBefore); diff --git a/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandHandler.test.ts b/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandHandler.test.ts index 928fbc6..5628278 100644 --- a/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandHandler.test.ts +++ b/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandHandler.test.ts @@ -9,6 +9,12 @@ import { CourseNameMother } from '../../domain/CourseNameMother'; import { CourseDurationMother } from '../../domain/CourseDurationMother'; import { CourseIdMother } from '../../../Shared/domain/Courses/CourseIdMother'; import { CourseNotFound } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseNotFound'; +import { RenameCourseCommand } from '../../../../../../src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand'; +import { CourseDuration } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseDuration'; +import { CourseName } from '../../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseName'; +import { CourseId } from '../../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseId'; +import { CourseDescriptionMother } from '../../domain/CourseDescriptionMother'; +import { CourseDescription } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseDescription'; let repository: CourseRepositoryMock; let handler: RenameCourseCommandHandler; @@ -25,19 +31,28 @@ beforeEach(() => { it('should rename a course', async () => { const command = RenameCourseCommandMother.random(); const id = CourseIdMother.create(command.id); - const oldName = CourseNameMother.random(); const name = CourseNameMother.create(command.name); + const oldName = CourseNameMother.random(); const duration = CourseDurationMother.random(); - const courseBefore = CourseMother.create(id, oldName, duration); - const renamedCourse = CourseMother.create(id, name, duration); - + const description = CourseDescriptionMother.random(); + const courseBefore = CourseMother.create(id, oldName, duration, description); repository.returnOnSearch(courseBefore); - await handler.handle(command); - repository.assertSearch(id); - repository.assertLastSavedCourseIs(renamedCourse); + + await whenRenameCourseIsInvoked(command); + thenTheCourseShouldBeRenamed(id, name, duration, description); }); it('should get an exception', async () => { const command = RenameCourseCommandMother.random(); await expect(handler.handle(command)).rejects.toBeInstanceOf(CourseNotFound); -}); \ No newline at end of file +}); + +async function whenRenameCourseIsInvoked(command: RenameCourseCommand) : Promise { + await handler.handle(command); +} + +function thenTheCourseShouldBeRenamed(id: CourseId, name: CourseName, duration: CourseDuration, description: CourseDescription) : void { + const renamedCourse = CourseMother.create(id, name, duration, description); + repository.assertSearch(id); + repository.assertLastSavedCourseIs(renamedCourse); +} \ No newline at end of file diff --git a/tests/Contexts/Mooc/Courses/application/CreateCourseCommandMother.ts b/tests/Contexts/Mooc/Courses/application/CreateCourseCommandMother.ts index cdaca2d..44e9008 100644 --- a/tests/Contexts/Mooc/Courses/application/CreateCourseCommandMother.ts +++ b/tests/Contexts/Mooc/Courses/application/CreateCourseCommandMother.ts @@ -2,13 +2,14 @@ import { CourseDurationMother } from '../domain/CourseDurationMother'; import { CourseIdMother } from '../../Shared/domain/Courses/CourseIdMother'; import { CourseNameMother } from '../domain/CourseNameMother'; import { CreateCourseCommand } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand'; +import { CourseDescriptionMother } from '../domain/CourseDescriptionMother'; export class CreateCourseCommandMother { - static create(id: string, name: string, duration: string): CreateCourseCommand { - return new CreateCourseCommand({ id, name, duration }); + static create(id: string, name: string, duration: string, description: string): CreateCourseCommand { + return new CreateCourseCommand({ id, name, duration, description }); } static random(): CreateCourseCommand { - return this.create(CourseIdMother.random().value, CourseNameMother.random().value, CourseDurationMother.random().value); + return this.create(CourseIdMother.random().value, CourseNameMother.random().value, CourseDurationMother.random().value, CourseDescriptionMother.random().value); } } diff --git a/tests/Contexts/Mooc/Courses/domain/Course.test.ts b/tests/Contexts/Mooc/Courses/domain/Course.test.ts index 9753e29..0448413 100644 --- a/tests/Contexts/Mooc/Courses/domain/Course.test.ts +++ b/tests/Contexts/Mooc/Courses/domain/Course.test.ts @@ -4,6 +4,7 @@ import { Course } from '../../../../../src/Contexts/Mooc/Courses/domain/Course'; import { CourseIdMother } from '../../Shared/domain/Courses/CourseIdMother'; import { CourseNameMother } from './CourseNameMother'; import { CourseDurationMother } from './CourseDurationMother'; +import { CourseDescriptionMother } from './CourseDescriptionMother'; describe('Course', () => { @@ -15,10 +16,11 @@ describe('Course', () => { expect(course.id.value).toBe(command.id); expect(course.name.value).toBe(command.name); expect(course.duration.value).toBe(command.duration); + expect(course.description.value).toBe(command.description); }); it('should record a CourseCreatedDomainEvent after its creation', () => { - const course = Course.create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random()); + const course = Course.create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random(), CourseDescriptionMother.random()); const events = course.pullDomainEvents(); @@ -29,7 +31,6 @@ describe('Course', () => { it('should record a CourseRenamedDomainEvent after rename', () => { const command = CreateCourseCommandMother.random(); const course = CourseMother.fromCreateCommand(command); - const oldName = course.name; const newName = CourseNameMother.random(); course.rename(newName); diff --git a/tests/Contexts/Mooc/Courses/domain/CourseDescriptionMother.ts b/tests/Contexts/Mooc/Courses/domain/CourseDescriptionMother.ts new file mode 100644 index 0000000..f5b5f31 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/domain/CourseDescriptionMother.ts @@ -0,0 +1,12 @@ +import { WordMother } from '../../../Shared/domain/WordMother'; +import { CourseDescription } from '../../../../../src/Contexts/Mooc/Courses/domain/CourseDescription'; + +export class CourseDescriptionMother { + static create(value: string): CourseDescription { + return new CourseDescription(value); + } + + static random(): CourseDescription { + return this.create(WordMother.random()); + } +} diff --git a/tests/Contexts/Mooc/Courses/domain/CourseMother.ts b/tests/Contexts/Mooc/Courses/domain/CourseMother.ts index 2b950f9..ad2cd08 100644 --- a/tests/Contexts/Mooc/Courses/domain/CourseMother.ts +++ b/tests/Contexts/Mooc/Courses/domain/CourseMother.ts @@ -7,17 +7,20 @@ import { CourseNameMother } from './CourseNameMother'; import { CourseDurationMother } from './CourseDurationMother'; import { CreateCourseCommand } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand'; import { RenameCourseCommand } from '../../../../../src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand'; +import { CourseDescription } from '../../../../../src/Contexts/Mooc/Courses/domain/CourseDescription'; +import { CourseDescriptionMother } from './CourseDescriptionMother'; export class CourseMother { - static create(id: CourseId, name: CourseName, duration: CourseDuration): Course { - return new Course(id, name, duration); + static create(id: CourseId, name: CourseName, duration: CourseDuration, description: CourseDescription): Course { + return new Course(id, name, duration, description); } static fromCreateCommand(command: CreateCourseCommand): Course { return this.create( CourseIdMother.create(command.id), CourseNameMother.create(command.name), - CourseDurationMother.create(command.duration) + CourseDurationMother.create(command.duration), + CourseDescriptionMother.create(command.description) ); } @@ -25,11 +28,12 @@ export class CourseMother { return this.create( CourseIdMother.create(command.id), CourseNameMother.create(command.name), - CourseDurationMother.random() + CourseDurationMother.random(), + CourseDescriptionMother.random() ); } static random(): Course { - return this.create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random()); + return this.create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random(), CourseDescriptionMother.random()); } } diff --git a/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts b/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts index fd4298a..3009e3d 100644 --- a/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts +++ b/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts @@ -32,7 +32,7 @@ export class MongoEnvironmentArranger extends EnvironmentArranger { private async addCourse(): Promise { const client = await this.client(); const id = "ef8ac118-8d7f-49cc-abec-78e0d05af80b"; - await client.db().collection("courses").updateOne({ _id: id } ,{$set: { id, name: 'Test Course!', duration: '1' } }, { upsert: true }); + await client.db().collection("courses").updateOne({ _id: id } ,{$set: { id, name: 'Test Course!', duration: '1', description: 'Trust me, this is a test course' } }, { upsert: true }); } protected client(): Promise { diff --git a/tests/apps/mooc_backend/features/courses/create-course.feature b/tests/apps/mooc_backend/features/courses/create-course.feature index 1d5ca8d..b2f39d5 100644 --- a/tests/apps/mooc_backend/features/courses/create-course.feature +++ b/tests/apps/mooc_backend/features/courses/create-course.feature @@ -8,7 +8,8 @@ Feature: Create a new course """ { "name": "The best course", - "duration": "5 hours" + "duration": "5 hours", + "description": "Trust me, this is the best course." } """ Then the response status code should be 201 diff --git a/tests/apps/mooc_backend/features/courses/get-course.feature b/tests/apps/mooc_backend/features/courses/get-course.feature index cb329e8..8664a84 100644 --- a/tests/apps/mooc_backend/features/courses/get-course.feature +++ b/tests/apps/mooc_backend/features/courses/get-course.feature @@ -11,7 +11,8 @@ Feature: Obtain a course { "id": "ef8ac118-8d7f-49cc-abec-78e0d05af80b", "duration": "1", - "name": "Test Course!" + "name": "Test Course!", + "description": "Trust me, this is a test course" } """ diff --git a/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts b/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts index a70221f..5edb031 100644 --- a/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts +++ b/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts @@ -24,7 +24,7 @@ Then('the response should be empty', () => { assert.deepEqual(_response.body, {}); }); -Then('the response content should be:', response => { +Then('the response content should be:', (response: any) => { assert.deepEqual(_response.body, JSON.parse(response)); }); From 83ab0f3f61840c781175049fec89034cb8f0b08b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Sun, 13 Dec 2020 08:07:02 +0000 Subject: [PATCH 25/26] Cqrs/get all courses query (#3) * rename getCourseResponse to CourseResponse * added get courses query * added get courses controller and response * added add course to environment arranger * fix scenario tests * added course repository get all and tests * added get courses handler application uc * added get courses handler application tests * add course with id replace add course on arranger * add get courses test scenarios * added get courses route * added dependency injection config * fix test --- .../application/GetCourse/CourseFinder.ts | 6 +-- .../GetCourse/GetCourseQueryHandler.ts | 6 +-- .../application/GetCourses/CoursesResponse.ts | 16 +++++++ .../application/GetCourses/CoursesSearcher.ts | 20 ++++++++ .../application/GetCourses/GetCoursesQuery.ts | 8 ++++ .../GetCourses/GetCoursesQueryHandler.ts | 17 +++++++ .../Mooc/Courses/domain/CourseRepository.ts | 2 + .../persistence/MongoCourseRepository.ts | 11 +++++ .../Courses/application/CourseResponse.ts} | 4 +- .../Courses/application.yaml | 10 ++++ .../apps/application.yaml | 4 ++ .../controllers/CourseGetController.ts | 4 +- .../controllers/CoursesGetController.ts | 19 ++++++++ src/apps/mooc_backend/routes/courses.route.ts | 4 ++ .../Courses/__mocks__/CourseRepositoryMock.ts | 16 +++++++ .../GetCourse/CourseFinder.test.ts | 4 +- .../GetCourse/GetCourseQueryHandler.test.ts | 4 +- .../GetCourses/GetCoursesQueryHandler.test.ts | 46 +++++++++++++++++++ .../persistence/CourseRepository.test.ts | 25 +++++++++- .../arranger/EnvironmentArranger.ts | 2 + .../mongo/MongoEnvironmentArranger.ts | 4 +- .../features/courses/get-course.feature | 1 + .../features/courses/get-courses.feature | 38 +++++++++++++++ .../features/courses/rename-course.feature | 1 + .../step_definitions/controller.steps.ts | 11 +++++ 25 files changed, 265 insertions(+), 18 deletions(-) create mode 100644 src/Contexts/Mooc/Courses/application/GetCourses/CoursesResponse.ts create mode 100644 src/Contexts/Mooc/Courses/application/GetCourses/CoursesSearcher.ts create mode 100644 src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQuery.ts create mode 100644 src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler.ts rename src/Contexts/Mooc/{Courses/application/GetCourse/GetCourseResponse.ts => Shared/domain/Courses/application/CourseResponse.ts} (79%) create mode 100644 src/apps/mooc_backend/controllers/CoursesGetController.ts create mode 100644 tests/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler.test.ts create mode 100644 tests/apps/mooc_backend/features/courses/get-courses.feature diff --git a/src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts b/src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts index 80c0548..3fc91f8 100644 --- a/src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts +++ b/src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.ts @@ -1,6 +1,6 @@ import { CourseFinder as DomainCourseFinder } from '../../domain/CourseFinder'; import { CourseId } from '../../../Shared/domain/Courses/CourseId'; -import { GetCourseResponse } from './GetCourseResponse'; +import { CourseResponse } from '../../../Shared/domain/Courses/application/CourseResponse'; export type Params = { courseId: CourseId; @@ -13,8 +13,8 @@ export class CourseFinder { this.courseFinder = courseFinder; } - async run({ courseId }: Params): Promise { + async run({ courseId }: Params): Promise { const course = await this.courseFinder.run(courseId); - return new GetCourseResponse(course); + return new CourseResponse(course); } } diff --git a/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.ts b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.ts index 735fb00..c41f95c 100644 --- a/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.ts +++ b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.ts @@ -1,18 +1,18 @@ import { GetCourseQuery } from './GetCourseQuery'; import { CourseId } from '../../../Shared/domain/Courses/CourseId'; import { CourseFinder } from './CourseFinder'; -import { GetCourseResponse } from './GetCourseResponse'; +import { CourseResponse } from '../../../Shared/domain/Courses/application/CourseResponse'; import { QueryHandler } from '../../../../Shared/domain/QueryHandler'; import { Query } from '../../../../Shared/domain/Query'; -export class GetCourseQueryHandler implements QueryHandler { +export class GetCourseQueryHandler implements QueryHandler { constructor(private courseFinder: CourseFinder) {} subscribedTo(): Query { return GetCourseQuery; } - async handle(query: GetCourseQuery): Promise { + async handle(query: GetCourseQuery): Promise { const courseId = new CourseId(query.id); return this.courseFinder.run({ courseId }); } diff --git a/src/Contexts/Mooc/Courses/application/GetCourses/CoursesResponse.ts b/src/Contexts/Mooc/Courses/application/GetCourses/CoursesResponse.ts new file mode 100644 index 0000000..9e26556 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourses/CoursesResponse.ts @@ -0,0 +1,16 @@ +import { Course } from '../../domain/Course'; +import { CourseResponse } from '../../../Shared/domain/Courses/application/CourseResponse'; +import { Nullable } from '../../../../Shared/domain/Nullable'; +export class CoursesResponse { + readonly data: CourseResponse[]; + + constructor(courses: Nullable) { + this.data = []; + if (courses !== null) { + courses.forEach(course => { + this.data.push(new CourseResponse(course)) + }); + } + } +} + \ No newline at end of file diff --git a/src/Contexts/Mooc/Courses/application/GetCourses/CoursesSearcher.ts b/src/Contexts/Mooc/Courses/application/GetCourses/CoursesSearcher.ts new file mode 100644 index 0000000..6aac20b --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourses/CoursesSearcher.ts @@ -0,0 +1,20 @@ +import { CourseFinder as DomainCourseFinder } from '../../domain/CourseFinder'; +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { CourseResponse } from '../../../Shared/domain/Courses/application/CourseResponse'; +import { CourseRepository } from '../../domain/CourseRepository'; +import { Course } from '../../domain/Course'; +import { CoursesResponse } from './CoursesResponse'; +import { Nullable } from '../../../../Shared/domain/Nullable'; + +export class CoursesSearcher { + private repository: CourseRepository; + + constructor(repository: CourseRepository) { + this.repository = repository; + } + + async run(): Promise { + const courses : Nullable = await this.repository.getAll(); + return new CoursesResponse(courses); + } +} diff --git a/src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQuery.ts b/src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQuery.ts new file mode 100644 index 0000000..e28f707 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQuery.ts @@ -0,0 +1,8 @@ +import { Query } from '../../../../Shared/domain/Query'; + +export class GetCoursesQuery extends Query { + + constructor() { + super(); + } +} diff --git a/src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler.ts b/src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler.ts new file mode 100644 index 0000000..a8b3b2e --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler.ts @@ -0,0 +1,17 @@ +import { QueryHandler } from '../../../../Shared/domain/QueryHandler'; +import { Query } from '../../../../Shared/domain/Query'; +import { CoursesResponse } from './CoursesResponse'; +import { GetCoursesQuery } from './GetCoursesQuery'; +import { CoursesSearcher } from './CoursesSearcher'; + +export class GetCoursesQueryHandler implements QueryHandler { + constructor(private coursesSearcher: CoursesSearcher) {} + + subscribedTo(): Query { + return GetCoursesQuery; + } + + async handle(query: GetCoursesQuery): Promise { + return this.coursesSearcher.run(); + } +} diff --git a/src/Contexts/Mooc/Courses/domain/CourseRepository.ts b/src/Contexts/Mooc/Courses/domain/CourseRepository.ts index 4588d16..0f91b49 100644 --- a/src/Contexts/Mooc/Courses/domain/CourseRepository.ts +++ b/src/Contexts/Mooc/Courses/domain/CourseRepository.ts @@ -6,4 +6,6 @@ export interface CourseRepository { save(course: Course): Promise; search(id: CourseId): Promise>; + + getAll(): Promise; } diff --git a/src/Contexts/Mooc/Courses/infrastructure/persistence/MongoCourseRepository.ts b/src/Contexts/Mooc/Courses/infrastructure/persistence/MongoCourseRepository.ts index d6d1340..e381b84 100644 --- a/src/Contexts/Mooc/Courses/infrastructure/persistence/MongoCourseRepository.ts +++ b/src/Contexts/Mooc/Courses/infrastructure/persistence/MongoCourseRepository.ts @@ -17,6 +17,17 @@ export class MongoCourseRepository extends MongoRepository implements Co return document ? Course.fromPrimitives({ ...document, id: id.value }) : null; } + public async getAll(): Promise { + const collection = await this.collection(); + + const documents = await collection.find().toArray(); + + const courses : Course[] = []; + documents.forEach((document: any) => document ? courses.push(Course.fromPrimitives({...document, id: document._id})) : undefined); + + return courses; + } + protected moduleName(): string { return 'courses'; } diff --git a/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse.ts b/src/Contexts/Mooc/Shared/domain/Courses/application/CourseResponse.ts similarity index 79% rename from src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse.ts rename to src/Contexts/Mooc/Shared/domain/Courses/application/CourseResponse.ts index 9a4ceb2..0e16581 100644 --- a/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse.ts +++ b/src/Contexts/Mooc/Shared/domain/Courses/application/CourseResponse.ts @@ -1,5 +1,5 @@ -import { Course } from '../../domain/Course'; -export class GetCourseResponse { +import { Course } from '../../../../Courses/domain/Course'; +export class CourseResponse { readonly id: string; readonly name: string; readonly duration: string; diff --git a/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml b/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml index 63fecaa..f86f60e 100644 --- a/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml @@ -38,3 +38,13 @@ services: tags: - { name: 'commandHandler' } + Mooc.courses.CoursesSearcher: + class: ../../../../../Contexts/Mooc/Courses/application/GetCourses/CoursesSearcher + arguments: ["@Mooc.courses.CourseRepository"] + + Mooc.courses.GetCoursesQueryHandler: + class: ../../../../../Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler + arguments: ["@Mooc.courses.CoursesSearcher"] + tags: + - { name: 'queryHandler' } + diff --git a/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml b/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml index 7fdec48..0d1d537 100644 --- a/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml @@ -18,4 +18,8 @@ services: Apps.mooc.controllers.CourseGetController: class: ../../../controllers/CourseGetController + arguments: ["@Shared.QueryBus"] + + Apps.mooc.controllers.CoursesGetController: + class: ../../../controllers/CoursesGetController arguments: ["@Shared.QueryBus"] \ No newline at end of file diff --git a/src/apps/mooc_backend/controllers/CourseGetController.ts b/src/apps/mooc_backend/controllers/CourseGetController.ts index edf64de..8a7f59a 100644 --- a/src/apps/mooc_backend/controllers/CourseGetController.ts +++ b/src/apps/mooc_backend/controllers/CourseGetController.ts @@ -4,7 +4,7 @@ import httpStatus = require('http-status'); import { QueryBus } from '../../../Contexts/Shared/domain/QueryBus'; import { GetCourseQuery } from '../../../Contexts/Mooc/Courses/application/GetCourse/GetCourseQuery'; -import { GetCourseResponse } from '../../../Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse'; +import { CourseResponse } from '../../../Contexts/Mooc/Shared/domain/Courses/application/CourseResponse'; import { CourseNotFound } from '../../../Contexts/Mooc/Courses/domain/CourseNotFound'; export class CourseGetController implements Controller { @@ -13,7 +13,7 @@ export class CourseGetController implements Controller { try { const id: string = req.params.id; const query = new GetCourseQuery({id}); - const course = await this.queryBus.ask(query); + const course = await this.queryBus.ask(query); res.status(httpStatus.OK).send(course); } catch (e) { if (e instanceof CourseNotFound) { diff --git a/src/apps/mooc_backend/controllers/CoursesGetController.ts b/src/apps/mooc_backend/controllers/CoursesGetController.ts new file mode 100644 index 0000000..c6d9725 --- /dev/null +++ b/src/apps/mooc_backend/controllers/CoursesGetController.ts @@ -0,0 +1,19 @@ +import { CoursesResponse } from './../../../Contexts/Mooc/Courses/application/GetCourses/CoursesResponse'; +import { Controller } from './Controller'; +import { Request, Response } from 'express'; +import httpStatus = require('http-status'); +import { QueryBus } from '../../../Contexts/Shared/domain/QueryBus'; +import { GetCoursesQuery } from '../../../Contexts/Mooc/Courses/application/GetCourses/GetCoursesQuery'; + +export class CoursesGetController implements Controller { + constructor(private queryBus: QueryBus) {} + async run(req: Request, res: Response): Promise { + try { + const query = new GetCoursesQuery(); + const course = await this.queryBus.ask(query); + res.status(httpStatus.OK).send(course); + } catch (e) { + res.status(httpStatus.INTERNAL_SERVER_ERROR).send(); + } + } +} diff --git a/src/apps/mooc_backend/routes/courses.route.ts b/src/apps/mooc_backend/routes/courses.route.ts index 03176f4..4083640 100644 --- a/src/apps/mooc_backend/routes/courses.route.ts +++ b/src/apps/mooc_backend/routes/courses.route.ts @@ -2,6 +2,7 @@ import { Router, Request, Response } from 'express'; import container from '../config/dependency-injection'; export const register = (router: Router) => { + const coursePutController = container.get('Apps.mooc.controllers.CoursePutController'); router.put('/courses/:id', (req: Request, res: Response) => coursePutController.run(req, res)); @@ -11,6 +12,9 @@ export const register = (router: Router) => { const courseGetController = container.get('Apps.mooc.controllers.CourseGetController'); router.get('/courses/:id', (req: Request, res: Response) => courseGetController.run(req, res)); + const coursesGetController = container.get('Apps.mooc.controllers.CoursesGetController'); + router.get('/courses', (req: Request, res: Response) => coursesGetController.run(req, res)); + const coursesCounterGetController = container.get('Apps.mooc.controllers.CoursesCounterGetController'); router.get('/courses-counter', (req: Request, res: Response) => coursesCounterGetController.run(req, res)); }; diff --git a/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts b/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts index 638bb6e..549f5b3 100644 --- a/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts +++ b/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts @@ -4,9 +4,12 @@ import { CourseId } from '../../../../../src/Contexts/Mooc/Shared/domain/Courses import { Nullable } from '../../../../../src/Contexts/Shared/domain/Nullable'; export class CourseRepositoryMock implements CourseRepository { + private mockSave = jest.fn(); private mockSearch = jest.fn(); + private mockGetAll = jest.fn(); private course: Nullable = null; + private courses: Course[] = []; async save(course: Course): Promise { this.mockSave(course); @@ -40,4 +43,17 @@ export class CourseRepositoryMock implements CourseRepository { returnOnSearch(course: Course) { this.course = course; } + + async getAll(): Promise { + this.mockGetAll(); + return this.courses; + } + + returnOnGetAll(courses: Course[]) { + this.courses = courses; + } + + assertGetAll() { + expect(this.mockGetAll).toHaveBeenCalled(); + } } diff --git a/tests/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.test.ts b/tests/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.test.ts index aa293ba..8b2a4ab 100644 --- a/tests/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.test.ts +++ b/tests/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.test.ts @@ -2,10 +2,10 @@ import { CourseFinder } from '../../../../../../src/Contexts/Mooc/Courses/applic import { CourseFinder as DomainCourseFinder } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseFinder'; import { CourseRepositoryMock } from '../../__mocks__/CourseRepositoryMock'; import { CourseMother } from '../../domain/CourseMother'; -import { GetCourseResponse } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse'; import { ParamsMother } from './ParamsMother'; import { CourseIdMother } from '../../../Shared/domain/Courses/CourseIdMother'; import { CourseNotFound } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseNotFound'; +import { CourseResponse } from '../../../../../../src/Contexts/Mooc/Shared/domain/Courses/application/CourseResponse'; let repository: CourseRepositoryMock; let finder: CourseFinder; @@ -25,7 +25,7 @@ it('should get a course', async () => { const response = await finder.run(params); repository.assertSearch(id); - const expected = new GetCourseResponse(course); + const expected = new CourseResponse(course); expect(expected).toEqual(response); }); diff --git a/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.test.ts b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.test.ts index d2c5436..1de4b02 100644 --- a/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.test.ts +++ b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.test.ts @@ -6,7 +6,7 @@ import { GetCourseQuery } from '../../../../../../src/Contexts/Mooc/Courses/appl import { CourseIdMother } from '../../../Shared/domain/Courses/CourseIdMother'; import { GetCourseQueryHandler } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler'; import { CourseNotFound } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseNotFound'; -import { GetCourseResponse } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse'; +import { CourseResponse } from '../../../../../../src/Contexts/Mooc/Shared/domain/Courses/application/CourseResponse'; describe('GetCourse QueryHandler', () => { let repository: CourseRepositoryMock; @@ -30,7 +30,7 @@ describe('GetCourse QueryHandler', () => { repository.assertSearch(id); - const expected = new GetCourseResponse(course); + const expected = new CourseResponse(course); expect(expected).toEqual(response); }); diff --git a/tests/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler.test.ts b/tests/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler.test.ts new file mode 100644 index 0000000..b8eb104 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler.test.ts @@ -0,0 +1,46 @@ +import { CourseRepositoryMock } from '../../__mocks__/CourseRepositoryMock'; +import { CourseMother } from '../../domain/CourseMother'; +import { CoursesSearcher } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourses/CoursesSearcher'; +import { GetCoursesQueryHandler } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler'; +import { GetCoursesQuery } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQuery'; +import { CoursesResponse } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourses/CoursesResponse'; + +describe('GetCourse QueryHandler', () => { + let repository: CourseRepositoryMock; + let coursesSerarcher: CoursesSearcher; + + beforeEach(() => { + repository = new CourseRepositoryMock(); + coursesSerarcher = new CoursesSearcher(repository); + }); + + + it('should search all courses', async () => { + const firstCourse = CourseMother.random(); + const secondCourse = CourseMother.random(); + const courses = [firstCourse, secondCourse]; + repository.returnOnGetAll(courses); + + const handler = new GetCoursesQueryHandler(new CoursesSearcher(repository)); + + const query = new GetCoursesQuery(); + const response = await handler.handle(query); + + repository.assertGetAll(); + + const expected = new CoursesResponse(courses); + expect(expected).toEqual(response); + }); + + it('should get no one course', async () => { + const handler = new GetCoursesQueryHandler(new CoursesSearcher(repository)); + + const query = new GetCoursesQuery(); + const response = await handler.handle(query); + + repository.assertGetAll(); + + const expected = new CoursesResponse([]); + expect(expected).toEqual(response); + }); +}); diff --git a/tests/Contexts/Mooc/Courses/infrastructure/persistence/CourseRepository.test.ts b/tests/Contexts/Mooc/Courses/infrastructure/persistence/CourseRepository.test.ts index cd42f72..69026c4 100644 --- a/tests/Contexts/Mooc/Courses/infrastructure/persistence/CourseRepository.test.ts +++ b/tests/Contexts/Mooc/Courses/infrastructure/persistence/CourseRepository.test.ts @@ -1,4 +1,5 @@ import container from '../../../../../../src/apps/mooc_backend/config/dependency-injection'; +import { Course } from '../../../../../../src/Contexts/Mooc/Courses/domain/Course'; import { CourseRepository } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseRepository'; import { EnvironmentArranger } from '../../../../Shared/infrastructure/arranger/EnvironmentArranger'; import { CourseMother } from '../../domain/CourseMother'; @@ -27,11 +28,33 @@ describe('Search Course', () => { const course = CourseMother.random(); await repository.save(course); + const response = await repository.search(course.id); - expect(course).toEqual(await repository.search(course.id)); + expect(course).toEqual(response); }); it('should not return a non existing course', async () => { expect(await repository.search(CourseMother.random().id)).toBeFalsy(); }); }); + +describe('Get all Courses', () => { + it('should return a list of existing courses', async () => { + const firstCourse = CourseMother.random(); + const secondCourse = CourseMother.random(); + await repository.save(firstCourse); + await repository.save(secondCourse); + + const response = await repository.getAll(); + + const expected = [firstCourse, secondCourse]; + expect(expected).toEqual(response); + }); + + it('should return an empty list', async () => { + const response = await repository.getAll(); + + const expected : Course[] = []; + expect(expected).toEqual(response); + }); +}); \ No newline at end of file diff --git a/tests/Contexts/Shared/infrastructure/arranger/EnvironmentArranger.ts b/tests/Contexts/Shared/infrastructure/arranger/EnvironmentArranger.ts index 2f88c0f..c58261c 100644 --- a/tests/Contexts/Shared/infrastructure/arranger/EnvironmentArranger.ts +++ b/tests/Contexts/Shared/infrastructure/arranger/EnvironmentArranger.ts @@ -2,4 +2,6 @@ export abstract class EnvironmentArranger { public abstract arrange(): Promise; public abstract close(): Promise; + + public abstract addCourseWithId(id: string): Promise; } diff --git a/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts b/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts index 3009e3d..b79cb00 100644 --- a/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts +++ b/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts @@ -8,7 +8,6 @@ export class MongoEnvironmentArranger extends EnvironmentArranger { public async arrange(): Promise { await this.cleanDatabase(); - await this.addCourse(); } protected async cleanDatabase(): Promise { @@ -29,9 +28,8 @@ export class MongoEnvironmentArranger extends EnvironmentArranger { return collections.map(collection => collection.name); } - private async addCourse(): Promise { + public async addCourseWithId(id: string): Promise { const client = await this.client(); - const id = "ef8ac118-8d7f-49cc-abec-78e0d05af80b"; await client.db().collection("courses").updateOne({ _id: id } ,{$set: { id, name: 'Test Course!', duration: '1', description: 'Trust me, this is a test course' } }, { upsert: true }); } diff --git a/tests/apps/mooc_backend/features/courses/get-course.feature b/tests/apps/mooc_backend/features/courses/get-course.feature index 8664a84..f0aef18 100644 --- a/tests/apps/mooc_backend/features/courses/get-course.feature +++ b/tests/apps/mooc_backend/features/courses/get-course.feature @@ -4,6 +4,7 @@ Feature: Obtain a course I want to see the courses Scenario: Retrieve a course + Given a previous course has been already created When I send a GET request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80b" Then the response status code should be 200 And the response content should be: diff --git a/tests/apps/mooc_backend/features/courses/get-courses.feature b/tests/apps/mooc_backend/features/courses/get-courses.feature new file mode 100644 index 0000000..5230539 --- /dev/null +++ b/tests/apps/mooc_backend/features/courses/get-courses.feature @@ -0,0 +1,38 @@ +Feature: Obtain a list course + In order to have a course info list + As a user + I want to see the courses + + Scenario: Retrieve a list of courses + Given Previous courses has been already created + When I send a GET request to "/courses" + Then the response status code should be 200 + And the response content should be: + """ + { + "data": [ + { + "id": "ef8ac118-8d7f-49cc-abec-78e0d05af80b", + "duration": "1", + "name": "Test Course!", + "description": "Trust me, this is a test course" + }, + { + "id": "ef8ac118-8d7f-49cc-abec-78e0d05af80c", + "duration": "1", + "name": "Test Course!", + "description": "Trust me, this is a test course" + } + ] + } + """ + + Scenario: Get an empty list + When I send a GET request to "/courses" + Then the response status code should be 200 + And the response content should be: + """ + { + "data": [] + } + """ \ No newline at end of file diff --git a/tests/apps/mooc_backend/features/courses/rename-course.feature b/tests/apps/mooc_backend/features/courses/rename-course.feature index 98a771a..baf7675 100644 --- a/tests/apps/mooc_backend/features/courses/rename-course.feature +++ b/tests/apps/mooc_backend/features/courses/rename-course.feature @@ -4,6 +4,7 @@ Feature: Rename a new course I want to rename an existing course Scenario: A valid existing course + Given a previous course has been already created Given I send a PUT request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80b/rename" with body: """ { diff --git a/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts b/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts index 5edb031..fef5732 100644 --- a/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts +++ b/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts @@ -28,6 +28,17 @@ Then('the response content should be:', (response: any) => { assert.deepEqual(_response.body, JSON.parse(response)); }); +Given('a previous course has been already created', async ()=> { + const environmentArranger: Promise = container.get('Mooc.EnvironmentArranger'); + await (await environmentArranger).addCourseWithId('ef8ac118-8d7f-49cc-abec-78e0d05af80b'); +}) + +Given('Previous courses has been already created', async ()=> { + const environmentArranger: Promise = container.get('Mooc.EnvironmentArranger'); + await (await environmentArranger).addCourseWithId('ef8ac118-8d7f-49cc-abec-78e0d05af80b'); + await (await environmentArranger).addCourseWithId('ef8ac118-8d7f-49cc-abec-78e0d05af80c'); +}) + Before(async () => { const environmentArranger: Promise = container.get('Mooc.EnvironmentArranger'); await (await environmentArranger).arrange(); From f499a30d8476d735b1493abd9485e1227bedd908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Guti=C3=A9rrez?= Date: Wed, 16 Dec 2020 19:32:12 +0000 Subject: [PATCH 26/26] Feature/course like (#4) * added scenario, controller and command * added userId * added like functionality to course * added structure and tests * fix feature scenario --- .../application/LikeCourse/CourseLiker.ts | 21 ++++++++ .../LikeCourse/LikeCourseCommand.ts | 16 +++++++ .../LikeCourse/LikeCourseCommandHandler.ts | 19 ++++++++ src/Contexts/Mooc/Courses/domain/Course.ts | 15 +++++- .../Courses/domain/CourseLikedDomainEvent.ts | 38 +++++++++++++++ src/Contexts/Mooc/Courses/domain/UserId.ts | 3 ++ .../Courses/application.yaml | 10 ++++ .../apps/application.yaml | 6 ++- .../controllers/CourseLikePostController.ts | 29 +++++++++++ src/apps/mooc_backend/routes/courses.route.ts | 4 +- .../Courses/__mocks__/CourseRepositoryMock.ts | 9 +++- .../LikeCourseCommandHandler.test.ts | 48 +++++++++++++++++++ .../LikeCourse/LikeCourseCommandMother.ts | 13 +++++ .../application/LikeCourse/UserIdMother.ts | 12 +++++ .../features/courses/course-like.feature | 25 ++++++++++ .../step_definitions/controller.steps.ts | 12 +++-- 16 files changed, 271 insertions(+), 9 deletions(-) create mode 100644 src/Contexts/Mooc/Courses/application/LikeCourse/CourseLiker.ts create mode 100644 src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommand.ts create mode 100644 src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler.ts create mode 100644 src/Contexts/Mooc/Courses/domain/CourseLikedDomainEvent.ts create mode 100644 src/Contexts/Mooc/Courses/domain/UserId.ts create mode 100644 src/apps/mooc_backend/controllers/CourseLikePostController.ts create mode 100644 tests/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler.test.ts create mode 100644 tests/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandMother.ts create mode 100644 tests/Contexts/Mooc/Courses/application/LikeCourse/UserIdMother.ts create mode 100644 tests/apps/mooc_backend/features/courses/course-like.feature diff --git a/src/Contexts/Mooc/Courses/application/LikeCourse/CourseLiker.ts b/src/Contexts/Mooc/Courses/application/LikeCourse/CourseLiker.ts new file mode 100644 index 0000000..17c9296 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/LikeCourse/CourseLiker.ts @@ -0,0 +1,21 @@ +import { CourseId } from './../../../Shared/domain/Courses/CourseId'; +import { CourseFinder } from '../../domain/CourseFinder'; +import { UserId } from '../../domain/UserId'; +import { CourseRepository } from '../../domain/CourseRepository'; + +type Params = { + courseId: CourseId; + userId: UserId; +}; + +export class CourseLiker { + constructor(private courseFinder: CourseFinder, private repository: CourseRepository) {} + + async run({ courseId, userId }: Params): Promise { + const course = await this.courseFinder.run(courseId); + + course.like(userId); + + await this.repository.save(course); + } +} diff --git a/src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommand.ts b/src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommand.ts new file mode 100644 index 0000000..4d05b37 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommand.ts @@ -0,0 +1,16 @@ +import { Command } from '../../../../Shared/domain/Command'; + +type Params = { + id: string; + userId: string; +}; + +export class LikeCourseCommand implements Command { + readonly id: string; + readonly userId: string; + + constructor({ id, userId }: Params) { + this.id = id; + this.userId = userId; + } +} diff --git a/src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler.ts b/src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler.ts new file mode 100644 index 0000000..6878e76 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler.ts @@ -0,0 +1,19 @@ +import { Command } from '../../../../Shared/domain/Command'; +import { CommandHandler } from '../../../../Shared/domain/CommandHandler'; +import { LikeCourseCommand } from './LikeCourseCommand'; +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { CourseLiker } from './CourseLiker'; +import { UserId } from '../../domain/UserId'; +export class LikeCourseCommandHandler implements CommandHandler { + constructor(private courseLiker: CourseLiker) {} + + subscribedTo(): Command { + return LikeCourseCommand; + } + + handle(command: LikeCourseCommand): Promise { + const courseId = new CourseId(command.id); + const userId = new UserId(command.userId); + return this.courseLiker.run({ courseId, userId }); + } +} diff --git a/src/Contexts/Mooc/Courses/domain/Course.ts b/src/Contexts/Mooc/Courses/domain/Course.ts index eb501a6..cfc2bd7 100644 --- a/src/Contexts/Mooc/Courses/domain/Course.ts +++ b/src/Contexts/Mooc/Courses/domain/Course.ts @@ -5,6 +5,8 @@ import { CourseId } from '../../Shared/domain/Courses/CourseId'; import { CourseName } from '../../Shared/domain/Courses/CourseName'; import { CourseRenamedDomainEvent } from './CourseRenamedDomainEvent'; import { CourseDescription } from './CourseDescription'; +import { CourseLikedDomainEvent } from './CourseLikedDomainEvent'; +import { UserId } from './UserId'; export class Course extends AggregateRoot { readonly id: CourseId; @@ -45,10 +47,19 @@ export class Course extends AggregateRoot { oldName: oldName.value, newName: name.value }) - ) + ); + } + + like(userId: UserId) { + this.record( + new CourseLikedDomainEvent({ + id: this.id.value, + userId: userId.value + }) + ); } - static fromPrimitives(plainData: { id: string; name: string; duration: string, description: string }): Course { + static fromPrimitives(plainData: { id: string; name: string; duration: string; description: string }): Course { return new Course( new CourseId(plainData.id), new CourseName(plainData.name), diff --git a/src/Contexts/Mooc/Courses/domain/CourseLikedDomainEvent.ts b/src/Contexts/Mooc/Courses/domain/CourseLikedDomainEvent.ts new file mode 100644 index 0000000..7402a26 --- /dev/null +++ b/src/Contexts/Mooc/Courses/domain/CourseLikedDomainEvent.ts @@ -0,0 +1,38 @@ +import { DomainEvent } from '../../../Shared/domain/DomainEvent'; + +export class CourseLikedDomainEvent extends DomainEvent { + static readonly EVENT_NAME = 'course.liked'; + + readonly userId: string; + + constructor({ + id, + userId, + eventId, + occurredOn + }: { + id: string; + userId: string; + eventId?: string; + occurredOn?: Date; + }) { + super(CourseLikedDomainEvent.EVENT_NAME, id, eventId, occurredOn); + this.userId = userId; + } + + toPrimitive(): { userId: string } { + const { userId } = this; + return { + userId + }; + } + + static fromPrimitives(aggregateId: string, body: { userId: string }, eventId: string, occurredOn: Date): DomainEvent { + return new CourseLikedDomainEvent({ + id: aggregateId, + userId: body.userId, + eventId, + occurredOn + }); + } +} diff --git a/src/Contexts/Mooc/Courses/domain/UserId.ts b/src/Contexts/Mooc/Courses/domain/UserId.ts new file mode 100644 index 0000000..36c65c7 --- /dev/null +++ b/src/Contexts/Mooc/Courses/domain/UserId.ts @@ -0,0 +1,3 @@ +import { Uuid } from '../../../Shared/domain/value-object/Uuid'; + +export class UserId extends Uuid {} diff --git a/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml b/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml index f86f60e..775b1f7 100644 --- a/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml @@ -48,3 +48,13 @@ services: tags: - { name: 'queryHandler' } + Mooc.courses.CourseLiker: + class: ../../../../../Contexts/Mooc/Courses/application/LikeCourse/CourseLiker + arguments: ["@Mooc.courses.DomainCourseFinder", "@Mooc.courses.CourseRepository"] + + Mooc.courses.LikeCourseCommandHandler: + class: ../../../../../Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler + arguments: ['@Mooc.courses.CourseLiker'] + tags: + - { name: 'commandHandler' } + diff --git a/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml b/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml index 0d1d537..f728559 100644 --- a/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml @@ -22,4 +22,8 @@ services: Apps.mooc.controllers.CoursesGetController: class: ../../../controllers/CoursesGetController - arguments: ["@Shared.QueryBus"] \ No newline at end of file + arguments: ["@Shared.QueryBus"] + + Apps.mooc.controllers.CourseLikePostController: + class: ../../../controllers/CourseLikePostController + arguments: ["@Shared.CommandBus"] \ No newline at end of file diff --git a/src/apps/mooc_backend/controllers/CourseLikePostController.ts b/src/apps/mooc_backend/controllers/CourseLikePostController.ts new file mode 100644 index 0000000..651f3e7 --- /dev/null +++ b/src/apps/mooc_backend/controllers/CourseLikePostController.ts @@ -0,0 +1,29 @@ +import { Controller } from './Controller'; +import { Request, Response } from 'express'; +import httpStatus from 'http-status'; +import { LikeCourseCommand } from '../../../Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommand'; +import { CourseNotFound } from '../../../Contexts/Mooc/Courses/domain/CourseNotFound'; +import { CommandBus } from '../../../Contexts/Shared/domain/CommandBus'; + +export class CourseLikePostController implements Controller { + constructor(private commandBus: CommandBus) {} + + async run(req: Request, res: Response): Promise { + const id = req.params.id; + const { userId } = req.body; + + const command = new LikeCourseCommand({ id, userId }); + + try { + await this.commandBus.dispatch(command); + } catch (error) { + if (error instanceof CourseNotFound) { + res.status(httpStatus.NOT_FOUND).send(error.message); + } else { + res.status(httpStatus.INTERNAL_SERVER_ERROR).json(error); + } + } + + res.status(httpStatus.NO_CONTENT).send(); + } +} diff --git a/src/apps/mooc_backend/routes/courses.route.ts b/src/apps/mooc_backend/routes/courses.route.ts index 4083640..a912c38 100644 --- a/src/apps/mooc_backend/routes/courses.route.ts +++ b/src/apps/mooc_backend/routes/courses.route.ts @@ -2,7 +2,6 @@ import { Router, Request, Response } from 'express'; import container from '../config/dependency-injection'; export const register = (router: Router) => { - const coursePutController = container.get('Apps.mooc.controllers.CoursePutController'); router.put('/courses/:id', (req: Request, res: Response) => coursePutController.run(req, res)); @@ -15,6 +14,9 @@ export const register = (router: Router) => { const coursesGetController = container.get('Apps.mooc.controllers.CoursesGetController'); router.get('/courses', (req: Request, res: Response) => coursesGetController.run(req, res)); + const courseLikePostController = container.get('Apps.mooc.controllers.CourseLikePostController'); + router.post('/courses/:id/like', (req: Request, res: Response) => courseLikePostController.run(req, res)); + const coursesCounterGetController = container.get('Apps.mooc.controllers.CoursesCounterGetController'); router.get('/courses-counter', (req: Request, res: Response) => coursesCounterGetController.run(req, res)); }; diff --git a/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts b/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts index 549f5b3..29ad0ec 100644 --- a/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts +++ b/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts @@ -4,7 +4,6 @@ import { CourseId } from '../../../../../src/Contexts/Mooc/Shared/domain/Courses import { Nullable } from '../../../../../src/Contexts/Shared/domain/Nullable'; export class CourseRepositoryMock implements CourseRepository { - private mockSave = jest.fn(); private mockSearch = jest.fn(); private mockGetAll = jest.fn(); @@ -22,6 +21,14 @@ export class CourseRepositoryMock implements CourseRepository { expect(lastSavedCourse.toPrimitives()).toEqual(expected.toPrimitives()); } + assertLastSavedCourseEventIs(expected: Course): void { + const mock = this.mockSave.mock; + const lastSavedCourse = mock.calls[mock.calls.length - 1][0] as Course; + expect(lastSavedCourse).toBeInstanceOf(Course); + expect(lastSavedCourse.toPrimitives()).toEqual(expected.toPrimitives()); + expect(lastSavedCourse.pullDomainEvents()).toEqual(expected.pullDomainEvents()); + } + async search(id: CourseId): Promise> { this.mockSearch(id); return this.course; diff --git a/tests/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler.test.ts b/tests/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler.test.ts new file mode 100644 index 0000000..a22598e --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler.test.ts @@ -0,0 +1,48 @@ +import EventBusMock from '../../__mocks__/EventBusMock'; +import { LikeCourseCommandHandler } from '../../../../../../src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler'; +import { CourseRepositoryMock } from '../../__mocks__/CourseRepositoryMock'; +import { CourseFinder } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseFinder'; +import { CourseLiker } from '../../../../../../src/Contexts/Mooc/Courses/application/LikeCourse/CourseLiker'; +import { LikeCourseCommandMother } from './LikeCourseCommandMother'; +import { CourseMother } from '../../domain/CourseMother'; +import { UserIdMother } from './UserIdMother'; +import { CourseLikedDomainEvent } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseLikedDomainEvent'; +import { Course } from '../../../../../../src/Contexts/Mooc/Courses/domain/Course'; + +let handler: LikeCourseCommandHandler; +let repository: CourseRepositoryMock; + +describe('Like Course command handler tests', () => { + beforeEach(() => { + repository = new CourseRepositoryMock(); + const finder = new CourseFinder(repository); + const liker = new CourseLiker(finder, repository); + handler = new LikeCourseCommandHandler(liker); + }); + it('Should like a video', async () => { + //Given + const course = CourseMother.random(); + const userId = UserIdMother.random(); + repository.returnOnSearch(course); + const command = LikeCourseCommandMother.create(course.id.value, userId.value); + + // when + await handler.handle(command); + + // then + thenCourseHasBeenLiked(course, userId.value); + }); +}); + +function thenCourseHasBeenLiked(course: Course, userId: string): void { + const likedCourseEvent = aLikedCourseDomainEvent(course.id.value, userId); + course.record(likedCourseEvent); + repository.assertLastSavedCourseEventIs(course); +} + +function aLikedCourseDomainEvent(id: string, userId: string): CourseLikedDomainEvent { + return new CourseLikedDomainEvent({ + id, + userId + }); +} diff --git a/tests/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandMother.ts b/tests/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandMother.ts new file mode 100644 index 0000000..eac6341 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandMother.ts @@ -0,0 +1,13 @@ +import { LikeCourseCommand } from '../../../../../../src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommand'; +import { CourseIdMother } from '../../../Shared/domain/Courses/CourseIdMother'; +import { UserIdMother } from './UserIdMother'; + +export class LikeCourseCommandMother { + static create(id: string, userId: string): LikeCourseCommand { + return new LikeCourseCommand({ id, userId }); + } + + static random(): LikeCourseCommand { + return this.create(CourseIdMother.random().value, UserIdMother.random().value); + } +} diff --git a/tests/Contexts/Mooc/Courses/application/LikeCourse/UserIdMother.ts b/tests/Contexts/Mooc/Courses/application/LikeCourse/UserIdMother.ts new file mode 100644 index 0000000..fa6e2ad --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/LikeCourse/UserIdMother.ts @@ -0,0 +1,12 @@ +import { UserId } from '../../../../../../src/Contexts/Mooc/Courses/domain/UserId'; +import { UuidMother } from '../../../../Shared/domain/UuidMother'; + +export class UserIdMother { + static create(value: string): UserId { + return new UserId(value); + } + + static random(): UserId { + return this.create(UuidMother.random()); + } +} diff --git a/tests/apps/mooc_backend/features/courses/course-like.feature b/tests/apps/mooc_backend/features/courses/course-like.feature new file mode 100644 index 0000000..faed05e --- /dev/null +++ b/tests/apps/mooc_backend/features/courses/course-like.feature @@ -0,0 +1,25 @@ +Feature: Like a course + In order to be able to like courses in the platform + As a user + I want to Like an existing course + + Scenario: A valid existing course + Given a previous course has been already created + Given I send a POST request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80b/like" with body: + """ + { + "userId": "ef8ac118-8d7f-49cc-abec-78e0d05af80a" + } + """ + Then the response status code should be 204 + And the response should be empty + + Scenario: A not existing course + Given I send a PUT request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80a/like" with body: + """ + { + "userId": "ef8ac118-8d7f-49cc-abec-78e0d05af80b" + } + """ + Then the response status code should be 404 + And the response should be empty \ No newline at end of file diff --git a/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts b/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts index fef5732..e432368 100644 --- a/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts +++ b/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts @@ -16,6 +16,10 @@ Given('I send a PUT request to {string} with body:', (route: string, body: strin _request = request(app).put(route).send(JSON.parse(body)); }); +Given('I send a POST request to {string} with body:', (route: string, body: string) => { + _request = request(app).post(route).send(JSON.parse(body)); +}); + Then('the response status code should be {int}', async (status: number) => { _response = await _request.expect(status); }); @@ -28,16 +32,16 @@ Then('the response content should be:', (response: any) => { assert.deepEqual(_response.body, JSON.parse(response)); }); -Given('a previous course has been already created', async ()=> { +Given('a previous course has been already created', async () => { const environmentArranger: Promise = container.get('Mooc.EnvironmentArranger'); await (await environmentArranger).addCourseWithId('ef8ac118-8d7f-49cc-abec-78e0d05af80b'); -}) +}); -Given('Previous courses has been already created', async ()=> { +Given('Previous courses has been already created', async () => { const environmentArranger: Promise = container.get('Mooc.EnvironmentArranger'); await (await environmentArranger).addCourseWithId('ef8ac118-8d7f-49cc-abec-78e0d05af80b'); await (await environmentArranger).addCourseWithId('ef8ac118-8d7f-49cc-abec-78e0d05af80c'); -}) +}); Before(async () => { const environmentArranger: Promise = container.get('Mooc.EnvironmentArranger');