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 ff3c9df..5e8d400 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,16 @@ "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 && npm run cypress:run", "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", + "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/Contexts/Backoffice/application/SearchAll/CoursesFinder.ts b/src/Contexts/Backoffice/application/SearchAll/CoursesFinder.ts new file mode 100644 index 0000000..2d298f8 --- /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); + } +} 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..9c13b41 --- /dev/null +++ b/src/Contexts/Backoffice/application/SearchAll/SearchAllCoursesResponse.ts @@ -0,0 +1,9 @@ +import { BackofficeCourse } from '../../domain/BackofficeCourse'; + +export class SearchAllCoursesResponse { + readonly courses: Array; + + constructor(courses: Array) { + this.courses = courses; + } +} diff --git a/src/Contexts/Backoffice/domain/AggregateRoot.ts b/src/Contexts/Backoffice/domain/AggregateRoot.ts new file mode 100644 index 0000000..e111fef --- /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..fed5e1c --- /dev/null +++ b/src/Contexts/Backoffice/domain/BackofficeCourse.ts @@ -0,0 +1,43 @@ +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; + + 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/Contexts/Backoffice/domain/BackofficeCourseDuration.ts b/src/Contexts/Backoffice/domain/BackofficeCourseDuration.ts new file mode 100644 index 0000000..52fa081 --- /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 {} diff --git a/src/Contexts/Backoffice/domain/BackofficeCourseId.ts b/src/Contexts/Backoffice/domain/BackofficeCourseId.ts new file mode 100644 index 0000000..a62c7f2 --- /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 {} diff --git a/src/Contexts/Backoffice/domain/BackofficeCourseName.ts b/src/Contexts/Backoffice/domain/BackofficeCourseName.ts new file mode 100644 index 0000000..a04a9c0 --- /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 {} 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/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/src/apps/backoffice/backend/app.ts b/src/apps/backoffice/backend/app.ts new file mode 100644 index 0000000..d575de2 --- /dev/null +++ b/src/apps/backoffice/backend/app.ts @@ -0,0 +1,21 @@ +import bodyParser from 'body-parser'; +import express from 'express'; +import helmet from 'helmet'; +import compress from 'compression'; +import { registerRoutes } from './routes'; + +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); + +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/Courses/application.yaml b/src/apps/backoffice/backend/config/dependency-injection/Courses/application.yaml new file mode 100644 index 0000000..e9abb18 --- /dev/null +++ b/src/apps/backoffice/backend/config/dependency-injection/Courses/application.yaml @@ -0,0 +1,15 @@ +services: + Backoffice.Backend.courses.BackofficeCourseRepository: + class: ../../../../../../Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository + arguments: ['@Shared.ConnectionManager'] + + 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/Shared/application.yaml b/src/apps/backoffice/backend/config/dependency-injection/Shared/application.yaml new file mode 100644 index 0000000..09f7db8 --- /dev/null +++ b/src/apps/backoffice/backend/config/dependency-injection/Shared/application.yaml @@ -0,0 +1,18 @@ +services: + Shared.Logger: + class: ../../../../../../Contexts/Shared/infrastructure/WinstonLogger + arguments: [] + + Shared.ConnectionManager: + factory: + class: ../../../../../../Contexts/Shared/infrastructure/persistence/mongo/MongoClientFactory + method: 'createClient' + 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/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..72e0c4c --- /dev/null +++ b/src/apps/backoffice/backend/config/dependency-injection/application.yaml @@ -0,0 +1,4 @@ +imports: + - { resource: ./Shared/application.yaml } + - { resource: ./apps/application.yaml } + - { resource: ./Courses/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..dfdc50a --- /dev/null +++ b/src/apps/backoffice/backend/config/dependency-injection/application_test.yaml @@ -0,0 +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..e37a466 --- /dev/null +++ b/src/apps/backoffice/backend/config/dependency-injection/apps/application.yaml @@ -0,0 +1,8 @@ +services: + Apps.Backoffice.Backend.controllers.StatusGetController: + class: ../../../controllers/StatusGetController + arguments: [] + + Apps.Backoffice.Backend.controllers.CoursesGetController: + class: ../../../controllers/CoursesGetController + arguments: ['@Shared.QueryBus'] \ No newline at end of file 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/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/CoursesGetController.ts b/src/apps/backoffice/backend/controllers/CoursesGetController.ts new file mode 100644 index 0000000..82be7c7 --- /dev/null +++ b/src/apps/backoffice/backend/controllers/CoursesGetController.ts @@ -0,0 +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) { + const query = new SearchAllCoursesQuery(); + const courses = await this.queryBus.ask(query); + + res.status(httpStatus.OK).send(courses); + } +} 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/courses.route.ts b/src/apps/backoffice/backend/routes/courses.route.ts new file mode 100644 index 0000000..f885b6c --- /dev/null +++ b/src/apps/backoffice/backend/routes/courses.route.ts @@ -0,0 +1,8 @@ +import { Express } from 'express'; +import container from '../config/dependency-injection'; +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.bind(coursesGetController)); +}; 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/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; 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..0992d93 --- /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); + } +} diff --git a/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts b/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts new file mode 100644 index 0000000..6b48d1c --- /dev/null +++ b/tests/Contexts/Backoffice/infrastructure/MongoBackofficeCourseRepository.test.ts @@ -0,0 +1,26 @@ +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()]; + + await Promise.all(courses.map(course => repository.save(course))); + + const expectedCourses = await repository.searchAll(); + expect(courses.sort()).toEqual(expectedCourses.sort()); + }); +}); 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..2a54e19 --- /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/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..b203470 --- /dev/null +++ b/tests/apps/backoffice/backend/features/step_definitions/controller.steps.ts @@ -0,0 +1,34 @@ +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); +}); + +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(); +}); + +AfterAll(async () => { + const environmentArranger: Promise = container.get('Backoffice.Backend.EnvironmentArranger'); + await (await environmentArranger).arrange(); + await (await environmentArranger).close(); +}); 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..14ab86c --- /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.BackofficeCourseRepository'); + +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))); +});