diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index c148dee..82a6447 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -25,6 +25,7 @@ jobs: run: | npm install npm run build --if-present + npm run lint npm test env: CI: true diff --git a/package.json b/package.json index 59a19f4..45a50f1 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test": "npm run test:unit && npm run test:features", "test:unit": "NODE_ENV=test jest", "test:features": "NODE_ENV=test cucumber-js -p default", + "lint": "tslint src/**/*.ts{,x}", "start": "NODE_ENV=production node dist/src/apps/mooc_backend/server", "build": "npm run build:clean && npm run build:tsc && npm run build:di", "build:tsc": "tsc -p tsconfig.prod.json", diff --git a/src/Contexts/Mooc/Courses/application/CourseCreator.ts b/src/Contexts/Mooc/Courses/application/CourseCreator.ts index 67acc99..0880cdf 100644 --- a/src/Contexts/Mooc/Courses/application/CourseCreator.ts +++ b/src/Contexts/Mooc/Courses/application/CourseCreator.ts @@ -6,6 +6,12 @@ import { CourseName } from '../domain/CourseName'; import { CourseDuration } from '../domain/CourseDuration'; import { EventBus } from '../../../Shared/domain/EventBus'; +type Params = { + courseId: CourseId; + courseName: CourseName; + courseDuration: CourseDuration; +}; + export class CourseCreator { private repository: CourseRepository; private eventBus: EventBus; @@ -15,11 +21,11 @@ export class CourseCreator { this.eventBus = eventBus; } - async run(request: CreateCourseRequest): Promise { + async run({ courseId, courseName, courseDuration }: Params): Promise { const course = Course.create( - new CourseId(request.id), - new CourseName(request.name), - new CourseDuration(request.duration) + courseId, + courseName, + courseDuration ); await this.repository.save(course); diff --git a/src/Contexts/Mooc/Courses/application/CreateCourseCommand.ts b/src/Contexts/Mooc/Courses/application/CreateCourseCommand.ts new file mode 100644 index 0000000..884cf7d --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/CreateCourseCommand.ts @@ -0,0 +1,20 @@ +import { Command } from '../../../Shared/domain/Command'; + +type Params = { + id: string; + name: string; + duration: string; +}; + +export class CreateCourseCommand extends Command { + id: string; + name: string; + duration: string; + + constructor({ id, name, duration }: Params) { + super(); + this.id = id; + this.name = name; + this.duration = duration; + } +} diff --git a/src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler.ts b/src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler.ts new file mode 100644 index 0000000..2c8c60e --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler.ts @@ -0,0 +1,22 @@ +import { CreateCourseCommand } from './CreateCourseCommand'; +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'; + +export class CreateCourseCommandHandler implements CommandHandler { + constructor(private courseCreator: CourseCreator) {} + + subscribedTo(): Command { + return CreateCourseCommand; + } + + async handle(command: CreateCourseCommand): Promise { + const courseId = new CourseId(command.id); + const courseName = new CourseName(command.name); + const courseDuration = new CourseDuration(command.duration); + await this.courseCreator.run({ courseId, courseName, courseDuration }); + } +} diff --git a/src/Contexts/Mooc/CoursesCounter/application/Find/CoursesCounterFinder.ts b/src/Contexts/Mooc/CoursesCounter/application/Find/CoursesCounterFinder.ts index 6f95bcb..445fde7 100644 --- a/src/Contexts/Mooc/CoursesCounter/application/Find/CoursesCounterFinder.ts +++ b/src/Contexts/Mooc/CoursesCounter/application/Find/CoursesCounterFinder.ts @@ -1,6 +1,6 @@ import { CoursesCounterRepository } from '../../domain/CoursesCounterRepository'; import { CoursesCounterNotExist } from '../../domain/CoursesCounterNotExist'; -import { CoursesCounterResponse } from './CoursesCounterResponse'; +import { FindCoursesCounterResponse } from './FindCoursesCounterResponse'; export class CoursesCounterFinder { constructor(private repository: CoursesCounterRepository) {} @@ -11,6 +11,6 @@ export class CoursesCounterFinder { throw new CoursesCounterNotExist(); } - return new CoursesCounterResponse(counter.total.value); + return new FindCoursesCounterResponse(counter.total.value); } } diff --git a/src/Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterQuery.ts b/src/Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterQuery.ts new file mode 100644 index 0000000..bdbf087 --- /dev/null +++ b/src/Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterQuery.ts @@ -0,0 +1,3 @@ +import { Query } from '../../../../Shared/domain/Query'; + +export class FindCoursesCounterQuery implements Query {} diff --git a/src/Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterQueryHandler.ts b/src/Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterQueryHandler.ts new file mode 100644 index 0000000..b456d6f --- /dev/null +++ b/src/Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterQueryHandler.ts @@ -0,0 +1,17 @@ +import { QueryHandler } from '../../../../Shared/domain/QueryHandler'; +import { FindCoursesCounterQuery } from './FindCoursesCounterQuery'; +import { FindCoursesCounterResponse } from './FindCoursesCounterResponse'; +import { Query } from '../../../../Shared/domain/Query'; +import { CoursesCounterFinder } from './CoursesCounterFinder'; + +export class FindCoursesCounterQueryHandler + implements QueryHandler { + constructor(private finder: CoursesCounterFinder) {} + + subscribedTo(): Query { + return FindCoursesCounterQuery; + } + handle(_query: FindCoursesCounterQuery): Promise { + return this.finder.run(); + } +} diff --git a/src/Contexts/Mooc/CoursesCounter/application/Find/CoursesCounterResponse.ts b/src/Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterResponse.ts similarity index 67% rename from src/Contexts/Mooc/CoursesCounter/application/Find/CoursesCounterResponse.ts rename to src/Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterResponse.ts index af89a06..fa5bab2 100644 --- a/src/Contexts/Mooc/CoursesCounter/application/Find/CoursesCounterResponse.ts +++ b/src/Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterResponse.ts @@ -1,4 +1,4 @@ -export class CoursesCounterResponse { +export class FindCoursesCounterResponse { readonly total: number; constructor(total: number) { diff --git a/src/Contexts/Shared/domain/Command.ts b/src/Contexts/Shared/domain/Command.ts new file mode 100644 index 0000000..9074445 --- /dev/null +++ b/src/Contexts/Shared/domain/Command.ts @@ -0,0 +1 @@ +export abstract class Command {} diff --git a/src/Contexts/Shared/domain/CommandBus.ts b/src/Contexts/Shared/domain/CommandBus.ts new file mode 100644 index 0000000..ac32da3 --- /dev/null +++ b/src/Contexts/Shared/domain/CommandBus.ts @@ -0,0 +1,5 @@ +import { Command } from './Command'; + +export interface CommandBus { + dispatch(command: Command): Promise; +} diff --git a/src/Contexts/Shared/domain/CommandHandler.ts b/src/Contexts/Shared/domain/CommandHandler.ts new file mode 100644 index 0000000..65be088 --- /dev/null +++ b/src/Contexts/Shared/domain/CommandHandler.ts @@ -0,0 +1,6 @@ +import { Command } from './Command'; + +export interface CommandHandler { + subscribedTo(): Command; + handle(command: T): Promise; +} diff --git a/src/Contexts/Shared/domain/CommandNotRegisteredError.ts b/src/Contexts/Shared/domain/CommandNotRegisteredError.ts new file mode 100644 index 0000000..1da7052 --- /dev/null +++ b/src/Contexts/Shared/domain/CommandNotRegisteredError.ts @@ -0,0 +1,7 @@ +import { Command } from './Command'; + +export class CommandNotRegisteredError extends Error { + constructor(command: Command) { + super(`The command <${command.constructor.name}> hasn't a command handler associated`); + } +} diff --git a/src/Contexts/Shared/domain/Query.ts b/src/Contexts/Shared/domain/Query.ts new file mode 100644 index 0000000..1acca03 --- /dev/null +++ b/src/Contexts/Shared/domain/Query.ts @@ -0,0 +1 @@ +export abstract class Query {} diff --git a/src/Contexts/Shared/domain/QueryBus.ts b/src/Contexts/Shared/domain/QueryBus.ts new file mode 100644 index 0000000..38c5b49 --- /dev/null +++ b/src/Contexts/Shared/domain/QueryBus.ts @@ -0,0 +1,6 @@ +import { Query } from './Query'; +import { Response } from './Response'; + +export interface QueryBus { + ask(query: Query): Promise; +} diff --git a/src/Contexts/Shared/domain/QueryHandler.ts b/src/Contexts/Shared/domain/QueryHandler.ts new file mode 100644 index 0000000..a988d70 --- /dev/null +++ b/src/Contexts/Shared/domain/QueryHandler.ts @@ -0,0 +1,7 @@ +import { Query } from './Query'; +import { Response } from './Response'; + +export interface QueryHandler { + subscribedTo(): Query; + handle(query: Q): Promise; +} diff --git a/src/Contexts/Shared/domain/QueryNotRegisteredError.ts b/src/Contexts/Shared/domain/QueryNotRegisteredError.ts new file mode 100644 index 0000000..aa26fc4 --- /dev/null +++ b/src/Contexts/Shared/domain/QueryNotRegisteredError.ts @@ -0,0 +1,7 @@ +import { Query } from './Query'; + +export class QueryNotRegisteredError extends Error { + constructor(query: Query) { + super(`The query <${query.constructor.name}> hasn't a query handler associated`); + } +} diff --git a/src/Contexts/Shared/domain/Response.ts b/src/Contexts/Shared/domain/Response.ts new file mode 100644 index 0000000..158c809 --- /dev/null +++ b/src/Contexts/Shared/domain/Response.ts @@ -0,0 +1 @@ +export interface Response {} diff --git a/src/Contexts/Shared/infrastructure/CommandBus/CommandHandlersInformation.ts b/src/Contexts/Shared/infrastructure/CommandBus/CommandHandlersInformation.ts new file mode 100644 index 0000000..d157ed6 --- /dev/null +++ b/src/Contexts/Shared/infrastructure/CommandBus/CommandHandlersInformation.ts @@ -0,0 +1,31 @@ +import { Command } from '../../domain/Command'; +import { CommandHandler } from '../../domain/CommandHandler'; +import { CommandNotRegisteredError } from '../../domain/CommandNotRegisteredError'; + +export class CommandHandlersInformation { + private commandHandlersMap: Map>; + + constructor(commandHandlers: Array>) { + this.commandHandlersMap = this.formatHandlers(commandHandlers); + } + + private formatHandlers(commandHandlers: Array>): Map> { + const handlersMap = new Map(); + + commandHandlers.forEach(commandHandler => { + handlersMap.set(commandHandler.subscribedTo(), commandHandler); + }); + + return handlersMap; + } + + public search(command: Command): CommandHandler { + const commandHandler = this.commandHandlersMap.get(command.constructor); + + if (!commandHandler) { + throw new CommandNotRegisteredError(command); + } + + return commandHandler; + } +} diff --git a/src/Contexts/Shared/infrastructure/CommandBus/InMemoryCommandBus.ts b/src/Contexts/Shared/infrastructure/CommandBus/InMemoryCommandBus.ts new file mode 100644 index 0000000..011bdb2 --- /dev/null +++ b/src/Contexts/Shared/infrastructure/CommandBus/InMemoryCommandBus.ts @@ -0,0 +1,13 @@ +import { Command } from '../../domain/Command'; +import { CommandBus } from './../../domain/CommandBus'; +import { CommandHandlersInformation } from './CommandHandlersInformation'; + +export class InMemoryCommandBus implements CommandBus { + constructor(private commandHandlersInformation: CommandHandlersInformation) {} + + async dispatch(command: Command): Promise { + const handler = this.commandHandlersInformation.search(command); + + await handler.handle(command); + } +} diff --git a/src/Contexts/Shared/infrastructure/QueryBus/InMemoryQueryBus.ts b/src/Contexts/Shared/infrastructure/QueryBus/InMemoryQueryBus.ts new file mode 100644 index 0000000..ad7cbe6 --- /dev/null +++ b/src/Contexts/Shared/infrastructure/QueryBus/InMemoryQueryBus.ts @@ -0,0 +1,14 @@ +import { Query } from '../../domain/Query'; +import { Response } from '../../domain/Response'; +import { QueryBus } from './../../domain/QueryBus'; +import { QueryHandlersInformation } from './QueryHandlersInformation'; + +export class InMemoryQueryBus implements QueryBus { + constructor(private queryHandlersInformation: QueryHandlersInformation) {} + + async ask(query: Query): Promise { + const handler = this.queryHandlersInformation.search(query); + + return handler.handle(query) as Promise; + } +} diff --git a/src/Contexts/Shared/infrastructure/QueryBus/QueryHandlersInformation.ts b/src/Contexts/Shared/infrastructure/QueryBus/QueryHandlersInformation.ts new file mode 100644 index 0000000..dd34e48 --- /dev/null +++ b/src/Contexts/Shared/infrastructure/QueryBus/QueryHandlersInformation.ts @@ -0,0 +1,34 @@ +import { Query } from '../../domain/Query'; +import { QueryHandler } from '../../domain/QueryHandler'; +import { Response } from '../../domain/Response'; +import { QueryNotRegisteredError } from '../../domain/QueryNotRegisteredError'; + +export class QueryHandlersInformation { + private queryHandlersMap: Map>; + + constructor(queryHandlers: Array>) { + this.queryHandlersMap = this.formatHandlers(queryHandlers); + } + + private formatHandlers( + queryHandlers: Array> + ): Map> { + const handlersMap = new Map(); + + queryHandlers.forEach(queryHandler => { + handlersMap.set(queryHandler.subscribedTo(), queryHandler); + }); + + return handlersMap; + } + + public search(query: Query): QueryHandler { + const queryHandler = this.queryHandlersMap.get(query.constructor); + + if (!queryHandler) { + throw new QueryNotRegisteredError(query); + } + + return queryHandler; + } +} 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 c4abfad..8acb39e 100644 --- a/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml @@ -7,3 +7,9 @@ services: Mooc.courses.CourseCreator: class: ../../../../../Contexts/Mooc/Courses/application/CourseCreator arguments: ['@Mooc.courses.CourseRepository', '@Mooc.shared.EventBus'] + + Mooc.courses.CreateCourseCommandHandler: + class: ../../../../../Contexts/Mooc/Courses/application/CreateCourseCommandHandler + arguments: ['@Mooc.courses.CourseCreator'] + tags: + - { name: 'commandHandler' } \ No newline at end of file diff --git a/src/apps/mooc_backend/config/dependency-injection/CoursesCounter/application.yaml b/src/apps/mooc_backend/config/dependency-injection/CoursesCounter/application.yaml index 8314388..493864c 100644 --- a/src/apps/mooc_backend/config/dependency-injection/CoursesCounter/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/CoursesCounter/application.yaml @@ -1,5 +1,4 @@ services: - Mooc.coursesCounter.CoursesCounterRepository: class: ../../../../../Contexts/Mooc/CoursesCounter/infrastructure/persistence/mongo/MongoCoursesCounterRepository arguments: ["@Mooc.shared.ConnectionManager"] @@ -21,4 +20,10 @@ services: class: ../../../../../Contexts/Mooc/CoursesCounter/application/Find/CoursesCounterFinder arguments: ["@Mooc.coursesCounter.CoursesCounterRepository"] + + Mooc.coursesCounter.FindCoursesCounterQueryHandler: + class: ../../../../../Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterQueryHandler + arguments: ["@Mooc.coursesCounter.CoursesCounterFinder"] + tags: + - { name: 'queryHandler' } diff --git a/src/apps/mooc_backend/config/dependency-injection/Shared/application.yaml b/src/apps/mooc_backend/config/dependency-injection/Shared/application.yaml index 62137a3..624a233 100644 --- a/src/apps/mooc_backend/config/dependency-injection/Shared/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/Shared/application.yaml @@ -13,6 +13,14 @@ services: class: ../../../../../Contexts/Shared/infrastructure/EventBus/InMemoryAsyncEventBus arguments: [] + Mooc.shared.CommandHandlersInformation: + class: ../../../../../Contexts/Shared/infrastructure/CommandBus/CommandHandlersInformation + arguments: ['!tagged commandHandler'] + + Mooc.shared.CommandBus: + class: ../../../../../Contexts/Shared/infrastructure/CommandBus/InMemoryCommandBus + arguments: ['@Mooc.shared.CommandHandlersInformation'] + Mooc.shared.EventBus.DomainEventMapping: class: ../../../../../Contexts/Shared/infrastructure/EventBus/DomainEventMapping arguments: ['!tagged domainEventSubscriber'] @@ -20,3 +28,11 @@ services: Mooc.shared.EventBus.DomainEventJsonDeserializer: class: ../../../../../Contexts/Shared/infrastructure/EventBus/DomainEventJsonDeserializer arguments: ['@Mooc.shared.EventBus.DomainEventMapping'] + + Mooc.shared.QueryHandlersInformation: + class: ../../../../../Contexts/Shared/infrastructure/QueryBus/QueryHandlersInformation + arguments: ['!tagged queryHandler'] + + Mooc.shared.QueryBus: + class: ../../../../../Contexts/Shared/infrastructure/QueryBus/InMemoryQueryBus + arguments: ['@Mooc.shared.QueryHandlersInformation'] 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 308a6f9..131ee62 100644 --- a/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml @@ -2,7 +2,7 @@ services: Apps.mooc.controllers.CoursePutController: class: ../../../controllers/CoursePutController - arguments: ["@Mooc.courses.CourseCreator"] + arguments: ["@Mooc.shared.CommandBus"] Apps.mooc.controllers.StatusGetController: class: ../../../controllers/StatusGetController @@ -10,4 +10,4 @@ services: Apps.mooc.controllers.CoursesCounterGetController: class: ../../../controllers/CoursesCounterGetController - arguments: ["@Mooc.coursesCounter.CoursesCounterFinder"] + arguments: ["@Mooc.shared.QueryBus"] diff --git a/src/apps/mooc_backend/controllers/CoursePutController.ts b/src/apps/mooc_backend/controllers/CoursePutController.ts index 4b1724e..1863ab7 100644 --- a/src/apps/mooc_backend/controllers/CoursePutController.ts +++ b/src/apps/mooc_backend/controllers/CoursePutController.ts @@ -1,19 +1,21 @@ import { Request, Response } from 'express'; -import { CourseCreator } from '../../../Contexts/Mooc/Courses/application/CourseCreator'; 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'; export class CoursePutController implements Controller { - constructor(private courseCreator: CourseCreator) {} + constructor(private commandBus: CommandBus) {} async run(req: Request, res: Response) { const id: string = req.params.id; const name: string = req.body.name; const duration: string = req.body.duration; + const createCourseCommand = new CreateCourseCommand({ id, name, duration }); try { - await this.courseCreator.run({ id, name, duration }); + await this.commandBus.dispatch(createCourseCommand); } catch (error) { if (error instanceof CourseAlreadyExists) { res.status(httpStatus.BAD_REQUEST).send(error.message); diff --git a/src/apps/mooc_backend/controllers/CoursesCounterGetController.ts b/src/apps/mooc_backend/controllers/CoursesCounterGetController.ts index 088faeb..9f97752 100644 --- a/src/apps/mooc_backend/controllers/CoursesCounterGetController.ts +++ b/src/apps/mooc_backend/controllers/CoursesCounterGetController.ts @@ -1,15 +1,19 @@ import { Controller } from './Controller'; import { Request, Response } from 'express'; -import { CoursesCounterFinder } from '../../../Contexts/Mooc/CoursesCounter/application/Find/CoursesCounterFinder'; import httpStatus = require('http-status'); import { CoursesCounterNotExist } from '../../../Contexts/Mooc/CoursesCounter/domain/CoursesCounterNotExist'; +import { FindCoursesCounterQuery } from '../../../Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterQuery'; +import { QueryBus } from '../../../Contexts/Shared/domain/QueryBus'; +import { FindCoursesCounterResponse } from '../../../Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterResponse'; export class CoursesCounterGetController implements Controller { - constructor(private coursesCounterFinder: CoursesCounterFinder) {} + constructor(private queryBus: QueryBus) {} async run(req: Request, res: Response): Promise { try { - const counter = await this.coursesCounterFinder.run(); - res.status(httpStatus.OK).send(counter); + const query = new FindCoursesCounterQuery(); + const count = await this.queryBus.ask(query); + + res.status(httpStatus.OK).send(count); } catch (e) { if (e instanceof CoursesCounterNotExist) { res.status(httpStatus.NOT_FOUND).send(); diff --git a/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts b/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts index 3609c2c..a406009 100644 --- a/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts +++ b/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts @@ -1,25 +1,25 @@ import { CourseCreator } from '../../../../../src/Contexts/Mooc/Courses/application/CourseCreator'; import { CourseMother } from '../domain/CourseMother'; import { CourseRepositoryMock } from '../__mocks__/CourseRepositoryMock'; -import { CreateCourseRequestMother } from './CreateCourseRequestMother'; +import { CreateCourseCommandMother } from './CreateCourseCommandMother'; import EventBusMock from '../__mocks__/EventBusMock'; +import { CreateCourseCommandHandler } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler'; let repository: CourseRepositoryMock; -let creator: CourseCreator; +let handler: CreateCourseCommandHandler; const eventBus = new EventBusMock(); beforeEach(() => { repository = new CourseRepositoryMock(); - creator = new CourseCreator(repository, eventBus); + const creator = new CourseCreator(repository, eventBus); + handler = new CreateCourseCommandHandler(creator); }); it('should create a valid course', async () => { - const request = CreateCourseRequestMother.random(); - - const course = CourseMother.fromRequest(request); - - await creator.run(request); + const command = CreateCourseCommandMother.random(); + await handler.handle(command); + const course = CourseMother.fromCommand(command); repository.assertLastSavedCourseIs(course); }); diff --git a/tests/Contexts/Mooc/Courses/application/CreateCourseCommandMother.ts b/tests/Contexts/Mooc/Courses/application/CreateCourseCommandMother.ts new file mode 100644 index 0000000..02b796d --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/CreateCourseCommandMother.ts @@ -0,0 +1,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/CreateCourseCommand'; + +export class CreateCourseCommandMother { + static create(id: string, name: string, duration: string): CreateCourseCommand { + return new CreateCourseCommand({ id, name, duration }); + } + + static random(): CreateCourseCommand { + return this.create(CourseIdMother.random().value, CourseNameMother.random().value, CourseDurationMother.random().value); + } +} diff --git a/tests/Contexts/Mooc/Courses/application/CreateCourseRequestMother.ts b/tests/Contexts/Mooc/Courses/application/CreateCourseRequestMother.ts deleted file mode 100644 index d780d22..0000000 --- a/tests/Contexts/Mooc/Courses/application/CreateCourseRequestMother.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { CreateCourseRequest } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourseRequest'; -import { CourseDuration } from '../../../../../src/Contexts/Mooc/Courses/domain/CourseDuration'; -import { CourseId } from '../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseId'; -import { CourseName } from '../../../../../src/Contexts/Mooc/Courses/domain/CourseName'; -import { CourseDurationMother } from '../domain/CourseDurationMother'; -import { CourseIdMother } from '../../Shared/domain/Courses/CourseIdMother'; -import { CourseNameMother } from '../domain/CourseNameMother'; - -export class CreateCourseRequestMother { - static create(id: CourseId, name: CourseName, duration: CourseDuration): CreateCourseRequest { - return { id: id.value, name: name.value, duration: duration.value }; - } - - static random(): CreateCourseRequest { - return this.create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random()); - } -} diff --git a/tests/Contexts/Mooc/Courses/domain/Course.test.ts b/tests/Contexts/Mooc/Courses/domain/Course.test.ts index f6f65fd..cdd67c1 100644 --- a/tests/Contexts/Mooc/Courses/domain/Course.test.ts +++ b/tests/Contexts/Mooc/Courses/domain/Course.test.ts @@ -1,4 +1,4 @@ -import { CreateCourseRequestMother } from '../application/CreateCourseRequestMother'; +import { CreateCourseCommandMother } from '../application/CreateCourseCommandMother'; import { CourseMother } from './CourseMother'; import { Course } from '../../../../../src/Contexts/Mooc/Courses/domain/Course'; import { CourseIdMother } from '../../Shared/domain/Courses/CourseIdMother'; @@ -8,13 +8,13 @@ import { CourseDurationMother } from './CourseDurationMother'; describe('Course', () => { it('should return a new course instance', () => { - const request = CreateCourseRequestMother.random(); + const command = CreateCourseCommandMother.random(); - const course = CourseMother.fromRequest(request); + const course = CourseMother.fromCommand(command); - expect(course.id.value).toBe(request.id); - expect(course.name.value).toBe(request.name); - expect(course.duration.value).toBe(request.duration); + expect(course.id.value).toBe(command.id); + expect(course.name.value).toBe(command.name); + expect(course.duration.value).toBe(command.duration); }); it('should record a CourseCreatedDomainEvent after its creation', () => { diff --git a/tests/Contexts/Mooc/Courses/domain/CourseMother.ts b/tests/Contexts/Mooc/Courses/domain/CourseMother.ts index 5d53285..4c388be 100644 --- a/tests/Contexts/Mooc/Courses/domain/CourseMother.ts +++ b/tests/Contexts/Mooc/Courses/domain/CourseMother.ts @@ -6,17 +6,18 @@ import { CreateCourseRequest } from '../../../../../src/Contexts/Mooc/Courses/ap import { CourseIdMother } from '../../Shared/domain/Courses/CourseIdMother'; import { CourseNameMother } from './CourseNameMother'; import { CourseDurationMother } from './CourseDurationMother'; +import { CreateCourseCommand } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourseCommand'; export class CourseMother { static create(id: CourseId, name: CourseName, duration: CourseDuration): Course { return new Course(id, name, duration); } - static fromRequest(request: CreateCourseRequest): Course { + static fromCommand(command: CreateCourseCommand): Course { return this.create( - CourseIdMother.create(request.id), - CourseNameMother.create(request.name), - CourseDurationMother.create(request.duration) + CourseIdMother.create(command.id), + CourseNameMother.create(command.name), + CourseDurationMother.create(command.duration) ); } diff --git a/tests/Contexts/Mooc/CoursesCounter/__mocks__/CoursesCounterRepositoryMock.ts b/tests/Contexts/Mooc/CoursesCounter/__mocks__/CoursesCounterRepositoryMock.ts index cd88fe5..15bf9a0 100644 --- a/tests/Contexts/Mooc/CoursesCounter/__mocks__/CoursesCounterRepositoryMock.ts +++ b/tests/Contexts/Mooc/CoursesCounter/__mocks__/CoursesCounterRepositoryMock.ts @@ -1,7 +1,6 @@ import { CoursesCounterRepository } from '../../../../../src/Contexts/Mooc/CoursesCounter/domain/CoursesCounterRepository'; import { CoursesCounter } from '../../../../../src/Contexts/Mooc/CoursesCounter/domain/CoursesCounter'; import { Nullable } from '../../../../../src/Contexts/Shared/domain/Nullable'; -import { CourseId } from '../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseId'; export class CoursesCounterRepositoryMock implements CoursesCounterRepository { private mockSave = jest.fn(); diff --git a/tests/Contexts/Mooc/CoursesCounter/application/Find/CoursesCounterFinder.test.ts b/tests/Contexts/Mooc/CoursesCounter/application/Find/FindCourseCounterQueryHandler.test.ts similarity index 55% rename from tests/Contexts/Mooc/CoursesCounter/application/Find/CoursesCounterFinder.test.ts rename to tests/Contexts/Mooc/CoursesCounter/application/Find/FindCourseCounterQueryHandler.test.ts index a52fd9a..260fa00 100644 --- a/tests/Contexts/Mooc/CoursesCounter/application/Find/CoursesCounterFinder.test.ts +++ b/tests/Contexts/Mooc/CoursesCounter/application/Find/FindCourseCounterQueryHandler.test.ts @@ -3,28 +3,37 @@ import { CoursesCounterMother } from '../../domain/CoursesCounterMother'; import { CoursesCounterRepositoryMock } from '../../__mocks__/CoursesCounterRepositoryMock'; import { CoursesCounterResponseMother } from '../../domain/CoursesCounterResponseMother'; import { CoursesCounterNotExist } from '../../../../../../src/Contexts/Mooc/CoursesCounter/domain/CoursesCounterNotExist'; +import { FindCoursesCounterQueryHandler } from '../../../../../../src/Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterQueryHandler'; +import { FindCoursesCounterQuery } from '../../../../../../src/Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterQuery'; -describe('CoursesCounter Finder', () => { - let finder: CoursesCounterFinder; +describe('FindCourseCounter QueryHandler', () => { let repository: CoursesCounterRepositoryMock; beforeEach(() => { repository = new CoursesCounterRepositoryMock(); - finder = new CoursesCounterFinder(repository); }); + it('should find an existing courses counter', async () => { const counter = CoursesCounterMother.random(); - const expected = CoursesCounterResponseMother.create(counter.total); repository.returnOnSearch(counter); - const actual = await finder.run(); - + const handler = new FindCoursesCounterQueryHandler(new CoursesCounterFinder(repository)); + + const query = new FindCoursesCounterQuery(); + const response = await handler.handle(query); + repository.assertSearch(); - expect(expected).toEqual(actual); + + const expected = CoursesCounterResponseMother.create(counter.total); + expect(expected).toEqual(response); }); it('should throw an exception when courses counter does not exists', async () => { - await expect(finder.run()).rejects.toBeInstanceOf(CoursesCounterNotExist); + const handler = new FindCoursesCounterQueryHandler(new CoursesCounterFinder(repository)); + + const query = new FindCoursesCounterQuery(); + + await expect(handler.handle(query)).rejects.toBeInstanceOf(CoursesCounterNotExist); }); }); diff --git a/tests/Contexts/Mooc/CoursesCounter/domain/CoursesCounterResponseMother.ts b/tests/Contexts/Mooc/CoursesCounter/domain/CoursesCounterResponseMother.ts index d383200..677b388 100644 --- a/tests/Contexts/Mooc/CoursesCounter/domain/CoursesCounterResponseMother.ts +++ b/tests/Contexts/Mooc/CoursesCounter/domain/CoursesCounterResponseMother.ts @@ -1,8 +1,8 @@ import { CoursesCounterTotal } from '../../../../../src/Contexts/Mooc/CoursesCounter/domain/CoursesCounterTotal'; -import { CoursesCounterResponse } from '../../../../../src/Contexts/Mooc/CoursesCounter/application/Find/CoursesCounterResponse'; +import { FindCoursesCounterResponse } from '../../../../../src/Contexts/Mooc/CoursesCounter/application/Find/FindCoursesCounterResponse'; export class CoursesCounterResponseMother { static create(total: CoursesCounterTotal) { - return new CoursesCounterResponse(total.value); + return new FindCoursesCounterResponse(total.value); } } diff --git a/tests/Contexts/Shared/infrastructure/CommandBus/InMemoryCommandBus.test.ts b/tests/Contexts/Shared/infrastructure/CommandBus/InMemoryCommandBus.test.ts new file mode 100644 index 0000000..141bfa3 --- /dev/null +++ b/tests/Contexts/Shared/infrastructure/CommandBus/InMemoryCommandBus.test.ts @@ -0,0 +1,49 @@ +import { Command } from '../../../../../src/Contexts/Shared/domain/Command'; +import { CommandHandler } from '../../../../../src/Contexts/Shared/domain/CommandHandler'; +import { CommandNotRegisteredError } from '../../../../../src/Contexts/Shared/domain/CommandNotRegisteredError'; +import { CommandHandlersInformation } from '../../../../../src/Contexts/Shared/infrastructure/CommandBus/CommandHandlersInformation'; +import { InMemoryCommandBus } from '../../../../../src/Contexts/Shared/infrastructure/CommandBus/InMemoryCommandBus'; + +class UnhandledCommand extends Command { + static COMMAND_NAME = 'unhandled.command'; +} + +class HandledCommand extends Command { + static COMMAND_NAME = 'handled.command'; +} + +class MyCommandHandler implements CommandHandler { + subscribedTo(): HandledCommand { + return HandledCommand; + } + + async handle(command: HandledCommand): Promise {} +} + +describe('InMemoryCommandBus', () => { + it('throws an error if dispatches a command without handler', async () => { + const unhandledCommand = new UnhandledCommand(); + const commandHandlersInformation = new CommandHandlersInformation([]); + const commandBus = new InMemoryCommandBus(commandHandlersInformation); + + let exception = null; + + try { + await commandBus.dispatch(unhandledCommand); + } catch (error) { + exception = error; + } + + expect(exception).toBeInstanceOf(CommandNotRegisteredError); + expect(exception.message).toBe(`The command hasn't a command handler associated`); + }); + + it('accepts a command with handler', async () => { + const handledCommand = new HandledCommand(); + const myCommandHandler = new MyCommandHandler(); + const commandHandlersInformation = new CommandHandlersInformation([myCommandHandler]); + const commandBus = new InMemoryCommandBus(commandHandlersInformation); + + await commandBus.dispatch(handledCommand); + }); +}); diff --git a/tests/Contexts/Shared/infrastructure/QueryBus/InMemoryQueryBus.test.ts b/tests/Contexts/Shared/infrastructure/QueryBus/InMemoryQueryBus.test.ts new file mode 100644 index 0000000..829e1c5 --- /dev/null +++ b/tests/Contexts/Shared/infrastructure/QueryBus/InMemoryQueryBus.test.ts @@ -0,0 +1,50 @@ +import { Query } from '../../../../../src/Contexts/Shared/domain/Query'; +import { QueryHandlersInformation } from '../../../../../src/Contexts/Shared/infrastructure/QueryBus/QueryHandlersInformation'; +import { QueryNotRegisteredError } from '../../../../../src/Contexts/Shared/domain/QueryNotRegisteredError'; +import { QueryHandler } from '../../../../../src/Contexts/Shared/domain/QueryHandler'; +import { Response } from '../../../../../src/Contexts/Shared/domain/Response'; +import { InMemoryQueryBus } from '../../../../../src/Contexts/Shared/infrastructure/QueryBus/InMemoryQueryBus'; + +class UnhandledQuery extends Query { + static QUERY_NAME = 'unhandled.query'; +} + +class HandledQuery extends Query { + static QUERY_NAME = 'handled.query'; +} + +class MyQueryHandler implements QueryHandler { + subscribedTo(): HandledQuery { + return HandledQuery; + } + + async handle(query: HandledQuery): Promise {return {};} +} + +describe('InMemoryQueryBus', () => { + it('throws an error if dispatches a query without handler', async () => { + const unhandledQuery = new UnhandledQuery(); + const queryHandlersInformation = new QueryHandlersInformation([]); + const queryBus = new InMemoryQueryBus(queryHandlersInformation); + + let exception = null; + + try { + await queryBus.ask(unhandledQuery); + } catch (error) { + exception = error; + } + + expect(exception).toBeInstanceOf(QueryNotRegisteredError); + expect(exception.message).toBe(`The query hasn't a query handler associated`); + }); + + it('accepts a query with handler', async () => { + const handledQuery = new HandledQuery(); + const myQueryHandler = new MyQueryHandler(); + const queryHandlersInformation = new QueryHandlersInformation([myQueryHandler]); + const queryBus = new InMemoryQueryBus(queryHandlersInformation); + + await queryBus.ask(handledQuery); + }); +}); diff --git a/tslint.json b/tslint.json index b29bf01..b43960e 100644 --- a/tslint.json +++ b/tslint.json @@ -4,7 +4,7 @@ "rules": { "adjacent-overload-signatures": true, "curly": true, - "eofline": false, + "eofline": true, "align": [true, "parameters"], "class-name": true, "indent": [true, "spaces"], @@ -26,7 +26,7 @@ "no-bitwise": true, "no-debugger": true, "prefer-const": true, - "no-empty-interface": true, + "no-empty-interface": false, "no-string-throw": true, "unified-signatures": true, "space-before-function-paren": [