Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<CourseCreatedDomainEvent> {
constructor(private creator: BackofficeCourseCreator) {}

subscribedTo(): DomainEventClass[] {
return [CourseCreatedDomainEvent];
}

async on(domainEvent: CourseCreatedDomainEvent): Promise<void> {
const { aggregateId, duration, name } = domainEvent;

return this.creator.run(aggregateId, duration, name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import { BackofficeCourse } from './BackofficeCourse';

export interface BackofficeCourseRepository {
searchAll(): Promise<Array<BackofficeCourse>>;
save(course: BackofficeCourse): Promise<void>;
}
2 changes: 2 additions & 0 deletions src/apps/backoffice/backend/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -17,5 +18,6 @@ app.use(helmet.frameguard({ action: 'deny' }));
app.use(compress());

registerRoutes(app);
registerSubscribers();

export default app;
Original file line number Diff line number Diff line change
Expand Up @@ -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' }

Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,17 @@ services:

Shared.QueryBus:
class: ../../../../../../Contexts/Shared/infrastructure/QueryBus/InMemoryQueryBus
arguments: ['@Shared.QueryHandlersInformation']
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']

13 changes: 11 additions & 2 deletions src/apps/backoffice/backend/controllers/CoursesGetController.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<BackofficeCourse>) {
return courses.map(course => ({
id: course.id.toString(),
duration: course.duration.toString(),
name: course.name.toString()
}));
}
}
14 changes: 14 additions & 0 deletions src/apps/backoffice/backend/subscribers.ts
Original file line number Diff line number Diff line change
@@ -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<String, Definition>;
const subscribers: Array<DomainEventSubscriber<DomainEvent>> = [];

subscriberDefinitions.forEach((value: any, key: any) => subscribers.push(container.get(key)));
eventBus.addSubscribers(subscribers);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<BackofficeCourse> = [];

returnOnSearchAll(courses: Array<BackofficeCourse>) {
Expand All @@ -17,4 +18,12 @@ export class BackofficeCourseRepositoryMock implements BackofficeCourseRepositor
assertSearchAll() {
expect(this.mockSearchAll).toHaveBeenCalled();
}

async save(course: BackofficeCourse): Promise<void> {
this.mockSave(course);
}

assertSaveHasBeenCalledWith(course: BackofficeCourse) {
expect(this.mockSave).toHaveBeenCalledWith(course);
}
}
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@ 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()];

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 () => {
const course = BackofficeCourseMother.random();

await repository.save(course);
expect(await repository.searchAll()).toContainEqual(course);
});
});
64 changes: 41 additions & 23 deletions tests/apps/backoffice/backend/features/courses/get-courses.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
"""
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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]);
});