From 0b11e41d0a7553deb64466bb1302a0d0acd3459f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Tue, 3 Nov 2020 09:11:28 +0100 Subject: [PATCH 1/4] Add save method to BackofficeCourseRepository --- .../Backoffice/domain/BackofficeCourseRepository.ts | 1 + .../MongoBackofficeCourseRepository.test.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Contexts/Backoffice/domain/BackofficeCourseRepository.ts b/src/Contexts/Backoffice/domain/BackofficeCourseRepository.ts index c7d1a60..440ec1c 100644 --- a/src/Contexts/Backoffice/domain/BackofficeCourseRepository.ts +++ b/src/Contexts/Backoffice/domain/BackofficeCourseRepository.ts @@ -2,4 +2,5 @@ import { BackofficeCourse } from './BackofficeCourse'; export interface BackofficeCourseRepository { searchAll(): Promise>; + save(course: BackofficeCourse): Promise; } diff --git a/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts b/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts index 6b48d1c..64e21cb 100644 --- a/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts +++ b/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts @@ -14,7 +14,7 @@ afterAll(async () => { await (await environmentArranger).close(); }); -describe('Search all courses', () => { +describe('Mongo BackofficeCourse Repository', () => { it('should return the existing courses', async () => { const courses = [BackofficeCourseMother.random(), BackofficeCourseMother.random()]; @@ -23,4 +23,11 @@ describe('Search all courses', () => { const expectedCourses = await repository.searchAll(); expect(courses.sort()).toEqual(expectedCourses.sort()); }); + + it('should save a course', async () => { + const course = BackofficeCourseMother.random(); + + await repository.save(course); + expect(await repository.searchAll()).toContainEqual(course); + }); }); From bd565612e921d6cdac3fe5a37e5c3cd5a216caa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Tue, 3 Nov 2020 09:21:33 +0100 Subject: [PATCH 2/4] Create BackofficeCourseCreator --- .../Courses/BackofficeCourseCreator.ts | 19 +++++++++++++++++++ .../BackofficeCourseRepositoryMock.ts | 9 +++++++++ .../Courses/BackofficeCourseCreator.test.ts | 16 ++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 src/Contexts/Backoffice/application/Courses/BackofficeCourseCreator.ts create mode 100644 tests/Contexts/Backoffice/application/Courses/BackofficeCourseCreator.test.ts diff --git a/src/Contexts/Backoffice/application/Courses/BackofficeCourseCreator.ts b/src/Contexts/Backoffice/application/Courses/BackofficeCourseCreator.ts new file mode 100644 index 0000000..4758899 --- /dev/null +++ b/src/Contexts/Backoffice/application/Courses/BackofficeCourseCreator.ts @@ -0,0 +1,19 @@ +import { BackofficeCourse } from '../../domain/BackofficeCourse'; +import { BackofficeCourseDuration } from '../../domain/BackofficeCourseDuration'; +import { BackofficeCourseId } from '../../domain/BackofficeCourseId'; +import { BackofficeCourseName } from '../../domain/BackofficeCourseName'; +import { BackofficeCourseRepository } from '../../domain/BackofficeCourseRepository'; + +export class BackofficeCourseCreator { + constructor(private backofficeCourseRepository: BackofficeCourseRepository) {} + + async run(id: string, duration: string, name: string) { + const course = new BackofficeCourse( + new BackofficeCourseId(id), + new BackofficeCourseName(name), + new BackofficeCourseDuration(duration) + ); + + return this.backofficeCourseRepository.save(course); + } +} diff --git a/tests/Contexts/Backoffice/__mocks__/BackofficeCourseRepositoryMock.ts b/tests/Contexts/Backoffice/__mocks__/BackofficeCourseRepositoryMock.ts index b15a629..c41a6a1 100644 --- a/tests/Contexts/Backoffice/__mocks__/BackofficeCourseRepositoryMock.ts +++ b/tests/Contexts/Backoffice/__mocks__/BackofficeCourseRepositoryMock.ts @@ -3,6 +3,7 @@ import { BackofficeCourseRepository } from '../../../../src/Contexts/Backoffice/ export class BackofficeCourseRepositoryMock implements BackofficeCourseRepository { private mockSearchAll = jest.fn(); + private mockSave = jest.fn(); private courses: Array = []; returnOnSearchAll(courses: Array) { @@ -17,4 +18,12 @@ export class BackofficeCourseRepositoryMock implements BackofficeCourseRepositor assertSearchAll() { expect(this.mockSearchAll).toHaveBeenCalled(); } + + async save(course: BackofficeCourse): Promise { + this.mockSave(course); + } + + assertSaveHasBeenCalledWith(course: BackofficeCourse) { + expect(this.mockSave).toHaveBeenCalledWith(course); + } } diff --git a/tests/Contexts/Backoffice/application/Courses/BackofficeCourseCreator.test.ts b/tests/Contexts/Backoffice/application/Courses/BackofficeCourseCreator.test.ts new file mode 100644 index 0000000..d9c8530 --- /dev/null +++ b/tests/Contexts/Backoffice/application/Courses/BackofficeCourseCreator.test.ts @@ -0,0 +1,16 @@ +import faker from 'faker'; +import { BackofficeCourseCreator } from '../../../../../src/Contexts/Backoffice/application/Courses/BackofficeCourseCreator'; +import { BackofficeCourseRepositoryMock } from '../../__mocks__/BackofficeCourseRepositoryMock'; +import { BackofficeCourseMother } from '../domain/BackofficeCourseMother'; +describe('BackofficeCourseCreator', () => { + it('creates a backoffice course', async () => { + const course = BackofficeCourseMother.random(); + + const repository = new BackofficeCourseRepositoryMock(); + const applicationService = new BackofficeCourseCreator(repository); + + await applicationService.run(course.id.toString(), course.duration.toString(), course.name.toString()); + + repository.assertSaveHasBeenCalledWith(course); + }); +}); From d592b7d7bfcb3b1cae39da452c390e3fa4f0dd17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Tue, 3 Nov 2020 10:02:58 +0100 Subject: [PATCH 3/4] Create backoffice course on course created --- .../CreateBackofficeCourseOnCourseCreated.ts | 18 ++++++ src/apps/backoffice/backend/app.ts | 2 + .../Courses/application.yaml | 10 +++ .../Shared/application.yaml | 15 ++++- .../controllers/CoursesGetController.ts | 13 +++- src/apps/backoffice/backend/subscribers.ts | 14 ++++ .../features/courses/get-courses.feature | 64 ++++++++++++------- .../step_definitions/controller.steps.ts | 2 +- .../step_definitions/eventBus.steps.ts | 12 ++++ 9 files changed, 123 insertions(+), 27 deletions(-) create mode 100644 src/Contexts/Backoffice/application/Courses/CreateBackofficeCourseOnCourseCreated.ts create mode 100644 src/apps/backoffice/backend/subscribers.ts create mode 100644 tests/apps/backoffice/backend/features/step_definitions/eventBus.steps.ts diff --git a/src/Contexts/Backoffice/application/Courses/CreateBackofficeCourseOnCourseCreated.ts b/src/Contexts/Backoffice/application/Courses/CreateBackofficeCourseOnCourseCreated.ts new file mode 100644 index 0000000..df9a36e --- /dev/null +++ b/src/Contexts/Backoffice/application/Courses/CreateBackofficeCourseOnCourseCreated.ts @@ -0,0 +1,18 @@ +import { CourseCreatedDomainEvent } from '../../../Mooc/Courses/domain/CourseCreatedDomainEvent'; +import { DomainEventClass } from '../../../Shared/domain/DomainEvent'; +import { DomainEventSubscriber } from '../../../Shared/domain/DomainEventSubscriber'; +import { BackofficeCourseCreator } from './BackofficeCourseCreator'; + +export class CreateBackofficeCourseOnCourseCreated implements DomainEventSubscriber { + constructor(private creator: BackofficeCourseCreator) {} + + subscribedTo(): DomainEventClass[] { + return [CourseCreatedDomainEvent]; + } + + async on(domainEvent: CourseCreatedDomainEvent): Promise { + const { aggregateId, duration, name } = domainEvent; + + return this.creator.run(aggregateId, duration, name); + } +} diff --git a/src/apps/backoffice/backend/app.ts b/src/apps/backoffice/backend/app.ts index d575de2..2adeb62 100644 --- a/src/apps/backoffice/backend/app.ts +++ b/src/apps/backoffice/backend/app.ts @@ -3,6 +3,7 @@ import express from 'express'; import helmet from 'helmet'; import compress from 'compression'; import { registerRoutes } from './routes'; +import { registerSubscribers } from './subscribers'; const app: express.Express = express(); @@ -17,5 +18,6 @@ app.use(helmet.frameguard({ action: 'deny' })); app.use(compress()); registerRoutes(app); +registerSubscribers(); export default app; diff --git a/src/apps/backoffice/backend/config/dependency-injection/Courses/application.yaml b/src/apps/backoffice/backend/config/dependency-injection/Courses/application.yaml index e9abb18..3199584 100644 --- a/src/apps/backoffice/backend/config/dependency-injection/Courses/application.yaml +++ b/src/apps/backoffice/backend/config/dependency-injection/Courses/application.yaml @@ -13,3 +13,13 @@ services: tags: - { name: 'queryHandler' } + Backoffice.Backend.courses.BackofficeCourseCreator: + class: ../../../../../../Contexts/Backoffice/application/Courses/BackofficeCourseCreator + arguments: ["@Backoffice.Backend.courses.BackofficeCourseRepository"] + + Backoffice.Backend.courses.CreateBackofficeCourseOnCourseCreated: + class: ../../../../../../Contexts/Backoffice/application/Courses/CreateBackofficeCourseOnCourseCreated + arguments: ['@Backoffice.Backend.courses.BackofficeCourseCreator'] + tags: + - { name: 'domainEventSubscriber' } + diff --git a/src/apps/backoffice/backend/config/dependency-injection/Shared/application.yaml b/src/apps/backoffice/backend/config/dependency-injection/Shared/application.yaml index 09f7db8..63ec53e 100644 --- a/src/apps/backoffice/backend/config/dependency-injection/Shared/application.yaml +++ b/src/apps/backoffice/backend/config/dependency-injection/Shared/application.yaml @@ -15,4 +15,17 @@ services: Shared.QueryBus: class: ../../../../../../Contexts/Shared/infrastructure/QueryBus/InMemoryQueryBus - arguments: ['@Shared.QueryHandlersInformation'] \ No newline at end of file + arguments: ['@Shared.QueryHandlersInformation'] + + Shared.EventBus: + class: ../../../../../../Contexts/Shared/infrastructure/EventBus/InMemorySyncEventBus + arguments: [] + + Shared.EventBus.DomainEventMapping: + class: ../../../../../../Contexts/Shared/infrastructure/EventBus/DomainEventMapping + arguments: ['!tagged domainEventSubscriber'] + + Shared.EventBus.DomainEventJsonDeserializer: + class: ../../../../../../Contexts/Shared/infrastructure/EventBus/DomainEventJsonDeserializer + arguments: ['@Shared.EventBus.DomainEventMapping'] + \ No newline at end of file diff --git a/src/apps/backoffice/backend/controllers/CoursesGetController.ts b/src/apps/backoffice/backend/controllers/CoursesGetController.ts index 82be7c7..9aa4f3f 100644 --- a/src/apps/backoffice/backend/controllers/CoursesGetController.ts +++ b/src/apps/backoffice/backend/controllers/CoursesGetController.ts @@ -1,6 +1,8 @@ import { Request, Response } from 'express'; import httpStatus from 'http-status'; import { SearchAllCoursesQuery } from '../../../../Contexts/Backoffice/application/SearchAll/SearchAllCoursesQuery'; +import { SearchAllCoursesResponse } from '../../../../Contexts/Backoffice/application/SearchAll/SearchAllCoursesResponse'; +import { BackofficeCourse } from '../../../../Contexts/Backoffice/domain/BackofficeCourse'; import { QueryBus } from '../../../../Contexts/Shared/domain/QueryBus'; import { Controller } from './Controller'; @@ -9,8 +11,15 @@ export class CoursesGetController implements Controller { async run(_req: Request, res: Response) { const query = new SearchAllCoursesQuery(); - const courses = await this.queryBus.ask(query); + const queryResponse: SearchAllCoursesResponse = await this.queryBus.ask(query); + res.status(httpStatus.OK).send(this.toResponse(queryResponse.courses)); + } - res.status(httpStatus.OK).send(courses); + private toResponse(courses: Array) { + return courses.map(course => ({ + id: course.id.toString(), + duration: course.duration.toString(), + name: course.name.toString() + })); } } diff --git a/src/apps/backoffice/backend/subscribers.ts b/src/apps/backoffice/backend/subscribers.ts new file mode 100644 index 0000000..16cbe51 --- /dev/null +++ b/src/apps/backoffice/backend/subscribers.ts @@ -0,0 +1,14 @@ +import container from './config/dependency-injection'; +import { InMemoryAsyncEventBus } from '../../../Contexts/Shared/infrastructure/EventBus/InMemoryAsyncEventBus'; +import { Definition } from 'node-dependency-injection'; +import { DomainEventSubscriber } from '../../../Contexts/Shared/domain/DomainEventSubscriber'; +import { DomainEvent } from '../../../Contexts/Shared/domain/DomainEvent'; + +export function registerSubscribers() { + const eventBus = container.get('Shared.EventBus') as InMemoryAsyncEventBus; + const subscriberDefinitions = container.findTaggedServiceIds('domainEventSubscriber') as Map; + const subscribers: Array> = []; + + subscriberDefinitions.forEach((value: any, key: any) => subscribers.push(container.get(key))); + eventBus.addSubscribers(subscribers); +} diff --git a/tests/apps/backoffice/backend/features/courses/get-courses.feature b/tests/apps/backoffice/backend/features/courses/get-courses.feature index 2a54e19..5c27f42 100644 --- a/tests/apps/backoffice/backend/features/courses/get-courses.feature +++ b/tests/apps/backoffice/backend/features/courses/get-courses.feature @@ -3,38 +3,56 @@ Feature: Get courses I want to get courses Scenario: All existing courses - Given I send a GET request to "/courses" - And there is the course: + Given the following event is received: """ { - "id": "8c900b20-e04a-4777-9183-32faab6d2fb5", - "name": "DDD en PHP!", - "duration": "25 hours" + "data": { + "id": "c77fa036-cbc7-4414-996b-c6a7a93cae09", + "type": "course.created", + "occurred_on": "2019-08-08T08:37:32+00:00", + "attributes": { + "id": "8c900b20-e04a-4777-9183-32faab6d2fb5", + "name": "DDD en PHP!", + "duration": "25 hours" + }, + "meta" : { + "host": "111.26.06.93" + } + } } """ - And there is the course: + And the following event is received: """ { - "id": "8c4a4ed8-9458-489e-a167-b099d81fa096", - "name": "DDD en Java!", - "duration": "24 hours" + "data": { + "id": "353baf48-56e4-4eb2-91a0-b8f826135e6a", + "type": "course.created", + "occurred_on": "2019-08-08T08:37:32+00:00", + "attributes": { + "id": "8c4a4ed8-9458-489e-a167-b099d81fa096", + "name": "DDD en Java!", + "duration": "24 hours" + }, + "meta" : { + "host": "111.26.06.93" + } + } } """ + And I send a GET request to "/courses" Then the response status code should be 200 And the response should be: """ - { - "courses": [ - { - "id": "8c900b20-e04a-4777-9183-32faab6d2fb5", - "name": "DDD en PHP!", - "duration": "25 hours" - }, - { - "id": "8c4a4ed8-9458-489e-a167-b099d81fa096", - "name": "DDD en Java!", - "duration": "24 hours" - } - ] - } + [ + { + "id": "8c900b20-e04a-4777-9183-32faab6d2fb5", + "name": "DDD en PHP!", + "duration": "25 hours" + }, + { + "id": "8c4a4ed8-9458-489e-a167-b099d81fa096", + "name": "DDD en Java!", + "duration": "24 hours" + } + ] """ diff --git a/tests/apps/backoffice/backend/features/step_definitions/controller.steps.ts b/tests/apps/backoffice/backend/features/step_definitions/controller.steps.ts index b203470..de12340 100644 --- a/tests/apps/backoffice/backend/features/step_definitions/controller.steps.ts +++ b/tests/apps/backoffice/backend/features/step_definitions/controller.steps.ts @@ -19,7 +19,7 @@ Then('the response status code should be {int}', async (status: number) => { Then('the response should be:', async response => { const expectedResponse = JSON.parse(response); _response = await _request; - assert(_response.body, expectedResponse); + assert.deepStrictEqual(_response.body, expectedResponse); }); Before(async () => { diff --git a/tests/apps/backoffice/backend/features/step_definitions/eventBus.steps.ts b/tests/apps/backoffice/backend/features/step_definitions/eventBus.steps.ts new file mode 100644 index 0000000..997950e --- /dev/null +++ b/tests/apps/backoffice/backend/features/step_definitions/eventBus.steps.ts @@ -0,0 +1,12 @@ +import { Given } from 'cucumber'; +import container from '../../../../../../src/apps/backoffice/backend/config/dependency-injection'; +import { EventBus } from '../../../../../../src/Contexts/Shared/domain/EventBus'; +import { DomainEventJsonDeserializer } from '../../../../../../src/Contexts/Shared/infrastructure/EventBus/DomainEventJsonDeserializer'; + +const eventBus = container.get('Shared.EventBus') as EventBus; +const deserializer = container.get('Shared.EventBus.DomainEventJsonDeserializer') as DomainEventJsonDeserializer; + +Given('the following event is received:', async (event: any) => { + const domainEvent = deserializer.deserialize(event); + await eventBus.publish([domainEvent]); +}); From d6bb3979e50f92153011fc06829b39aacf4b5032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Tue, 3 Nov 2020 10:42:53 +0100 Subject: [PATCH 4/4] Fix test --- .../infrastructure/MongoBackofficeCourseRepository.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts b/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts index 64e21cb..db9f2c0 100644 --- a/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts +++ b/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts @@ -21,7 +21,7 @@ describe('Mongo BackofficeCourse Repository', () => { await Promise.all(courses.map(course => repository.save(course))); const expectedCourses = await repository.searchAll(); - expect(courses.sort()).toEqual(expectedCourses.sort()); + expect(courses.sort()).toStrictEqual(expectedCourses.sort()); }); it('should save a course', async () => {