From 9c16fad57808cf4e6436d9e6588c8af38bb37eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Wed, 21 Oct 2020 21:21:38 +0200 Subject: [PATCH 01/12] Create new backoffice backend application --- package.json | 2 ++ src/apps/backoffice/backend/app.ts | 23 +++++++++++++++++++ .../Shared/application.yaml | 4 ++++ .../dependency-injection/application.yaml | 2 ++ .../dependency-injection/application_dev.yaml | 2 ++ .../application_production.yaml | 2 ++ .../application_staging.yaml | 2 ++ .../application_test.yaml | 2 ++ .../config/dependency-injection/index.ts | 9 ++++++++ src/apps/backoffice/backend/server.ts | 21 +++++++++++++++++ 10 files changed, 69 insertions(+) create mode 100644 src/apps/backoffice/backend/app.ts create mode 100644 src/apps/backoffice/backend/config/dependency-injection/Shared/application.yaml create mode 100644 src/apps/backoffice/backend/config/dependency-injection/application.yaml create mode 100644 src/apps/backoffice/backend/config/dependency-injection/application_dev.yaml create mode 100644 src/apps/backoffice/backend/config/dependency-injection/application_production.yaml create mode 100644 src/apps/backoffice/backend/config/dependency-injection/application_staging.yaml create mode 100644 src/apps/backoffice/backend/config/dependency-injection/application_test.yaml create mode 100644 src/apps/backoffice/backend/config/dependency-injection/index.ts create mode 100644 src/apps/backoffice/backend/server.ts diff --git a/package.json b/package.json index 4815d67..31feb20 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,14 @@ "scripts": { "dev": "NODE_ENV=dev ts-node-dev --ignore-watch node_modules --inspect=0.0.0.0:9267 ./src/apps/mooc_backend/server.ts", "dev:backoffice:frontend": "NODE_ENV=dev ts-node-dev --ignore-watch node_modules ./src/apps/backoffice/frontend/server.ts", + "dev:backoffice:backend": "NODE_ENV=dev ts-node-dev --ignore-watch node_modules ./src/apps/backoffice/backend/server.ts", "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", "start:backoffice:frontend": "NODE_ENV=production node dist/src/apps/backoffice/frontend/server", + "start:backoffice:backend": "NODE_ENV=production node dist/src/apps/backoffice/backend/server", "build": "npm run build:clean && npm run build:tsc && npm run build:di", "build:tsc": "tsc -p tsconfig.prod.json", "build:di": "copy 'src/**/*.{json,yaml,html,png}' dist/src", diff --git a/src/apps/backoffice/backend/app.ts b/src/apps/backoffice/backend/app.ts new file mode 100644 index 0000000..13f9366 --- /dev/null +++ b/src/apps/backoffice/backend/app.ts @@ -0,0 +1,23 @@ +import bodyParser from 'body-parser'; +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(); + +app.set('port', process.env.PORT || 3000); + +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); +app.use(helmet.xssFilter()); +app.use(helmet.noSniff()); +app.use(helmet.hidePoweredBy()); +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/Shared/application.yaml b/src/apps/backoffice/backend/config/dependency-injection/Shared/application.yaml new file mode 100644 index 0000000..ddaade8 --- /dev/null +++ b/src/apps/backoffice/backend/config/dependency-injection/Shared/application.yaml @@ -0,0 +1,4 @@ +services: + Shared.Logger: + class: ../../../../../../Contexts/Shared/infrastructure/WinstonLogger + arguments: [] \ No newline at end of file diff --git a/src/apps/backoffice/backend/config/dependency-injection/application.yaml b/src/apps/backoffice/backend/config/dependency-injection/application.yaml new file mode 100644 index 0000000..b110623 --- /dev/null +++ b/src/apps/backoffice/backend/config/dependency-injection/application.yaml @@ -0,0 +1,2 @@ +imports: + - { resource: ./Shared/application.yaml } \ No newline at end of file diff --git a/src/apps/backoffice/backend/config/dependency-injection/application_dev.yaml b/src/apps/backoffice/backend/config/dependency-injection/application_dev.yaml new file mode 100644 index 0000000..287933e --- /dev/null +++ b/src/apps/backoffice/backend/config/dependency-injection/application_dev.yaml @@ -0,0 +1,2 @@ +imports: + - { resource: ./application.yaml } diff --git a/src/apps/backoffice/backend/config/dependency-injection/application_production.yaml b/src/apps/backoffice/backend/config/dependency-injection/application_production.yaml new file mode 100644 index 0000000..287933e --- /dev/null +++ b/src/apps/backoffice/backend/config/dependency-injection/application_production.yaml @@ -0,0 +1,2 @@ +imports: + - { resource: ./application.yaml } diff --git a/src/apps/backoffice/backend/config/dependency-injection/application_staging.yaml b/src/apps/backoffice/backend/config/dependency-injection/application_staging.yaml new file mode 100644 index 0000000..287933e --- /dev/null +++ b/src/apps/backoffice/backend/config/dependency-injection/application_staging.yaml @@ -0,0 +1,2 @@ +imports: + - { resource: ./application.yaml } diff --git a/src/apps/backoffice/backend/config/dependency-injection/application_test.yaml b/src/apps/backoffice/backend/config/dependency-injection/application_test.yaml new file mode 100644 index 0000000..287933e --- /dev/null +++ b/src/apps/backoffice/backend/config/dependency-injection/application_test.yaml @@ -0,0 +1,2 @@ +imports: + - { resource: ./application.yaml } diff --git a/src/apps/backoffice/backend/config/dependency-injection/index.ts b/src/apps/backoffice/backend/config/dependency-injection/index.ts new file mode 100644 index 0000000..27d2a35 --- /dev/null +++ b/src/apps/backoffice/backend/config/dependency-injection/index.ts @@ -0,0 +1,9 @@ +import { ContainerBuilder, YamlFileLoader } from 'node-dependency-injection'; + +const container = new ContainerBuilder(); +const loader = new YamlFileLoader(container); +const env = process.env.NODE_ENV || 'dev'; + +loader.load(`${__dirname}/application_${env}.yaml`); + +export default container; diff --git a/src/apps/backoffice/backend/server.ts b/src/apps/backoffice/backend/server.ts new file mode 100644 index 0000000..fc072a8 --- /dev/null +++ b/src/apps/backoffice/backend/server.ts @@ -0,0 +1,21 @@ +import errorHandler from 'errorhandler'; +import app from './app'; +import container from './config/dependency-injection'; + +/** + * Error Handler. Provides full stack - remove for production + */ +app.use(errorHandler()); + +/** + * Start Express server. + */ +const server = app.listen(app.get('port'), () => { + // tslint:disable: no-console + const logger = container.get('Shared.Logger'); + + logger.info(` App is running at http://localhost:${app.get('port')} in ${app.get('env')} mode`); + console.log(' Press CTRL-C to stop\n'); +}); + +export default server; From 90efc6d76e962d38f8c864d1397903002b2b4a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Wed, 21 Oct 2020 21:59:25 +0200 Subject: [PATCH 02/12] Prepare feature testing for the new app and create status route --- cucumber.js | 20 +++++++++---- package.json | 4 ++- src/apps/backoffice/backend/app.ts | 4 +-- src/apps/backoffice/backend/config/config.ts | 22 +++++++++++++++ .../backoffice/backend/config/default.json | 1 + .../Shared/application.yaml | 8 +++++- .../dependency-injection/application.yaml | 3 +- .../application_test.yaml | 5 ++++ .../apps/application.yaml | 4 +++ .../backoffice/backend/config/staging.json | 1 + src/apps/backoffice/backend/config/test.json | 5 ++++ .../backend/controllers/Controller.ts | 5 ++++ .../controllers/StatusGetController.ts | 9 ++++++ src/apps/backoffice/backend/routes/index.ts | 12 ++++++++ .../backoffice/backend/routes/status.route.ts | 8 ++++++ .../backend/features/status.feature | 8 ++++++ .../step_definitions/controller.steps.ts | 28 +++++++++++++++++++ 17 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 src/apps/backoffice/backend/config/config.ts create mode 100644 src/apps/backoffice/backend/config/default.json create mode 100644 src/apps/backoffice/backend/config/dependency-injection/apps/application.yaml create mode 100644 src/apps/backoffice/backend/config/staging.json create mode 100644 src/apps/backoffice/backend/config/test.json create mode 100644 src/apps/backoffice/backend/controllers/Controller.ts create mode 100644 src/apps/backoffice/backend/controllers/StatusGetController.ts create mode 100644 src/apps/backoffice/backend/routes/index.ts create mode 100644 src/apps/backoffice/backend/routes/status.route.ts create mode 100644 tests/apps/backoffice/backend/features/status.feature create mode 100644 tests/apps/backoffice/backend/features/step_definitions/controller.steps.ts diff --git a/cucumber.js b/cucumber.js index ebaef82..7cc5397 100644 --- a/cucumber.js +++ b/cucumber.js @@ -1,9 +1,19 @@ -let common = [ - 'tests/**/features/**/*.feature', // Specify our feature files - '--require-module ts-node/register', // Load TypeScript module - '--require tests/**/features/step_definitions/*.steps.ts' // Load step definitions +const common = [ + '--require-module ts-node/register' // Load TypeScript module +]; + +const backoffice_backend = [ + ...common, + 'tests/apps/backoffice/backend/features/**/*.feature', + '--require tests/apps/backoffice/backend/features/step_definitions/*.steps.ts' +].join(' '); +const mooc_backend = [ + ...common, + 'tests/apps/mooc_backend/features/**/*.feature', + '--require tests/apps/mooc_backend/features/step_definitions/*.steps.ts' ].join(' '); module.exports = { - default: common + backoffice_backend, + mooc_backend }; diff --git a/package.json b/package.json index 31feb20..8066d7e 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "dev:backoffice:backend": "NODE_ENV=dev ts-node-dev --ignore-watch node_modules ./src/apps/backoffice/backend/server.ts", "test": "npm run test:unit && npm run test:features", "test:unit": "NODE_ENV=test jest", - "test:features": "NODE_ENV=test cucumber-js -p default", + "test:features": "npm run test:mooc:backend:features && npm run test:backoffice:backend:features", + "test:mooc:backend:features": "NODE_ENV=test cucumber-js -p mooc_backend", + "test:backoffice:backend:features": "NODE_ENV=test cucumber-js -p backoffice_backend", "lint": "tslint src/**/*.ts{,x}", "start": "NODE_ENV=production node dist/src/apps/mooc_backend/server", "start:backoffice:frontend": "NODE_ENV=production node dist/src/apps/backoffice/frontend/server", diff --git a/src/apps/backoffice/backend/app.ts b/src/apps/backoffice/backend/app.ts index 13f9366..9d8fe79 100644 --- a/src/apps/backoffice/backend/app.ts +++ b/src/apps/backoffice/backend/app.ts @@ -2,7 +2,7 @@ import bodyParser from 'body-parser'; import express from 'express'; import helmet from 'helmet'; import compress from 'compression'; -// import { registerRoutes } from './routes'; +import { registerRoutes } from './routes'; // import { registerSubscribers } from './subscribers'; const app: express.Express = express(); @@ -17,7 +17,7 @@ app.use(helmet.hidePoweredBy()); app.use(helmet.frameguard({ action: 'deny' })); app.use(compress()); -// registerRoutes(app); +registerRoutes(app); // registerSubscribers(); export default app; diff --git a/src/apps/backoffice/backend/config/config.ts b/src/apps/backoffice/backend/config/config.ts new file mode 100644 index 0000000..bb039a3 --- /dev/null +++ b/src/apps/backoffice/backend/config/config.ts @@ -0,0 +1,22 @@ +import convict from 'convict'; + +const convictConfig = convict({ + env: { + doc: 'The application environment.', + format: ['production', 'development', 'staging', 'test'], + default: 'default', + env: 'NODE_ENV' + }, + mongo: { + url: { + doc: 'The Mongo connection URL', + format: String, + env: 'MONGO_URL', + default: 'mongodb://localhost:27017/backoffice-backend-dev' + } + } +}); + +convictConfig.loadFile([__dirname + '/default.json', __dirname + '/' + convictConfig.get('env') + '.json']); + +export default convictConfig; diff --git a/src/apps/backoffice/backend/config/default.json b/src/apps/backoffice/backend/config/default.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/src/apps/backoffice/backend/config/default.json @@ -0,0 +1 @@ +{} 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 ddaade8..24f2d8e 100644 --- a/src/apps/backoffice/backend/config/dependency-injection/Shared/application.yaml +++ b/src/apps/backoffice/backend/config/dependency-injection/Shared/application.yaml @@ -1,4 +1,10 @@ services: Shared.Logger: class: ../../../../../../Contexts/Shared/infrastructure/WinstonLogger - arguments: [] \ No newline at end of file + arguments: [] + + Shared.ConnectionManager: + factory: + class: ../../../../../../Contexts/Shared/infrastructure/persistence/mongo/MongoClientFactory + method: 'createClient' + arguments: ['mooc'] \ No newline at end of file diff --git a/src/apps/backoffice/backend/config/dependency-injection/application.yaml b/src/apps/backoffice/backend/config/dependency-injection/application.yaml index b110623..48481ce 100644 --- a/src/apps/backoffice/backend/config/dependency-injection/application.yaml +++ b/src/apps/backoffice/backend/config/dependency-injection/application.yaml @@ -1,2 +1,3 @@ imports: - - { resource: ./Shared/application.yaml } \ No newline at end of file + - { resource: ./Shared/application.yaml } + - { resource: ./apps/application.yaml } \ No newline at end of file diff --git a/src/apps/backoffice/backend/config/dependency-injection/application_test.yaml b/src/apps/backoffice/backend/config/dependency-injection/application_test.yaml index 287933e..dfdc50a 100644 --- a/src/apps/backoffice/backend/config/dependency-injection/application_test.yaml +++ b/src/apps/backoffice/backend/config/dependency-injection/application_test.yaml @@ -1,2 +1,7 @@ imports: - { resource: ./application.yaml } + +services: + Backoffice.Backend.EnvironmentArranger: + class: ../../../../../../tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger + arguments: ['@Shared.ConnectionManager'] \ No newline at end of file diff --git a/src/apps/backoffice/backend/config/dependency-injection/apps/application.yaml b/src/apps/backoffice/backend/config/dependency-injection/apps/application.yaml new file mode 100644 index 0000000..170497d --- /dev/null +++ b/src/apps/backoffice/backend/config/dependency-injection/apps/application.yaml @@ -0,0 +1,4 @@ +services: + Apps.Backoffice.Backend.controllers.StatusGetController: + class: ../../../controllers/StatusGetController + arguments: [] \ No newline at end of file diff --git a/src/apps/backoffice/backend/config/staging.json b/src/apps/backoffice/backend/config/staging.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/src/apps/backoffice/backend/config/staging.json @@ -0,0 +1 @@ +{} diff --git a/src/apps/backoffice/backend/config/test.json b/src/apps/backoffice/backend/config/test.json new file mode 100644 index 0000000..eb4c086 --- /dev/null +++ b/src/apps/backoffice/backend/config/test.json @@ -0,0 +1,5 @@ +{ + "mongo": { + "url": "mongodb://localhost:27017/backoffice-backend-test" + } +} diff --git a/src/apps/backoffice/backend/controllers/Controller.ts b/src/apps/backoffice/backend/controllers/Controller.ts new file mode 100644 index 0000000..70f0d2a --- /dev/null +++ b/src/apps/backoffice/backend/controllers/Controller.ts @@ -0,0 +1,5 @@ +import { Request, Response } from 'express'; + +export interface Controller { + run(req: Request, res: Response): Promise; +} diff --git a/src/apps/backoffice/backend/controllers/StatusGetController.ts b/src/apps/backoffice/backend/controllers/StatusGetController.ts new file mode 100644 index 0000000..81c659f --- /dev/null +++ b/src/apps/backoffice/backend/controllers/StatusGetController.ts @@ -0,0 +1,9 @@ +import { Request, Response } from 'express'; +import httpStatus from 'http-status'; +import { Controller } from './Controller'; + +export default class StatusGetController implements Controller { + async run(req: Request, res: Response) { + res.status(httpStatus.OK).send(); + } +} diff --git a/src/apps/backoffice/backend/routes/index.ts b/src/apps/backoffice/backend/routes/index.ts new file mode 100644 index 0000000..844f646 --- /dev/null +++ b/src/apps/backoffice/backend/routes/index.ts @@ -0,0 +1,12 @@ +import { Express } from 'express'; +import glob from 'glob'; + +export function registerRoutes(app: Express) { + const routes = glob.sync(__dirname + '/**/*.route.*'); + routes.map(route => register(route, app)); +} + +function register(routePath: string, app: Express) { + const route = require(routePath); + route.register(app); +} diff --git a/src/apps/backoffice/backend/routes/status.route.ts b/src/apps/backoffice/backend/routes/status.route.ts new file mode 100644 index 0000000..9448fe1 --- /dev/null +++ b/src/apps/backoffice/backend/routes/status.route.ts @@ -0,0 +1,8 @@ +import { Express } from 'express'; +import container from '../config/dependency-injection'; +import StatusController from '../controllers/StatusGetController'; + +export const register = (app: Express) => { + const controller: StatusController = container.get('Apps.Backoffice.Backend.controllers.StatusGetController'); + app.get('/status', controller.run.bind(controller)); +}; diff --git a/tests/apps/backoffice/backend/features/status.feature b/tests/apps/backoffice/backend/features/status.feature new file mode 100644 index 0000000..61e614c --- /dev/null +++ b/tests/apps/backoffice/backend/features/status.feature @@ -0,0 +1,8 @@ +Feature: Api status + In order to know the server is up and running + As a health check + I want to check the api status + + Scenario: Check the api status + Given I send a GET request to "/status" + Then the response status code should be 200 diff --git a/tests/apps/backoffice/backend/features/step_definitions/controller.steps.ts b/tests/apps/backoffice/backend/features/step_definitions/controller.steps.ts new file mode 100644 index 0000000..96831f6 --- /dev/null +++ b/tests/apps/backoffice/backend/features/step_definitions/controller.steps.ts @@ -0,0 +1,28 @@ +import assert from 'assert'; +import { AfterAll, Before, Given, Then } from 'cucumber'; +import request from 'supertest'; +import app from '../../../../../../src/apps/backoffice/backend/app'; +import container from '../../../../../../src/apps/backoffice/backend/config/dependency-injection'; +import { EnvironmentArranger } from '../../../../../Contexts/Shared/infrastructure/arranger/EnvironmentArranger'; + +let _request: request.Test; +let _response: request.Response; + +Given('I send a GET request to {string}', (route: string) => { + _request = request(app).get(route); +}); + +Then('the response status code should be {int}', async (status: number) => { + _response = await _request.expect(status); +}); + +Before(async () => { + const environmentArranger: Promise = container.get('Backoffice.Backend.EnvironmentArranger'); + await (await environmentArranger).arrange(); +}); + +AfterAll(async () => { + const environmentArranger: Promise = container.get('Backoffice.Backend.EnvironmentArranger'); + await (await environmentArranger).arrange(); + await (await environmentArranger).close(); +}); From 8e94dbe6d03fc8de5f7c5bd9d0abfb34e54861f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Fri, 23 Oct 2020 17:12:21 +0200 Subject: [PATCH 03/12] Create get-courses feature --- src/apps/backoffice/backend/app.ts | 2 - .../Courses/application.yaml | 4 ++ .../dependency-injection/application.yaml | 3 +- .../features/courses/get-courses.feature | 40 +++++++++++++++++++ .../step_definitions/controller.steps.ts | 6 +++ .../step_definitions/repository.steps.ts | 14 +++++++ 6 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 src/apps/backoffice/backend/config/dependency-injection/Courses/application.yaml create mode 100644 tests/apps/backoffice/backend/features/courses/get-courses.feature create mode 100644 tests/apps/backoffice/backend/features/step_definitions/repository.steps.ts diff --git a/src/apps/backoffice/backend/app.ts b/src/apps/backoffice/backend/app.ts index 9d8fe79..d575de2 100644 --- a/src/apps/backoffice/backend/app.ts +++ b/src/apps/backoffice/backend/app.ts @@ -3,7 +3,6 @@ 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(); @@ -18,6 +17,5 @@ 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 new file mode 100644 index 0000000..9d0e0de --- /dev/null +++ b/src/apps/backoffice/backend/config/dependency-injection/Courses/application.yaml @@ -0,0 +1,4 @@ +services: + Backoffice.Backend.courses.CourseRepository: + class: ../../../../../../Contexts/Mooc/Courses/infrastructure/persistence/MongoCourseRepository + arguments: ['@Shared.ConnectionManager'] \ No newline at end of file diff --git a/src/apps/backoffice/backend/config/dependency-injection/application.yaml b/src/apps/backoffice/backend/config/dependency-injection/application.yaml index 48481ce..72e0c4c 100644 --- a/src/apps/backoffice/backend/config/dependency-injection/application.yaml +++ b/src/apps/backoffice/backend/config/dependency-injection/application.yaml @@ -1,3 +1,4 @@ imports: - { resource: ./Shared/application.yaml } - - { resource: ./apps/application.yaml } \ No newline at end of file + - { resource: ./apps/application.yaml } + - { resource: ./Courses/application.yaml } \ No newline at end of file diff --git a/tests/apps/backoffice/backend/features/courses/get-courses.feature b/tests/apps/backoffice/backend/features/courses/get-courses.feature new file mode 100644 index 0000000..adaeff6 --- /dev/null +++ b/tests/apps/backoffice/backend/features/courses/get-courses.feature @@ -0,0 +1,40 @@ +Feature: Get courses + As a user with permissions + I want to get courses + + Scenario: All existing courses + Given I send a GET request to "/courses" + And there is the course: + """ + { + "id": "8c900b20-e04a-4777-9183-32faab6d2fb5", + "name": "DDD en PHP!", + "duration": "25 hours" + } + """ + And there is the course: + """ + { + "id": "8c4a4ed8-9458-489e-a167-b099d81fa096", + "name": "DDD en Java!", + "duration": "24 hours" + } + """ + # 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" + } + ] + } + """ 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 96831f6..7d96cab 100644 --- a/tests/apps/backoffice/backend/features/step_definitions/controller.steps.ts +++ b/tests/apps/backoffice/backend/features/step_definitions/controller.steps.ts @@ -16,6 +16,12 @@ Then('the response status code should be {int}', async (status: number) => { _response = await _request.expect(status); }); +Then('the response should be:', async response => { + const expectedResponse = JSON.parse(response); + _response = await _request; + assert(response.body, expectedResponse); +}); + Before(async () => { const environmentArranger: Promise = container.get('Backoffice.Backend.EnvironmentArranger'); await (await environmentArranger).arrange(); diff --git a/tests/apps/backoffice/backend/features/step_definitions/repository.steps.ts b/tests/apps/backoffice/backend/features/step_definitions/repository.steps.ts new file mode 100644 index 0000000..711df31 --- /dev/null +++ b/tests/apps/backoffice/backend/features/step_definitions/repository.steps.ts @@ -0,0 +1,14 @@ +import { Given } from 'cucumber'; +import container from '../../../../../../src/apps/backoffice/backend/config/dependency-injection'; +import { Course } from '../../../../../../src/Contexts/Mooc/Courses/domain/Course'; +import { CourseDuration } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseDuration'; +import { CourseName } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseName'; +import { CourseRepository } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseRepository'; +import { CourseId } from '../../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseId'; + +const courseRepository: CourseRepository = container.get('Backoffice.Backend.courses.CourseRepository'); + +Given('there is the course:', async (course: any) => { + const { id, name, duration } = JSON.parse(course); + await courseRepository.save(new Course(new CourseId(id), new CourseName(name), new CourseDuration(duration))); +}); From 643eb0b190c41f275c02d96dd3b1dc60c5de808b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Fri, 23 Oct 2020 17:20:11 +0200 Subject: [PATCH 04/12] Create get courses route --- .../backend/routes/courses.route.ts | 20 +++++++++++++++++++ .../features/courses/get-courses.feature | 2 +- .../step_definitions/controller.steps.ts | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 src/apps/backoffice/backend/routes/courses.route.ts diff --git a/src/apps/backoffice/backend/routes/courses.route.ts b/src/apps/backoffice/backend/routes/courses.route.ts new file mode 100644 index 0000000..795ef00 --- /dev/null +++ b/src/apps/backoffice/backend/routes/courses.route.ts @@ -0,0 +1,20 @@ +import { Express } from 'express'; + +export const register = (app: Express) => { + app.get('/courses', (req, res) => { + res.status(200).send({ + "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" + } + ] + }); + }); +}; diff --git a/tests/apps/backoffice/backend/features/courses/get-courses.feature b/tests/apps/backoffice/backend/features/courses/get-courses.feature index adaeff6..2a54e19 100644 --- a/tests/apps/backoffice/backend/features/courses/get-courses.feature +++ b/tests/apps/backoffice/backend/features/courses/get-courses.feature @@ -20,7 +20,7 @@ Feature: Get courses "duration": "24 hours" } """ - # Then the response status code should be 200 + Then the response status code should be 200 And the response should be: """ { 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 7d96cab..b203470 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(_response.body, expectedResponse); }); Before(async () => { From bed04f0805f22d86cf9ef762b62d6b0bafb3ac11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Fri, 23 Oct 2020 17:27:46 +0200 Subject: [PATCH 05/12] Create CoursesGetController --- .../Courses/application.yaml | 4 +++- .../apps/application.yaml | 4 ++++ .../controllers/CoursesGetController.ts | 21 +++++++++++++++++++ .../backend/routes/courses.route.ts | 20 ++++-------------- 4 files changed, 32 insertions(+), 17 deletions(-) create mode 100644 src/apps/backoffice/backend/controllers/CoursesGetController.ts 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 9d0e0de..d9ebb37 100644 --- a/src/apps/backoffice/backend/config/dependency-injection/Courses/application.yaml +++ b/src/apps/backoffice/backend/config/dependency-injection/Courses/application.yaml @@ -1,4 +1,6 @@ services: Backoffice.Backend.courses.CourseRepository: class: ../../../../../../Contexts/Mooc/Courses/infrastructure/persistence/MongoCourseRepository - arguments: ['@Shared.ConnectionManager'] \ No newline at end of file + arguments: ['@Shared.ConnectionManager'] + + \ No newline at end of file diff --git a/src/apps/backoffice/backend/config/dependency-injection/apps/application.yaml b/src/apps/backoffice/backend/config/dependency-injection/apps/application.yaml index 170497d..8a1f37f 100644 --- a/src/apps/backoffice/backend/config/dependency-injection/apps/application.yaml +++ b/src/apps/backoffice/backend/config/dependency-injection/apps/application.yaml @@ -1,4 +1,8 @@ services: Apps.Backoffice.Backend.controllers.StatusGetController: class: ../../../controllers/StatusGetController + arguments: [] + + Apps.Backoffice.Backend.controllers.CoursesGetController: + class: ../../../controllers/CoursesGetController arguments: [] \ No newline at end of file diff --git a/src/apps/backoffice/backend/controllers/CoursesGetController.ts b/src/apps/backoffice/backend/controllers/CoursesGetController.ts new file mode 100644 index 0000000..eb5cccb --- /dev/null +++ b/src/apps/backoffice/backend/controllers/CoursesGetController.ts @@ -0,0 +1,21 @@ +import { Request, Response } from 'express'; +import { Controller } from './Controller'; + +export class CoursesGetController implements Controller { + async run(_req: Request, res: Response) { + res.status(200).send({ + 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' + } + ] + }); + } +} diff --git a/src/apps/backoffice/backend/routes/courses.route.ts b/src/apps/backoffice/backend/routes/courses.route.ts index 795ef00..91a5968 100644 --- a/src/apps/backoffice/backend/routes/courses.route.ts +++ b/src/apps/backoffice/backend/routes/courses.route.ts @@ -1,20 +1,8 @@ import { Express } from 'express'; +import container from '../config/dependency-injection'; +import { CoursesGetController } from '../controllers/CoursesGetController'; export const register = (app: Express) => { - app.get('/courses', (req, res) => { - res.status(200).send({ - "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" - } - ] - }); - }); + const coursesGetController: CoursesGetController = container.get('Apps.Backoffice.Backend.controllers.CoursesGetController'); + app.get('/courses', coursesGetController.run); }; From 6917969314b2f61204da4e89c16fde91ce86354b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Fri, 23 Oct 2020 17:50:37 +0200 Subject: [PATCH 06/12] Create BackofficeCourse aggregate root --- .../Backoffice/domain/AggregateRoot.ts | 19 +++++++++ .../Backoffice/domain/BackofficeCourse.ts | 39 +++++++++++++++++++ .../domain/BackofficeCourseDuration.ts | 3 ++ .../Backoffice/domain/BackofficeCourseId.ts | 3 ++ .../Backoffice/domain/BackofficeCourseName.ts | 3 ++ 5 files changed, 67 insertions(+) create mode 100644 src/Contexts/Backoffice/domain/AggregateRoot.ts create mode 100644 src/Contexts/Backoffice/domain/BackofficeCourse.ts create mode 100644 src/Contexts/Backoffice/domain/BackofficeCourseDuration.ts create mode 100644 src/Contexts/Backoffice/domain/BackofficeCourseId.ts create mode 100644 src/Contexts/Backoffice/domain/BackofficeCourseName.ts diff --git a/src/Contexts/Backoffice/domain/AggregateRoot.ts b/src/Contexts/Backoffice/domain/AggregateRoot.ts new file mode 100644 index 0000000..d44d335 --- /dev/null +++ b/src/Contexts/Backoffice/domain/AggregateRoot.ts @@ -0,0 +1,19 @@ +import { DomainEvent } from '../../../Shared/domain/DomainEvent'; + +export abstract class AggregateRoot { + private domainEvents: Array; + + constructor() { + this.domainEvents = []; + } + + pullDomainEvents(): Array { + return this.domainEvents; + } + + record(event: DomainEvent): void { + this.domainEvents.push(event); + } + + abstract toPrimitives(): any; +} diff --git a/src/Contexts/Backoffice/domain/BackofficeCourse.ts b/src/Contexts/Backoffice/domain/BackofficeCourse.ts new file mode 100644 index 0000000..61a72b7 --- /dev/null +++ b/src/Contexts/Backoffice/domain/BackofficeCourse.ts @@ -0,0 +1,39 @@ +import { AggregateRoot } from './AggregateRoot'; +import { BackofficeCourseDuration } from './BackofficeCourseDuration'; +import { BackofficeCourseId } from './BackofficeCourseId'; +import { BackofficeCourseName } from './BackofficeCourseName'; + +export class BackofficeCourse extends AggregateRoot { + readonly id: BackofficeCourseId; + readonly name: BackofficeCourseName; + readonly duration: BackofficeCourseDuration; + + constructor(id: BackofficeCourseId, name: BackofficeCourseName, duration: BackofficeCourseDuration) { + super(); + this.id = id; + this.name = name; + this.duration = duration; + } + + static create(id: BackofficeCourseId, name: BackofficeCourseName, duration: BackofficeCourseDuration): BackofficeCourse { + const course = new BackofficeCourse(id, name, duration); + + return course; + } + + static fromPrimitives(plainData: { id: string; name: string; duration: string }): BackofficeCourse { + return new BackofficeCourse( + new BackofficeCourseId(plainData.id), + new BackofficeCourseName(plainData.name), + new BackofficeCourseDuration(plainData.duration) + ); + } + + toPrimitives() { + return { + id: this.id.value, + name: this.name.value, + duration: this.duration.value + }; + } +} \ No newline at end of file diff --git a/src/Contexts/Backoffice/domain/BackofficeCourseDuration.ts b/src/Contexts/Backoffice/domain/BackofficeCourseDuration.ts new file mode 100644 index 0000000..891005a --- /dev/null +++ b/src/Contexts/Backoffice/domain/BackofficeCourseDuration.ts @@ -0,0 +1,3 @@ +import { StringValueObject } from '../../Shared/domain/value-object/StringValueObject'; + +export class BackofficeCourseDuration extends StringValueObject {} \ No newline at end of file diff --git a/src/Contexts/Backoffice/domain/BackofficeCourseId.ts b/src/Contexts/Backoffice/domain/BackofficeCourseId.ts new file mode 100644 index 0000000..399da0a --- /dev/null +++ b/src/Contexts/Backoffice/domain/BackofficeCourseId.ts @@ -0,0 +1,3 @@ +import { Uuid } from '../../Shared/domain/value-object/Uuid'; + +export class BackofficeCourseId extends Uuid {} \ No newline at end of file diff --git a/src/Contexts/Backoffice/domain/BackofficeCourseName.ts b/src/Contexts/Backoffice/domain/BackofficeCourseName.ts new file mode 100644 index 0000000..3986aea --- /dev/null +++ b/src/Contexts/Backoffice/domain/BackofficeCourseName.ts @@ -0,0 +1,3 @@ +import { StringValueObject } from '../../Shared/domain/value-object/StringValueObject'; + +export class BackofficeCourseName extends StringValueObject {} \ No newline at end of file From 3625f7b63d8795f959e1a38aa04f97f98bafb271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Fri, 23 Oct 2020 18:24:29 +0200 Subject: [PATCH 07/12] Create SearchAllCourses query and CoursesFinder --- .../application/SearchAll/CoursesFinder.ts | 12 ++++++++ .../SearchAll/SearchAllCoursesQuery.ts | 3 ++ .../SearchAll/SearchAllCoursesQueryHandler.ts | 17 +++++++++++ .../SearchAll/SearchAllCoursesResponse.ts | 10 +++++++ .../Backoffice/domain/AggregateRoot.ts | 2 +- .../domain/BackofficeCourseRepository.ts | 5 ++++ .../Shared/application.yaml | 10 ++++++- .../BackofficeCourseRepositoryMock.ts | 20 +++++++++++++ .../SearchAllCoursesQueryHandler.test.ts | 30 +++++++++++++++++++ .../domain/BackofficeCourseDurationMother.ts | 12 ++++++++ .../domain/BackofficeCourseIdMother.ts | 16 ++++++++++ .../domain/BackofficeCourseMother.ts | 25 ++++++++++++++++ .../domain/BackofficeCourseNameMother.ts | 12 ++++++++ .../domain/SearchAllCoursesResponseMother.ts | 8 +++++ 14 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 src/Contexts/Backoffice/application/SearchAll/CoursesFinder.ts create mode 100644 src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQuery.ts create mode 100644 src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQueryHandler.ts create mode 100644 src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesResponse.ts create mode 100644 src/Contexts/Backoffice/domain/BackofficeCourseRepository.ts create mode 100644 tests/Contexts/Backoffice/__mocks__/BackofficeCourseRepositoryMock.ts create mode 100644 tests/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQueryHandler.test.ts create mode 100644 tests/Contexts/Backoffice/application/domain/BackofficeCourseDurationMother.ts create mode 100644 tests/Contexts/Backoffice/application/domain/BackofficeCourseIdMother.ts create mode 100644 tests/Contexts/Backoffice/application/domain/BackofficeCourseMother.ts create mode 100644 tests/Contexts/Backoffice/application/domain/BackofficeCourseNameMother.ts create mode 100644 tests/Contexts/Backoffice/application/domain/SearchAllCoursesResponseMother.ts diff --git a/src/Contexts/Backoffice/application/SearchAll/CoursesFinder.ts b/src/Contexts/Backoffice/application/SearchAll/CoursesFinder.ts new file mode 100644 index 0000000..d5f5785 --- /dev/null +++ b/src/Contexts/Backoffice/application/SearchAll/CoursesFinder.ts @@ -0,0 +1,12 @@ +import { BackofficeCourseRepository } from '../../domain/BackofficeCourseRepository'; +import { SearchAllCoursesResponse } from './SearchAllCoursesResponse'; + +export class CoursesFinder { + constructor(private coursesRepository: BackofficeCourseRepository) {} + + async run() { + const courses = await this.coursesRepository.searchAll(); + + return new SearchAllCoursesResponse(courses); + } +} \ No newline at end of file diff --git a/src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQuery.ts b/src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQuery.ts new file mode 100644 index 0000000..dba974d --- /dev/null +++ b/src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQuery.ts @@ -0,0 +1,3 @@ +import { Query } from '../../../Shared/domain/Query'; + +export class SearchAllCoursesQuery implements Query {} diff --git a/src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQueryHandler.ts b/src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQueryHandler.ts new file mode 100644 index 0000000..123663d --- /dev/null +++ b/src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQueryHandler.ts @@ -0,0 +1,17 @@ +import { Query } from '../../../Shared/domain/Query'; +import { QueryHandler } from '../../../Shared/domain/QueryHandler'; +import { CoursesFinder } from './CoursesFinder'; +import { SearchAllCoursesQuery } from './SearchAllCoursesQuery'; +import { SearchAllCoursesResponse } from './SearchAllCoursesResponse'; + +export class SearchAllCoursesQueryHandler implements QueryHandler { + constructor(private coursesFinder: CoursesFinder) {} + + subscribedTo(): Query { + return SearchAllCoursesQuery; + } + + async handle(_query: SearchAllCoursesQuery): Promise { + return this.coursesFinder.run(); + } +} diff --git a/src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesResponse.ts b/src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesResponse.ts new file mode 100644 index 0000000..f98dfbb --- /dev/null +++ b/src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesResponse.ts @@ -0,0 +1,10 @@ +import { BackofficeCourse } from '../../domain/BackofficeCourse'; + +export class SearchAllCoursesResponse { + readonly courses: Array; + + constructor(courses: Array) { + this.courses = courses; + } + } + \ No newline at end of file diff --git a/src/Contexts/Backoffice/domain/AggregateRoot.ts b/src/Contexts/Backoffice/domain/AggregateRoot.ts index d44d335..e111fef 100644 --- a/src/Contexts/Backoffice/domain/AggregateRoot.ts +++ b/src/Contexts/Backoffice/domain/AggregateRoot.ts @@ -1,4 +1,4 @@ -import { DomainEvent } from '../../../Shared/domain/DomainEvent'; +import { DomainEvent } from '../../Shared/domain/DomainEvent'; export abstract class AggregateRoot { private domainEvents: Array; diff --git a/src/Contexts/Backoffice/domain/BackofficeCourseRepository.ts b/src/Contexts/Backoffice/domain/BackofficeCourseRepository.ts new file mode 100644 index 0000000..c7d1a60 --- /dev/null +++ b/src/Contexts/Backoffice/domain/BackofficeCourseRepository.ts @@ -0,0 +1,5 @@ +import { BackofficeCourse } from './BackofficeCourse'; + +export interface BackofficeCourseRepository { + searchAll(): Promise>; +} 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 24f2d8e..09f7db8 100644 --- a/src/apps/backoffice/backend/config/dependency-injection/Shared/application.yaml +++ b/src/apps/backoffice/backend/config/dependency-injection/Shared/application.yaml @@ -7,4 +7,12 @@ services: factory: class: ../../../../../../Contexts/Shared/infrastructure/persistence/mongo/MongoClientFactory method: 'createClient' - arguments: ['mooc'] \ No newline at end of file + arguments: ['mooc'] + + Shared.QueryHandlersInformation: + class: ../../../../../../Contexts/Shared/infrastructure/QueryBus/QueryHandlersInformation + arguments: ['!tagged queryHandler'] + + Shared.QueryBus: + class: ../../../../../../Contexts/Shared/infrastructure/QueryBus/InMemoryQueryBus + arguments: ['@Shared.QueryHandlersInformation'] \ No newline at end of file diff --git a/tests/Contexts/Backoffice/__mocks__/BackofficeCourseRepositoryMock.ts b/tests/Contexts/Backoffice/__mocks__/BackofficeCourseRepositoryMock.ts new file mode 100644 index 0000000..b15a629 --- /dev/null +++ b/tests/Contexts/Backoffice/__mocks__/BackofficeCourseRepositoryMock.ts @@ -0,0 +1,20 @@ +import { BackofficeCourse } from '../../../../src/Contexts/Backoffice/domain/BackofficeCourse'; +import { BackofficeCourseRepository } from '../../../../src/Contexts/Backoffice/domain/BackofficeCourseRepository'; + +export class BackofficeCourseRepositoryMock implements BackofficeCourseRepository { + private mockSearchAll = jest.fn(); + private courses: Array = []; + + returnOnSearchAll(courses: Array) { + this.courses = courses; + } + + async searchAll(): Promise { + this.mockSearchAll(); + return this.courses; + } + + assertSearchAll() { + expect(this.mockSearchAll).toHaveBeenCalled(); + } +} diff --git a/tests/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQueryHandler.test.ts b/tests/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQueryHandler.test.ts new file mode 100644 index 0000000..96bf9cb --- /dev/null +++ b/tests/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQueryHandler.test.ts @@ -0,0 +1,30 @@ +import { SearchAllCoursesQueryHandler } from '../../../../../src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQueryHandler'; +import { CoursesFinder } from '../../../../../src/Contexts/Backoffice/application/SearchAll/CoursesFinder'; +import { SearchAllCoursesQuery } from '../../../../../src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQUery'; +import { BackofficeCourseRepositoryMock } from '../../__mocks__/BackofficeCourseRepositoryMock'; +import { BackofficeCourseMother } from '../domain/BackofficeCourseMother'; +import { SearchAllCoursesResponseMother } from '../domain/SearchAllCoursesResponseMother'; + +describe('SearchAllCourses QueryHandler', () => { + let repository: BackofficeCourseRepositoryMock; + + beforeEach(() => { + repository = new BackofficeCourseRepositoryMock(); + }); + + + it('should find an existing courses counter', async () => { + const courses = [BackofficeCourseMother.random(), BackofficeCourseMother.random(), BackofficeCourseMother.random()]; + repository.returnOnSearchAll(courses); + + const handler = new SearchAllCoursesQueryHandler(new CoursesFinder(repository)); + + const query = new SearchAllCoursesQuery(); + const response = await handler.handle(query); + + repository.assertSearchAll(); + + const expected = SearchAllCoursesResponseMother.create(courses); + expect(expected).toEqual(response); + }); +}); diff --git a/tests/Contexts/Backoffice/application/domain/BackofficeCourseDurationMother.ts b/tests/Contexts/Backoffice/application/domain/BackofficeCourseDurationMother.ts new file mode 100644 index 0000000..b784aab --- /dev/null +++ b/tests/Contexts/Backoffice/application/domain/BackofficeCourseDurationMother.ts @@ -0,0 +1,12 @@ +import { BackofficeCourseDuration } from '../../../../../src/Contexts/Backoffice/domain/BackofficeCourseDuration'; +import { WordMother } from '../../../Shared/domain/WordMother'; + +export class BackofficeCourseDurationMother { + static create(value: string): BackofficeCourseDuration { + return new BackofficeCourseDuration(value); + } + + static random(): BackofficeCourseDuration { + return this.create(WordMother.random()); + } +} diff --git a/tests/Contexts/Backoffice/application/domain/BackofficeCourseIdMother.ts b/tests/Contexts/Backoffice/application/domain/BackofficeCourseIdMother.ts new file mode 100644 index 0000000..075ce12 --- /dev/null +++ b/tests/Contexts/Backoffice/application/domain/BackofficeCourseIdMother.ts @@ -0,0 +1,16 @@ +import { BackofficeCourseId } from '../../../../../src/Contexts/Backoffice/domain/BackofficeCourseId'; +import { UuidMother } from '../../../Shared/domain/UuidMother'; + +export class BackofficeCourseIdMother { + static create(value: string): BackofficeCourseId { + return new BackofficeCourseId(value); + } + + static creator() { + return () => BackofficeCourseIdMother.random(); + } + + static random(): BackofficeCourseId { + return this.create(UuidMother.random()); + } +} diff --git a/tests/Contexts/Backoffice/application/domain/BackofficeCourseMother.ts b/tests/Contexts/Backoffice/application/domain/BackofficeCourseMother.ts new file mode 100644 index 0000000..7acdb14 --- /dev/null +++ b/tests/Contexts/Backoffice/application/domain/BackofficeCourseMother.ts @@ -0,0 +1,25 @@ +import { BackofficeCourse } from '../../../../../src/Contexts/Backoffice/domain/BackofficeCourse'; +import { BackofficeCourseDuration } from '../../../../../src/Contexts/Backoffice/domain/BackofficeCourseDuration'; +import { BackofficeCourseId } from '../../../../../src/Contexts/Backoffice/domain/BackofficeCourseId'; +import { BackofficeCourseName } from '../../../../../src/Contexts/Backoffice/domain/BackofficeCourseName'; +import { BackofficeCourseDurationMother } from './BackofficeCourseDurationMother'; +import { BackofficeCourseIdMother } from './BackofficeCourseIdMother'; +import { BackofficeCourseNameMother } from './BackofficeCourseNameMother'; + +export class BackofficeCourseMother { + static create( + id: BackofficeCourseId, + name: BackofficeCourseName, + duration: BackofficeCourseDuration + ): BackofficeCourse { + return new BackofficeCourse(id, name, duration); + } + + static random(): BackofficeCourse { + return this.create( + BackofficeCourseIdMother.random(), + BackofficeCourseNameMother.random(), + BackofficeCourseDurationMother.random() + ); + } +} diff --git a/tests/Contexts/Backoffice/application/domain/BackofficeCourseNameMother.ts b/tests/Contexts/Backoffice/application/domain/BackofficeCourseNameMother.ts new file mode 100644 index 0000000..c23c47e --- /dev/null +++ b/tests/Contexts/Backoffice/application/domain/BackofficeCourseNameMother.ts @@ -0,0 +1,12 @@ +import { BackofficeCourseName } from '../../../../../src/Contexts/Backoffice/domain/BackofficeCourseName'; +import { WordMother } from '../../../Shared/domain/WordMother'; + +export class BackofficeCourseNameMother { + static create(value: string): BackofficeCourseName { + return new BackofficeCourseName(value); + } + + static random(): BackofficeCourseName { + return this.create(WordMother.random()); + } +} diff --git a/tests/Contexts/Backoffice/application/domain/SearchAllCoursesResponseMother.ts b/tests/Contexts/Backoffice/application/domain/SearchAllCoursesResponseMother.ts new file mode 100644 index 0000000..980c25e --- /dev/null +++ b/tests/Contexts/Backoffice/application/domain/SearchAllCoursesResponseMother.ts @@ -0,0 +1,8 @@ +import { BackofficeCourse } from '../../../../../src/Contexts/Backoffice/domain/BackofficeCourse'; +import { SearchAllCoursesResponse } from '../../../../../src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesResponse'; + +export class SearchAllCoursesResponseMother { + static create(courses: Array) { + return new SearchAllCoursesResponse(courses); + } +} From f4a71f20cf2a856747487c8bdd58ebf6bf9d82b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Sat, 24 Oct 2020 01:10:01 +0200 Subject: [PATCH 08/12] Implement MongoBackofficeCourseRepository --- .../MongoBackofficeCourseRepository.ts | 26 +++++++++++++++++++ .../MongoBackofficeCourseRepository.test.ts | 25 ++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 src/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.ts create mode 100644 tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts diff --git a/src/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.ts b/src/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.ts new file mode 100644 index 0000000..6bf839a --- /dev/null +++ b/src/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.ts @@ -0,0 +1,26 @@ +import { MongoRepository } from '../../Shared/infrastructure/persistence/mongo/MongoRepository'; +import { BackofficeCourse } from '../domain/BackofficeCourse'; +import { BackofficeCourseRepository } from '../domain/BackofficeCourseRepository'; + +export class MongoBackofficeCourseRepository extends MongoRepository + implements BackofficeCourseRepository { + protected moduleName(): string { + return 'backofficeCourses'; + } + + async searchAll(): Promise { + const collection = await this.collection(); + + const documents = await collection.find({}); + + const courses: Array = (await documents.toArray()).map(document => + BackofficeCourse.fromPrimitives({ ...document, id: document._id }) + ); + + return courses; + } + + async save(course: BackofficeCourse) { + return this.persist(course.id.value, course); + } +} diff --git a/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts b/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts new file mode 100644 index 0000000..8551899 --- /dev/null +++ b/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts @@ -0,0 +1,25 @@ +import container from '../../../../src/apps/backoffice/backend/config/dependency-injection'; +import { MongoBackofficeCourseRepository } from '../../../../src/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository'; +import { EnvironmentArranger } from '../../Shared/infrastructure/arranger/EnvironmentArranger'; +import { BackofficeCourseMother } from '../application/domain/BackofficeCourseMother'; + +const repository: MongoBackofficeCourseRepository = container.get('Backoffice.Backend.courses.BackofficeCourseRepository'); +const environmentArranger: Promise = container.get('Backoffice.Backend.EnvironmentArranger'); + +beforeEach(async () => { + await (await environmentArranger).arrange(); +}); + +afterAll(async () => { + await (await environmentArranger).close(); +}); + +describe('Search all courses', () => { + it('should return the existing courses', async () => { + const courses = [BackofficeCourseMother.random(), BackofficeCourseMother.random(), BackofficeCourseMother.random()]; + + await Promise.all(courses.map(course => repository.save(course))); + + expect(courses).toEqual(await repository.searchAll()); + }); +}); From 71098d6bd66b12c5b6b36f36d89318aec11e90af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Sat, 24 Oct 2020 01:10:35 +0200 Subject: [PATCH 09/12] Ask SearchAllCoursesQuery from controller --- .../Backoffice/domain/BackofficeCourse.ts | 70 ++++++++++--------- .../Courses/application.yaml | 15 +++- .../apps/application.yaml | 2 +- .../controllers/CoursesGetController.ts | 23 +++--- .../backend/routes/courses.route.ts | 2 +- .../step_definitions/repository.steps.ts | 2 +- 6 files changed, 61 insertions(+), 53 deletions(-) diff --git a/src/Contexts/Backoffice/domain/BackofficeCourse.ts b/src/Contexts/Backoffice/domain/BackofficeCourse.ts index 61a72b7..fed5e1c 100644 --- a/src/Contexts/Backoffice/domain/BackofficeCourse.ts +++ b/src/Contexts/Backoffice/domain/BackofficeCourse.ts @@ -1,39 +1,43 @@ -import { AggregateRoot } from './AggregateRoot'; +import { AggregateRoot } from '../../Mooc/Courses/domain/AggregateRoot'; import { BackofficeCourseDuration } from './BackofficeCourseDuration'; import { BackofficeCourseId } from './BackofficeCourseId'; import { BackofficeCourseName } from './BackofficeCourseName'; export class BackofficeCourse extends AggregateRoot { - readonly id: BackofficeCourseId; - readonly name: BackofficeCourseName; - readonly duration: BackofficeCourseDuration; + readonly id: BackofficeCourseId; + readonly name: BackofficeCourseName; + readonly duration: BackofficeCourseDuration; - constructor(id: BackofficeCourseId, name: BackofficeCourseName, duration: BackofficeCourseDuration) { - super(); - this.id = id; - this.name = name; - this.duration = duration; - } - - static create(id: BackofficeCourseId, name: BackofficeCourseName, duration: BackofficeCourseDuration): BackofficeCourse { - const course = new BackofficeCourse(id, name, duration); - - return course; - } - - static fromPrimitives(plainData: { id: string; name: string; duration: string }): BackofficeCourse { - return new BackofficeCourse( - new BackofficeCourseId(plainData.id), - new BackofficeCourseName(plainData.name), - new BackofficeCourseDuration(plainData.duration) - ); - } - - toPrimitives() { - return { - id: this.id.value, - name: this.name.value, - duration: this.duration.value - }; - } -} \ No newline at end of file + constructor(id: BackofficeCourseId, name: BackofficeCourseName, duration: BackofficeCourseDuration) { + super(); + this.id = id; + this.name = name; + this.duration = duration; + } + + static create( + id: BackofficeCourseId, + name: BackofficeCourseName, + duration: BackofficeCourseDuration + ): BackofficeCourse { + const course = new BackofficeCourse(id, name, duration); + + return course; + } + + static fromPrimitives(plainData: { id: string; name: string; duration: string }): BackofficeCourse { + return new BackofficeCourse( + new BackofficeCourseId(plainData.id), + new BackofficeCourseName(plainData.name), + new BackofficeCourseDuration(plainData.duration) + ); + } + + toPrimitives() { + return { + id: this.id.value, + name: this.name.value, + duration: this.duration.value + }; + } +} 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 d9ebb37..e9abb18 100644 --- a/src/apps/backoffice/backend/config/dependency-injection/Courses/application.yaml +++ b/src/apps/backoffice/backend/config/dependency-injection/Courses/application.yaml @@ -1,6 +1,15 @@ services: - Backoffice.Backend.courses.CourseRepository: - class: ../../../../../../Contexts/Mooc/Courses/infrastructure/persistence/MongoCourseRepository + Backoffice.Backend.courses.BackofficeCourseRepository: + class: ../../../../../../Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository arguments: ['@Shared.ConnectionManager'] - \ No newline at end of file + Backoffice.Backend.courses.CoursesFinder: + class: ../../../../../../Contexts/Backoffice/application/SearchAll/CoursesFinder + arguments: ["@Backoffice.Backend.courses.BackofficeCourseRepository"] + + Backoffice.Backend.courses.SearchAllCoursesQueryHandler: + class: ../../../../../../Contexts/Backoffice/application/SearchAll/SearchAllCoursesQueryHandler + arguments: ["@Backoffice.Backend.courses.CoursesFinder"] + tags: + - { name: 'queryHandler' } + diff --git a/src/apps/backoffice/backend/config/dependency-injection/apps/application.yaml b/src/apps/backoffice/backend/config/dependency-injection/apps/application.yaml index 8a1f37f..e37a466 100644 --- a/src/apps/backoffice/backend/config/dependency-injection/apps/application.yaml +++ b/src/apps/backoffice/backend/config/dependency-injection/apps/application.yaml @@ -5,4 +5,4 @@ services: Apps.Backoffice.Backend.controllers.CoursesGetController: class: ../../../controllers/CoursesGetController - arguments: [] \ No newline at end of file + arguments: ['@Shared.QueryBus'] \ 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 eb5cccb..82be7c7 100644 --- a/src/apps/backoffice/backend/controllers/CoursesGetController.ts +++ b/src/apps/backoffice/backend/controllers/CoursesGetController.ts @@ -1,21 +1,16 @@ import { Request, Response } from 'express'; +import httpStatus from 'http-status'; +import { SearchAllCoursesQuery } from '../../../../Contexts/Backoffice/application/SearchAll/SearchAllCoursesQuery'; +import { QueryBus } from '../../../../Contexts/Shared/domain/QueryBus'; import { Controller } from './Controller'; export class CoursesGetController implements Controller { + constructor(private queryBus: QueryBus) {} + async run(_req: Request, res: Response) { - res.status(200).send({ - 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' - } - ] - }); + const query = new SearchAllCoursesQuery(); + const courses = await this.queryBus.ask(query); + + res.status(httpStatus.OK).send(courses); } } diff --git a/src/apps/backoffice/backend/routes/courses.route.ts b/src/apps/backoffice/backend/routes/courses.route.ts index 91a5968..f885b6c 100644 --- a/src/apps/backoffice/backend/routes/courses.route.ts +++ b/src/apps/backoffice/backend/routes/courses.route.ts @@ -4,5 +4,5 @@ import { CoursesGetController } from '../controllers/CoursesGetController'; export const register = (app: Express) => { const coursesGetController: CoursesGetController = container.get('Apps.Backoffice.Backend.controllers.CoursesGetController'); - app.get('/courses', coursesGetController.run); + app.get('/courses', coursesGetController.run.bind(coursesGetController)); }; diff --git a/tests/apps/backoffice/backend/features/step_definitions/repository.steps.ts b/tests/apps/backoffice/backend/features/step_definitions/repository.steps.ts index 711df31..14ab86c 100644 --- a/tests/apps/backoffice/backend/features/step_definitions/repository.steps.ts +++ b/tests/apps/backoffice/backend/features/step_definitions/repository.steps.ts @@ -6,7 +6,7 @@ import { CourseName } from '../../../../../../src/Contexts/Mooc/Courses/domain/C import { CourseRepository } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseRepository'; import { CourseId } from '../../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseId'; -const courseRepository: CourseRepository = container.get('Backoffice.Backend.courses.CourseRepository'); +const courseRepository: CourseRepository = container.get('Backoffice.Backend.courses.BackofficeCourseRepository'); Given('there is the course:', async (course: any) => { const { id, name, duration } = JSON.parse(course); From 8f2145713518a25357fe7140ae04cd2f53856218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Sat, 24 Oct 2020 01:15:31 +0200 Subject: [PATCH 10/12] Fix tslint issues --- .../application/SearchAll/CoursesFinder.ts | 12 ++++++------ .../SearchAll/SearchAllCoursesResponse.ts | 11 +++++------ .../Backoffice/domain/BackofficeCourseDuration.ts | 2 +- src/Contexts/Backoffice/domain/BackofficeCourseId.ts | 2 +- .../Backoffice/domain/BackofficeCourseName.ts | 2 +- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/Contexts/Backoffice/application/SearchAll/CoursesFinder.ts b/src/Contexts/Backoffice/application/SearchAll/CoursesFinder.ts index d5f5785..2d298f8 100644 --- a/src/Contexts/Backoffice/application/SearchAll/CoursesFinder.ts +++ b/src/Contexts/Backoffice/application/SearchAll/CoursesFinder.ts @@ -2,11 +2,11 @@ import { BackofficeCourseRepository } from '../../domain/BackofficeCourseReposit import { SearchAllCoursesResponse } from './SearchAllCoursesResponse'; export class CoursesFinder { - constructor(private coursesRepository: BackofficeCourseRepository) {} + constructor(private coursesRepository: BackofficeCourseRepository) {} - async run() { - const courses = await this.coursesRepository.searchAll(); + async run() { + const courses = await this.coursesRepository.searchAll(); - return new SearchAllCoursesResponse(courses); - } -} \ No newline at end of file + return new SearchAllCoursesResponse(courses); + } +} diff --git a/src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesResponse.ts b/src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesResponse.ts index f98dfbb..9c13b41 100644 --- a/src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesResponse.ts +++ b/src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesResponse.ts @@ -1,10 +1,9 @@ import { BackofficeCourse } from '../../domain/BackofficeCourse'; export class SearchAllCoursesResponse { - readonly courses: Array; - - constructor(courses: Array) { - this.courses = courses; - } + readonly courses: Array; + + constructor(courses: Array) { + this.courses = courses; } - \ No newline at end of file +} diff --git a/src/Contexts/Backoffice/domain/BackofficeCourseDuration.ts b/src/Contexts/Backoffice/domain/BackofficeCourseDuration.ts index 891005a..52fa081 100644 --- a/src/Contexts/Backoffice/domain/BackofficeCourseDuration.ts +++ b/src/Contexts/Backoffice/domain/BackofficeCourseDuration.ts @@ -1,3 +1,3 @@ import { StringValueObject } from '../../Shared/domain/value-object/StringValueObject'; -export class BackofficeCourseDuration extends StringValueObject {} \ No newline at end of file +export class BackofficeCourseDuration extends StringValueObject {} diff --git a/src/Contexts/Backoffice/domain/BackofficeCourseId.ts b/src/Contexts/Backoffice/domain/BackofficeCourseId.ts index 399da0a..a62c7f2 100644 --- a/src/Contexts/Backoffice/domain/BackofficeCourseId.ts +++ b/src/Contexts/Backoffice/domain/BackofficeCourseId.ts @@ -1,3 +1,3 @@ import { Uuid } from '../../Shared/domain/value-object/Uuid'; -export class BackofficeCourseId extends Uuid {} \ No newline at end of file +export class BackofficeCourseId extends Uuid {} diff --git a/src/Contexts/Backoffice/domain/BackofficeCourseName.ts b/src/Contexts/Backoffice/domain/BackofficeCourseName.ts index 3986aea..a04a9c0 100644 --- a/src/Contexts/Backoffice/domain/BackofficeCourseName.ts +++ b/src/Contexts/Backoffice/domain/BackofficeCourseName.ts @@ -1,3 +1,3 @@ import { StringValueObject } from '../../Shared/domain/value-object/StringValueObject'; -export class BackofficeCourseName extends StringValueObject {} \ No newline at end of file +export class BackofficeCourseName extends StringValueObject {} From 3520bb2a3a2f3056ae9682d1145d7254982700e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Sat, 24 Oct 2020 01:19:13 +0200 Subject: [PATCH 11/12] Fix import typo --- .../application/SearchAll/SearchAllCoursesQueryHandler.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQueryHandler.test.ts b/tests/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQueryHandler.test.ts index 96bf9cb..0992d93 100644 --- a/tests/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQueryHandler.test.ts +++ b/tests/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQueryHandler.test.ts @@ -1,6 +1,6 @@ import { SearchAllCoursesQueryHandler } from '../../../../../src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQueryHandler'; import { CoursesFinder } from '../../../../../src/Contexts/Backoffice/application/SearchAll/CoursesFinder'; -import { SearchAllCoursesQuery } from '../../../../../src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQUery'; +import { SearchAllCoursesQuery } from '../../../../../src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesQuery'; import { BackofficeCourseRepositoryMock } from '../../__mocks__/BackofficeCourseRepositoryMock'; import { BackofficeCourseMother } from '../domain/BackofficeCourseMother'; import { SearchAllCoursesResponseMother } from '../domain/SearchAllCoursesResponseMother'; From e4494cb598dcf54c05bb1d9418673011be6335d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Portillo?= Date: Sat, 24 Oct 2020 01:33:01 +0200 Subject: [PATCH 12/12] Fix test --- .../infrastructure/MongoBackofficeCourseRepository.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts b/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts index 8551899..6b48d1c 100644 --- a/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts +++ b/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts @@ -16,10 +16,11 @@ afterAll(async () => { describe('Search all courses', () => { it('should return the existing courses', async () => { - const courses = [BackofficeCourseMother.random(), BackofficeCourseMother.random(), BackofficeCourseMother.random()]; + const courses = [BackofficeCourseMother.random(), BackofficeCourseMother.random()]; await Promise.all(courses.map(course => repository.save(course))); - expect(courses).toEqual(await repository.searchAll()); + const expectedCourses = await repository.searchAll(); + expect(courses.sort()).toEqual(expectedCourses.sort()); }); });