diff --git a/docker-compose.yml b/docker-compose.yml index 9fe833c..d7dcc89 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ services: mooc-backend: <<: *default-app - command: bash -c "npm run build && npm run start" + command: bash -c "npm run build && npm run dev" ports: - 3000:3000 diff --git a/src/Contexts/Mooc/Courses/application/ChangeDescriptionCourse/ChangeDescriptionCourseCommand.ts b/src/Contexts/Mooc/Courses/application/ChangeDescriptionCourse/ChangeDescriptionCourseCommand.ts new file mode 100644 index 0000000..bce1a4d --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/ChangeDescriptionCourse/ChangeDescriptionCourseCommand.ts @@ -0,0 +1,12 @@ +import { Command } from '../../../../Shared/domain/Command'; + +export class ChangeDescriptionCourseCommand extends Command { + readonly id: string; + readonly description: string; + + constructor(id: string, description: string) { + super(); + this.id = id; + this.description = description; + } +} \ No newline at end of file diff --git a/src/Contexts/Mooc/Courses/application/CourseCreator.ts b/src/Contexts/Mooc/Courses/application/CourseCreator.ts deleted file mode 100644 index db2b81b..0000000 --- a/src/Contexts/Mooc/Courses/application/CourseCreator.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { CourseRepository } from '../domain/CourseRepository'; -import { Course } from '../domain/Course'; -import { CreateCourseRequest } from './CreateCourseRequest'; -import { CourseId } from '../../Shared/domain/Courses/CourseId'; -import { CourseName } from '../domain/CourseName'; -import { CourseDuration } from '../domain/CourseDuration'; -import { EventBus } from '../../../Shared/domain/EventBus'; - -type Params = { - courseId: CourseId; - courseName: CourseName; - courseDuration: CourseDuration; -}; - -export class CourseCreator { - private repository: CourseRepository; - private eventBus: EventBus; - - constructor(repository: CourseRepository, eventBus: EventBus) { - this.repository = repository; - this.eventBus = eventBus; - } - - async run({ courseId, courseName, courseDuration }: Params): Promise { - const course = Course.create( - courseId, - courseName, - courseDuration - ); - - await this.repository.save(course); - await this.eventBus.publish(course.pullDomainEvents()); - } -} diff --git a/src/Contexts/Mooc/Courses/application/CreateCourse/CourseCreator.ts b/src/Contexts/Mooc/Courses/application/CreateCourse/CourseCreator.ts new file mode 100644 index 0000000..c674f2e --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/CreateCourse/CourseCreator.ts @@ -0,0 +1,36 @@ +import { CourseRepository } from '../../domain/CourseRepository'; +import { Course } from '../../domain/Course'; +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { CourseDuration } from '../../domain/CourseDuration'; +import { EventBus } from '../../../../Shared/domain/EventBus'; +import { CourseName } from '../../../Shared/domain/Courses/CourseName'; +import { CourseDescription } from '../../domain/CourseDescription'; + +type Params = { + courseId: CourseId; + courseName: CourseName; + courseDuration: CourseDuration; + courseDescription: CourseDescription +}; + +export class CourseCreator { + private repository: CourseRepository; + private eventBus: EventBus; + + constructor(repository: CourseRepository, eventBus: EventBus) { + this.repository = repository; + this.eventBus = eventBus; + } + + async run({ courseId, courseName, courseDuration, courseDescription }: Params): Promise { + const course = Course.create( + courseId, + courseName, + courseDuration, + courseDescription + ); + + await this.repository.save(course); + await this.eventBus.publish(course.pullDomainEvents()); + } +} diff --git a/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand.ts b/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand.ts new file mode 100644 index 0000000..0b3edfe --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand.ts @@ -0,0 +1,23 @@ +import { Command } from '../../../../Shared/domain/Command'; + +type Params = { + id: string; + name: string; + duration: string; + description: string; +}; + +export class CreateCourseCommand extends Command { + readonly id: string; + readonly name: string; + readonly duration: string; + readonly description: string; + + constructor({ id, name, duration, description }: Params) { + super(); + this.id = id; + this.name = name; + this.duration = duration; + this.description = description; + } +} diff --git a/src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler.ts b/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler.ts similarity index 55% rename from src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler.ts rename to src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler.ts index 2c8c60e..f872ca5 100644 --- a/src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler.ts +++ b/src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler.ts @@ -1,10 +1,11 @@ import { CreateCourseCommand } from './CreateCourseCommand'; -import { CommandHandler } from '../../../Shared/domain/CommandHandler'; +import { CommandHandler } from '../../../../Shared/domain/CommandHandler'; import { CourseCreator } from './CourseCreator'; -import { Command } from '../../../Shared/domain/Command'; -import { CourseId } from '../../Shared/domain/Courses/CourseId'; -import { CourseName } from '../domain/CourseName'; -import { CourseDuration } from '../domain/CourseDuration'; +import { Command } from '../../../../Shared/domain/Command'; +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { CourseDuration } from '../../domain/CourseDuration'; +import { CourseName } from '../../../Shared/domain/Courses/CourseName'; +import { CourseDescription } from '../../domain/CourseDescription'; export class CreateCourseCommandHandler implements CommandHandler { constructor(private courseCreator: CourseCreator) {} @@ -17,6 +18,7 @@ export class CreateCourseCommandHandler implements CommandHandler { + const course = await this.courseFinder.run(courseId); + return new CourseResponse(course); + } +} diff --git a/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQuery.ts b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQuery.ts new file mode 100644 index 0000000..0cda0ae --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQuery.ts @@ -0,0 +1,11 @@ +import { GetCourseRequest } from './GetCourseRequest'; +import { Query } from '../../../../Shared/domain/Query'; + +export class GetCourseQuery extends Query { + id: string; + + constructor({ id }: GetCourseRequest) { + super(); + this.id = id; + } +} diff --git a/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.ts b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.ts new file mode 100644 index 0000000..c41f95c --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.ts @@ -0,0 +1,19 @@ +import { GetCourseQuery } from './GetCourseQuery'; +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { CourseFinder } from './CourseFinder'; +import { CourseResponse } from '../../../Shared/domain/Courses/application/CourseResponse'; +import { QueryHandler } from '../../../../Shared/domain/QueryHandler'; +import { Query } from '../../../../Shared/domain/Query'; + +export class GetCourseQueryHandler implements QueryHandler { + constructor(private courseFinder: CourseFinder) {} + + subscribedTo(): Query { + return GetCourseQuery; + } + + async handle(query: GetCourseQuery): Promise { + const courseId = new CourseId(query.id); + return this.courseFinder.run({ courseId }); + } +} diff --git a/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequest.ts b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequest.ts new file mode 100644 index 0000000..b3b8015 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequest.ts @@ -0,0 +1,3 @@ +export type GetCourseRequest = { + id: string; +}; diff --git a/src/Contexts/Mooc/Courses/application/GetCourses/CoursesResponse.ts b/src/Contexts/Mooc/Courses/application/GetCourses/CoursesResponse.ts new file mode 100644 index 0000000..9e26556 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourses/CoursesResponse.ts @@ -0,0 +1,16 @@ +import { Course } from '../../domain/Course'; +import { CourseResponse } from '../../../Shared/domain/Courses/application/CourseResponse'; +import { Nullable } from '../../../../Shared/domain/Nullable'; +export class CoursesResponse { + readonly data: CourseResponse[]; + + constructor(courses: Nullable) { + this.data = []; + if (courses !== null) { + courses.forEach(course => { + this.data.push(new CourseResponse(course)) + }); + } + } +} + \ No newline at end of file diff --git a/src/Contexts/Mooc/Courses/application/GetCourses/CoursesSearcher.ts b/src/Contexts/Mooc/Courses/application/GetCourses/CoursesSearcher.ts new file mode 100644 index 0000000..6aac20b --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourses/CoursesSearcher.ts @@ -0,0 +1,20 @@ +import { CourseFinder as DomainCourseFinder } from '../../domain/CourseFinder'; +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { CourseResponse } from '../../../Shared/domain/Courses/application/CourseResponse'; +import { CourseRepository } from '../../domain/CourseRepository'; +import { Course } from '../../domain/Course'; +import { CoursesResponse } from './CoursesResponse'; +import { Nullable } from '../../../../Shared/domain/Nullable'; + +export class CoursesSearcher { + private repository: CourseRepository; + + constructor(repository: CourseRepository) { + this.repository = repository; + } + + async run(): Promise { + const courses : Nullable = await this.repository.getAll(); + return new CoursesResponse(courses); + } +} diff --git a/src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQuery.ts b/src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQuery.ts new file mode 100644 index 0000000..e28f707 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQuery.ts @@ -0,0 +1,8 @@ +import { Query } from '../../../../Shared/domain/Query'; + +export class GetCoursesQuery extends Query { + + constructor() { + super(); + } +} diff --git a/src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler.ts b/src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler.ts new file mode 100644 index 0000000..a8b3b2e --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler.ts @@ -0,0 +1,17 @@ +import { QueryHandler } from '../../../../Shared/domain/QueryHandler'; +import { Query } from '../../../../Shared/domain/Query'; +import { CoursesResponse } from './CoursesResponse'; +import { GetCoursesQuery } from './GetCoursesQuery'; +import { CoursesSearcher } from './CoursesSearcher'; + +export class GetCoursesQueryHandler implements QueryHandler { + constructor(private coursesSearcher: CoursesSearcher) {} + + subscribedTo(): Query { + return GetCoursesQuery; + } + + async handle(query: GetCoursesQuery): Promise { + return this.coursesSearcher.run(); + } +} diff --git a/src/Contexts/Mooc/Courses/application/LikeCourse/CourseLiker.ts b/src/Contexts/Mooc/Courses/application/LikeCourse/CourseLiker.ts new file mode 100644 index 0000000..17c9296 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/LikeCourse/CourseLiker.ts @@ -0,0 +1,21 @@ +import { CourseId } from './../../../Shared/domain/Courses/CourseId'; +import { CourseFinder } from '../../domain/CourseFinder'; +import { UserId } from '../../domain/UserId'; +import { CourseRepository } from '../../domain/CourseRepository'; + +type Params = { + courseId: CourseId; + userId: UserId; +}; + +export class CourseLiker { + constructor(private courseFinder: CourseFinder, private repository: CourseRepository) {} + + async run({ courseId, userId }: Params): Promise { + const course = await this.courseFinder.run(courseId); + + course.like(userId); + + await this.repository.save(course); + } +} diff --git a/src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommand.ts b/src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommand.ts new file mode 100644 index 0000000..4d05b37 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommand.ts @@ -0,0 +1,16 @@ +import { Command } from '../../../../Shared/domain/Command'; + +type Params = { + id: string; + userId: string; +}; + +export class LikeCourseCommand implements Command { + readonly id: string; + readonly userId: string; + + constructor({ id, userId }: Params) { + this.id = id; + this.userId = userId; + } +} diff --git a/src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler.ts b/src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler.ts new file mode 100644 index 0000000..6878e76 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler.ts @@ -0,0 +1,19 @@ +import { Command } from '../../../../Shared/domain/Command'; +import { CommandHandler } from '../../../../Shared/domain/CommandHandler'; +import { LikeCourseCommand } from './LikeCourseCommand'; +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { CourseLiker } from './CourseLiker'; +import { UserId } from '../../domain/UserId'; +export class LikeCourseCommandHandler implements CommandHandler { + constructor(private courseLiker: CourseLiker) {} + + subscribedTo(): Command { + return LikeCourseCommand; + } + + handle(command: LikeCourseCommand): Promise { + const courseId = new CourseId(command.id); + const userId = new UserId(command.userId); + return this.courseLiker.run({ courseId, userId }); + } +} diff --git a/src/Contexts/Mooc/Courses/application/RenameCourse/CourseRenamer.ts b/src/Contexts/Mooc/Courses/application/RenameCourse/CourseRenamer.ts new file mode 100644 index 0000000..ba8cdd4 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/RenameCourse/CourseRenamer.ts @@ -0,0 +1,30 @@ +import { CourseRepository } from '../../domain/CourseRepository'; +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { EventBus } from '../../../../Shared/domain/EventBus'; +import { CourseName } from '../../../Shared/domain/Courses/CourseName'; +import { CourseFinder } from '../../domain/CourseFinder'; + +type Params = { + courseId: CourseId; + courseName: CourseName; +}; + +export class CourseRenamer { + private courseFinder: CourseFinder; + private eventBus: EventBus; + private repository: CourseRepository; + + constructor(courseFinder: CourseFinder, repository: CourseRepository, eventBus: EventBus) { + this.courseFinder = courseFinder; + this.eventBus = eventBus; + this.repository = repository; + } + + async run({ courseId, courseName }: Params): Promise { + const course = await this.courseFinder.run(courseId); + course.rename(courseName); + + await this.repository.save(course); + await this.eventBus.publish(course.pullDomainEvents()); + } +} \ No newline at end of file diff --git a/src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand.ts b/src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand.ts new file mode 100644 index 0000000..b0f8c1a --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand.ts @@ -0,0 +1,17 @@ +import { Command } from '../../../../Shared/domain/Command'; + +type Params = { + id: string; + name: string; +}; + +export class RenameCourseCommand extends Command { + id: string; + name: string; + + constructor({ id, name }: Params) { + super(); + this.id = id; + this.name = name; + } +} diff --git a/src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommandHandler.ts b/src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommandHandler.ts new file mode 100644 index 0000000..506c3c0 --- /dev/null +++ b/src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommandHandler.ts @@ -0,0 +1,20 @@ +import { CommandHandler } from '../../../../Shared/domain/CommandHandler'; +import { Command } from '../../../../Shared/domain/Command'; +import { CourseId } from '../../../Shared/domain/Courses/CourseId'; +import { CourseName } from '../../../Shared/domain/Courses/CourseName'; +import { CourseRenamer } from './CourseRenamer'; +import { RenameCourseCommand } from './RenameCourseCommand'; + +export class RenameCourseCommandHandler implements CommandHandler { + constructor(private courseRenamer: CourseRenamer) {} + + subscribedTo(): Command { + return RenameCourseCommand; + } + + async handle(command: RenameCourseCommand): Promise { + const courseId = new CourseId(command.id); + const courseName = new CourseName(command.name); + await this.courseRenamer.run({ courseId, courseName }); + } +} diff --git a/src/Contexts/Mooc/Courses/domain/Course.ts b/src/Contexts/Mooc/Courses/domain/Course.ts index 4f2513e..cfc2bd7 100644 --- a/src/Contexts/Mooc/Courses/domain/Course.ts +++ b/src/Contexts/Mooc/Courses/domain/Course.ts @@ -1,40 +1,70 @@ import { AggregateRoot } from './AggregateRoot'; import { CourseCreatedDomainEvent } from './CourseCreatedDomainEvent'; -import { CourseName } from './CourseName'; import { CourseDuration } from './CourseDuration'; import { CourseId } from '../../Shared/domain/Courses/CourseId'; +import { CourseName } from '../../Shared/domain/Courses/CourseName'; +import { CourseRenamedDomainEvent } from './CourseRenamedDomainEvent'; +import { CourseDescription } from './CourseDescription'; +import { CourseLikedDomainEvent } from './CourseLikedDomainEvent'; +import { UserId } from './UserId'; export class Course extends AggregateRoot { readonly id: CourseId; - readonly name: CourseName; - readonly duration: CourseDuration; + name: CourseName; + duration: CourseDuration; + description: CourseDescription; - constructor(id: CourseId, name: CourseName, duration: CourseDuration) { + constructor(id: CourseId, name: CourseName, duration: CourseDuration, description: CourseDescription) { super(); this.id = id; this.name = name; this.duration = duration; + this.description = description; } - static create(id: CourseId, name: CourseName, duration: CourseDuration): Course { - const course = new Course(id, name, duration); + static create(id: CourseId, name: CourseName, duration: CourseDuration, description: CourseDescription): Course { + const course = new Course(id, name, duration, description); course.record( new CourseCreatedDomainEvent({ id: course.id.value, duration: course.duration.value, - name: course.name.value + name: course.name.value, + description: course.description.value }) ); return course; } - static fromPrimitives(plainData: { id: string; name: string; duration: string }): Course { + rename(name: CourseName) { + const oldName = this.name; + this.name = name; + + this.record( + new CourseRenamedDomainEvent({ + id: this.id.value, + oldName: oldName.value, + newName: name.value + }) + ); + } + + like(userId: UserId) { + this.record( + new CourseLikedDomainEvent({ + id: this.id.value, + userId: userId.value + }) + ); + } + + static fromPrimitives(plainData: { id: string; name: string; duration: string; description: string }): Course { return new Course( new CourseId(plainData.id), new CourseName(plainData.name), - new CourseDuration(plainData.duration) + new CourseDuration(plainData.duration), + new CourseDescription(plainData.description) ); } @@ -42,7 +72,8 @@ export class Course extends AggregateRoot { return { id: this.id.value, name: this.name.value, - duration: this.duration.value + duration: this.duration.value, + description: this.description.value }; } } diff --git a/src/Contexts/Mooc/Courses/domain/CourseCreatedDomainEvent.ts b/src/Contexts/Mooc/Courses/domain/CourseCreatedDomainEvent.ts index 2745366..445e702 100644 --- a/src/Contexts/Mooc/Courses/domain/CourseCreatedDomainEvent.ts +++ b/src/Contexts/Mooc/Courses/domain/CourseCreatedDomainEvent.ts @@ -3,6 +3,7 @@ import { DomainEvent } from '../../../Shared/domain/DomainEvent'; type CreateCourseDomainEventBody = { readonly duration: string; readonly name: string; + readonly description: string; }; export class CourseCreatedDomainEvent extends DomainEvent { @@ -10,11 +11,13 @@ export class CourseCreatedDomainEvent extends DomainEvent { readonly duration: string; readonly name: string; + readonly description: string; constructor({ id, name, duration, + description, eventId, occurredOn }: { @@ -22,18 +25,21 @@ export class CourseCreatedDomainEvent extends DomainEvent { eventId?: string; duration: string; name: string; + description: string; occurredOn?: Date; }) { super(CourseCreatedDomainEvent.EVENT_NAME, id, eventId, occurredOn); this.duration = duration; this.name = name; + this.description = description; } toPrimitive(): CreateCourseDomainEventBody { - const { name, duration } = this; + const { name, duration, description } = this; return { name, - duration + duration, + description }; } @@ -47,6 +53,7 @@ export class CourseCreatedDomainEvent extends DomainEvent { id: aggregateId, duration: body.duration, name: body.name, + description: body.description, eventId, occurredOn }); diff --git a/src/Contexts/Mooc/Courses/domain/CourseDescription.ts b/src/Contexts/Mooc/Courses/domain/CourseDescription.ts new file mode 100644 index 0000000..6815d35 --- /dev/null +++ b/src/Contexts/Mooc/Courses/domain/CourseDescription.ts @@ -0,0 +1,3 @@ +import { StringValueObject } from '../../../Shared/domain/value-object/StringValueObject'; + +export class CourseDescription extends StringValueObject {} diff --git a/src/Contexts/Mooc/Courses/domain/CourseFinder.ts b/src/Contexts/Mooc/Courses/domain/CourseFinder.ts new file mode 100644 index 0000000..ea0c1c0 --- /dev/null +++ b/src/Contexts/Mooc/Courses/domain/CourseFinder.ts @@ -0,0 +1,21 @@ +import { CourseRepository } from './CourseRepository'; +import { CourseId } from '../../Shared/domain/Courses/CourseId'; +import { Course } from './Course'; +import { Nullable } from '../../../Shared/domain/Nullable'; +import { CourseNotFound } from './CourseNotFound'; + +export class CourseFinder { + private repository: CourseRepository; + + constructor(repository: CourseRepository) { + this.repository = repository; + } + + async run(courseId : CourseId): Promise { + const course : Nullable = await this.repository.search(courseId); + if (!course) { + throw new CourseNotFound(); + } + return course; + } + } \ No newline at end of file diff --git a/src/Contexts/Mooc/Courses/domain/CourseLikedDomainEvent.ts b/src/Contexts/Mooc/Courses/domain/CourseLikedDomainEvent.ts new file mode 100644 index 0000000..7402a26 --- /dev/null +++ b/src/Contexts/Mooc/Courses/domain/CourseLikedDomainEvent.ts @@ -0,0 +1,38 @@ +import { DomainEvent } from '../../../Shared/domain/DomainEvent'; + +export class CourseLikedDomainEvent extends DomainEvent { + static readonly EVENT_NAME = 'course.liked'; + + readonly userId: string; + + constructor({ + id, + userId, + eventId, + occurredOn + }: { + id: string; + userId: string; + eventId?: string; + occurredOn?: Date; + }) { + super(CourseLikedDomainEvent.EVENT_NAME, id, eventId, occurredOn); + this.userId = userId; + } + + toPrimitive(): { userId: string } { + const { userId } = this; + return { + userId + }; + } + + static fromPrimitives(aggregateId: string, body: { userId: string }, eventId: string, occurredOn: Date): DomainEvent { + return new CourseLikedDomainEvent({ + id: aggregateId, + userId: body.userId, + eventId, + occurredOn + }); + } +} diff --git a/src/Contexts/Mooc/Courses/domain/CourseNotFound.ts b/src/Contexts/Mooc/Courses/domain/CourseNotFound.ts new file mode 100644 index 0000000..678ce6b --- /dev/null +++ b/src/Contexts/Mooc/Courses/domain/CourseNotFound.ts @@ -0,0 +1,5 @@ +export class CourseNotFound extends Error { + constructor() { + super('The course does not exists'); + } +} diff --git a/src/Contexts/Mooc/Courses/domain/CourseRenamedDomainEvent.ts b/src/Contexts/Mooc/Courses/domain/CourseRenamedDomainEvent.ts new file mode 100644 index 0000000..1c5e3a6 --- /dev/null +++ b/src/Contexts/Mooc/Courses/domain/CourseRenamedDomainEvent.ts @@ -0,0 +1,54 @@ +import { DomainEvent } from '../../../Shared/domain/DomainEvent'; + +type CourseRenamedDomainEventBody = { + readonly oldName: string; + readonly newName: string; +}; + +export class CourseRenamedDomainEvent extends DomainEvent { + static readonly EVENT_NAME = 'course.renamed'; + + readonly oldName: string; + readonly newName: string; + + constructor({ + id, + oldName, + newName, + eventId, + occurredOn + }: { + id: string; + eventId?: string; + oldName: string; + newName: string; + occurredOn?: Date; + }) { + super(CourseRenamedDomainEvent.EVENT_NAME, id, eventId, occurredOn); + this.oldName = oldName; + this.newName = newName; + } + + toPrimitive(): CourseRenamedDomainEventBody { + const { newName: oldName, oldName: newName } = this; + return { + oldName, + newName + }; + } + + static fromPrimitives( + aggregateId: string, + body: CourseRenamedDomainEventBody, + eventId: string, + occurredOn: Date + ): DomainEvent { + return new CourseRenamedDomainEvent({ + id: aggregateId, + oldName: body.oldName, + newName: body.newName, + eventId, + occurredOn + }); + } +} diff --git a/src/Contexts/Mooc/Courses/domain/CourseRepository.ts b/src/Contexts/Mooc/Courses/domain/CourseRepository.ts index 4588d16..0f91b49 100644 --- a/src/Contexts/Mooc/Courses/domain/CourseRepository.ts +++ b/src/Contexts/Mooc/Courses/domain/CourseRepository.ts @@ -6,4 +6,6 @@ export interface CourseRepository { save(course: Course): Promise; search(id: CourseId): Promise>; + + getAll(): Promise; } diff --git a/src/Contexts/Mooc/Courses/domain/UserId.ts b/src/Contexts/Mooc/Courses/domain/UserId.ts new file mode 100644 index 0000000..36c65c7 --- /dev/null +++ b/src/Contexts/Mooc/Courses/domain/UserId.ts @@ -0,0 +1,3 @@ +import { Uuid } from '../../../Shared/domain/value-object/Uuid'; + +export class UserId extends Uuid {} diff --git a/src/Contexts/Mooc/Courses/infrastructure/persistence/MongoCourseRepository.ts b/src/Contexts/Mooc/Courses/infrastructure/persistence/MongoCourseRepository.ts index d6d1340..e381b84 100644 --- a/src/Contexts/Mooc/Courses/infrastructure/persistence/MongoCourseRepository.ts +++ b/src/Contexts/Mooc/Courses/infrastructure/persistence/MongoCourseRepository.ts @@ -17,6 +17,17 @@ export class MongoCourseRepository extends MongoRepository implements Co return document ? Course.fromPrimitives({ ...document, id: id.value }) : null; } + public async getAll(): Promise { + const collection = await this.collection(); + + const documents = await collection.find().toArray(); + + const courses : Course[] = []; + documents.forEach((document: any) => document ? courses.push(Course.fromPrimitives({...document, id: document._id})) : undefined); + + return courses; + } + protected moduleName(): string { return 'courses'; } diff --git a/src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwit.ts b/src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwit.ts new file mode 100644 index 0000000..77a90fb --- /dev/null +++ b/src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwit.ts @@ -0,0 +1,17 @@ +import { CourseName } from "../../../Shared/domain/Courses/CourseName"; +import { NewCourseTwit } from "../../domain/NewCourseTwit"; +import { NewCourseTwitError } from "../../domain/NewCourseTwitError"; +import { TwitSender } from "../../domain/TwitSender"; + +export default class SendNewCourseTwit { + constructor(private tweetSender: TwitSender) {} + + async run(courseName: CourseName): Promise { + const newCourseTweet = new NewCourseTwit(courseName); + try { + await this.tweetSender.send(newCourseTweet); + } catch (error) { + throw new NewCourseTwitError(newCourseTweet); + } + } +} diff --git a/src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated.ts b/src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated.ts new file mode 100644 index 0000000..1045b05 --- /dev/null +++ b/src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated.ts @@ -0,0 +1,18 @@ +import { DomainEventSubscriber } from '../../../../Shared/domain/DomainEventSubscriber'; +import { DomainEventClass } from '../../../../Shared/domain/DomainEvent'; +import { CourseCreatedDomainEvent } from '../../domain/CourseCreatedDomainEvent'; +import SendNewCourseTwit from './SendNewCourseTwit'; +import { CourseName } from '../../../Shared/domain/Courses/CourseName'; + +export default class SendNewCourseTweetOnCourseCreated implements DomainEventSubscriber { + constructor(private sendNewCourseTwit: SendNewCourseTwit) {} + + subscribedTo(): DomainEventClass[] { + return [CourseCreatedDomainEvent]; + } + + async on(domainEvent: CourseCreatedDomainEvent): Promise { + const courseName = new CourseName(domainEvent.name); + await this.sendNewCourseTwit.run(courseName); + } +} diff --git a/src/Contexts/Mooc/Notifications/domain/CourseCreatedDomainEvent.ts b/src/Contexts/Mooc/Notifications/domain/CourseCreatedDomainEvent.ts new file mode 100644 index 0000000..2745366 --- /dev/null +++ b/src/Contexts/Mooc/Notifications/domain/CourseCreatedDomainEvent.ts @@ -0,0 +1,54 @@ +import { DomainEvent } from '../../../Shared/domain/DomainEvent'; + +type CreateCourseDomainEventBody = { + readonly duration: string; + readonly name: string; +}; + +export class CourseCreatedDomainEvent extends DomainEvent { + static readonly EVENT_NAME = 'course.created'; + + readonly duration: string; + readonly name: string; + + constructor({ + id, + name, + duration, + eventId, + occurredOn + }: { + id: string; + eventId?: string; + duration: string; + name: string; + occurredOn?: Date; + }) { + super(CourseCreatedDomainEvent.EVENT_NAME, id, eventId, occurredOn); + this.duration = duration; + this.name = name; + } + + toPrimitive(): CreateCourseDomainEventBody { + const { name, duration } = this; + return { + name, + duration + }; + } + + static fromPrimitives( + aggregateId: string, + body: CreateCourseDomainEventBody, + eventId: string, + occurredOn: Date + ): DomainEvent { + return new CourseCreatedDomainEvent({ + id: aggregateId, + duration: body.duration, + name: body.name, + eventId, + occurredOn + }); + } +} diff --git a/src/Contexts/Mooc/Notifications/domain/NewCourseTwit.ts b/src/Contexts/Mooc/Notifications/domain/NewCourseTwit.ts new file mode 100644 index 0000000..7388982 --- /dev/null +++ b/src/Contexts/Mooc/Notifications/domain/NewCourseTwit.ts @@ -0,0 +1,11 @@ +import { CourseName } from '../../Shared/domain/Courses/CourseName'; +import { Twit } from './Twit'; +import { TwitMessage } from './TwitMessage'; + +export class NewCourseTwit extends Twit { + constructor(courseName: CourseName) { + super({ + message: new TwitMessage(`New course published. ${courseName}`), + }); + } +} diff --git a/src/Contexts/Mooc/Notifications/domain/NewCourseTwitError.ts b/src/Contexts/Mooc/Notifications/domain/NewCourseTwitError.ts new file mode 100644 index 0000000..617fd3d --- /dev/null +++ b/src/Contexts/Mooc/Notifications/domain/NewCourseTwitError.ts @@ -0,0 +1,7 @@ +import { NewCourseTwit } from "./NewCourseTwit"; + +export class NewCourseTwitError extends Error { + constructor(twit: NewCourseTwit) { + super(`Error twiting new course message ${twit.message}`); + } +} diff --git a/src/Contexts/Mooc/Notifications/domain/Twit.ts b/src/Contexts/Mooc/Notifications/domain/Twit.ts new file mode 100644 index 0000000..3aa28ae --- /dev/null +++ b/src/Contexts/Mooc/Notifications/domain/Twit.ts @@ -0,0 +1,25 @@ +import { Uuid } from '../../../Shared/domain/value-object/Uuid'; +import { TwitId } from './TwitId'; +import { TwitMessage } from './TwitMessage'; + +type ConstructorParams = { + id?: TwitId; + message: TwitMessage; +}; + +export class Twit { + readonly message: TwitMessage; + readonly id: TwitId; + + constructor(params: ConstructorParams) { + this.id = params.id || new TwitId(Uuid.random().value); + this.message = params.message; + } + + equals(twit: Twit): boolean { + return ( + this.id.value === twit.id.value && + this.message.value === twit.message.value + ); + } +} diff --git a/src/Contexts/Mooc/Notifications/domain/TwitId.ts b/src/Contexts/Mooc/Notifications/domain/TwitId.ts new file mode 100644 index 0000000..603966f --- /dev/null +++ b/src/Contexts/Mooc/Notifications/domain/TwitId.ts @@ -0,0 +1,3 @@ +import { Uuid } from '../../../Shared/domain/value-object/Uuid'; + +export class TwitId extends Uuid {} diff --git a/src/Contexts/Mooc/Notifications/domain/TwitMessage.ts b/src/Contexts/Mooc/Notifications/domain/TwitMessage.ts new file mode 100644 index 0000000..2ab2469 --- /dev/null +++ b/src/Contexts/Mooc/Notifications/domain/TwitMessage.ts @@ -0,0 +1,3 @@ +import { StringValueObject } from '../../../Shared/domain/value-object/StringValueObject'; + +export class TwitMessage extends StringValueObject {} diff --git a/src/Contexts/Mooc/Notifications/domain/TwitSender.ts b/src/Contexts/Mooc/Notifications/domain/TwitSender.ts new file mode 100644 index 0000000..32a188b --- /dev/null +++ b/src/Contexts/Mooc/Notifications/domain/TwitSender.ts @@ -0,0 +1,5 @@ +import { Twit } from './Twit'; + +export interface TwitSender { + send(twit: Twit): Promise; +} diff --git a/src/Contexts/Mooc/Notifications/infrastructure/FakeTwitSender.ts b/src/Contexts/Mooc/Notifications/infrastructure/FakeTwitSender.ts new file mode 100644 index 0000000..01806b3 --- /dev/null +++ b/src/Contexts/Mooc/Notifications/infrastructure/FakeTwitSender.ts @@ -0,0 +1,8 @@ +import { Twit } from "../domain/Twit"; +import { TwitSender } from "../domain/TwitSender"; + +export default class FakeTwitSender implements TwitSender { + async send(twit: Twit): Promise { + // do nothing + } +} diff --git a/src/Contexts/Mooc/Courses/domain/CourseName.ts b/src/Contexts/Mooc/Shared/domain/Courses/CourseName.ts similarity index 65% rename from src/Contexts/Mooc/Courses/domain/CourseName.ts rename to src/Contexts/Mooc/Shared/domain/Courses/CourseName.ts index a7d92e4..64a5587 100644 --- a/src/Contexts/Mooc/Courses/domain/CourseName.ts +++ b/src/Contexts/Mooc/Shared/domain/Courses/CourseName.ts @@ -1,5 +1,5 @@ -import { StringValueObject } from '../../../Shared/domain/value-object/StringValueObject'; -import { InvalidArgumentError } from '../../../Shared/domain/value-object/InvalidArgumentError'; +import { InvalidArgumentError } from "../../../../Shared/domain/value-object/InvalidArgumentError"; +import { StringValueObject } from "../../../../Shared/domain/value-object/StringValueObject"; export class CourseName extends StringValueObject { constructor(value: string) { diff --git a/src/Contexts/Mooc/Shared/domain/Courses/application/CourseResponse.ts b/src/Contexts/Mooc/Shared/domain/Courses/application/CourseResponse.ts new file mode 100644 index 0000000..0e16581 --- /dev/null +++ b/src/Contexts/Mooc/Shared/domain/Courses/application/CourseResponse.ts @@ -0,0 +1,15 @@ +import { Course } from '../../../../Courses/domain/Course'; +export class CourseResponse { + readonly id: string; + readonly name: string; + readonly duration: string; + readonly description: string; + + constructor(course: Course) { + this.id = course.id.value; + this.name = course.name.value; + this.duration = course.duration.value; + this.description = course.description.value; + } +} + \ No newline at end of file diff --git a/src/apps/backoffice/frontend/config/dependency-injection/Courses/application.yaml b/src/apps/backoffice/frontend/config/dependency-injection/Courses/application.yaml index 40fddc3..07781b1 100644 --- a/src/apps/backoffice/frontend/config/dependency-injection/Courses/application.yaml +++ b/src/apps/backoffice/frontend/config/dependency-injection/Courses/application.yaml @@ -4,11 +4,11 @@ services: arguments: ['@Shared.ConnectionManager'] Mooc.courses.CourseCreator: - class: ../../../../../../Contexts/Mooc/Courses/application/CourseCreator + class: ../../../../../../Contexts/Mooc/Courses/application/CreateCourse/CourseCreator arguments: ['@Mooc.courses.CourseRepository', '@Shared.EventBus'] Mooc.courses.CreateCourseCommandHandler: - class: ../../../../../../Contexts/Mooc/Courses/application/CreateCourseCommandHandler + class: ../../../../../../Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler arguments: ['@Mooc.courses.CourseCreator'] tags: - { name: 'commandHandler' } diff --git a/src/apps/backoffice/frontend/controllers/CoursesPostController.ts b/src/apps/backoffice/frontend/controllers/CoursesPostController.ts index 204c996..b2258a2 100644 --- a/src/apps/backoffice/frontend/controllers/CoursesPostController.ts +++ b/src/apps/backoffice/frontend/controllers/CoursesPostController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import { CommandBus } from '../../../../Contexts/Shared/domain/CommandBus'; -import { CreateCourseCommand } from '../../../../Contexts/Mooc/Courses/application/CreateCourseCommand'; +import { CreateCourseCommand } from '../../../../Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand'; import { body, ValidationChain } from 'express-validator'; import { WebController } from './WebController'; @@ -30,7 +30,8 @@ export class CoursesPostController extends WebController { const createCourseCommand = new CreateCourseCommand({ id: req.body.id, name: req.body.name, - duration: req.body.duration + duration: req.body.duration, + description: req.body.description }); await this.commandBus.dispatch(createCourseCommand); diff --git a/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml b/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml index b0ab7c8..775b1f7 100644 --- a/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/Courses/application.yaml @@ -5,11 +5,56 @@ services: arguments: ['@Shared.ConnectionManager'] Mooc.courses.CourseCreator: - class: ../../../../../Contexts/Mooc/Courses/application/CourseCreator + class: ../../../../../Contexts/Mooc/Courses/application/CreateCourse/CourseCreator arguments: ['@Mooc.courses.CourseRepository', '@Shared.EventBus'] Mooc.courses.CreateCourseCommandHandler: - class: ../../../../../Contexts/Mooc/Courses/application/CreateCourseCommandHandler + class: ../../../../../Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler arguments: ['@Mooc.courses.CourseCreator'] tags: - { name: 'commandHandler' } + + Mooc.courses.DomainCourseFinder: + class: ../../../../../Contexts/Mooc/Courses/domain/CourseFinder + arguments: ["@Mooc.courses.CourseRepository"] + + Mooc.courses.CourseFinder: + class: ../../../../../Contexts/Mooc/Courses/application/GetCourse/CourseFinder + arguments: ["@Mooc.courses.DomainCourseFinder"] + + Mooc.courses.GetCourseQueryHandler: + class: ../../../../../Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler + arguments: ["@Mooc.courses.CourseFinder"] + tags: + - { name: 'queryHandler' } + + Mooc.courses.CourseRenamer: + class: ../../../../../Contexts/Mooc/Courses/application/RenameCourse/CourseRenamer + arguments: ["@Mooc.courses.DomainCourseFinder", "@Mooc.courses.CourseRepository", "@Shared.EventBus"] + + Mooc.courses.RenameCourseCommandHandler: + class: ../../../../../Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommandHandler + arguments: ['@Mooc.courses.CourseRenamer'] + tags: + - { name: 'commandHandler' } + + Mooc.courses.CoursesSearcher: + class: ../../../../../Contexts/Mooc/Courses/application/GetCourses/CoursesSearcher + arguments: ["@Mooc.courses.CourseRepository"] + + Mooc.courses.GetCoursesQueryHandler: + class: ../../../../../Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler + arguments: ["@Mooc.courses.CoursesSearcher"] + tags: + - { name: 'queryHandler' } + + Mooc.courses.CourseLiker: + class: ../../../../../Contexts/Mooc/Courses/application/LikeCourse/CourseLiker + arguments: ["@Mooc.courses.DomainCourseFinder", "@Mooc.courses.CourseRepository"] + + Mooc.courses.LikeCourseCommandHandler: + class: ../../../../../Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler + arguments: ['@Mooc.courses.CourseLiker'] + tags: + - { name: 'commandHandler' } + diff --git a/src/apps/mooc_backend/config/dependency-injection/Notifications/application.yaml b/src/apps/mooc_backend/config/dependency-injection/Notifications/application.yaml index ec4ae95..2503113 100644 --- a/src/apps/mooc_backend/config/dependency-injection/Notifications/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/Notifications/application.yaml @@ -10,5 +10,19 @@ services: Mooc.notifications.SendWelcomeUserEmailOnUserRegistered: class: ../../../../../Contexts/Mooc/Notifications/application/SendWelcomeUserEmail/SendWelcomeUserEmailOnUserRegistered arguments: ["@Mooc.notifications.SendWelcomeUserEmail"] + tags: + - { name: 'domainEventSubscriber' } + + Mooc.notifications.TwitSender: + class: ../../../../../Contexts/Mooc/Notifications/infrastructure/FakeTwitSender + arguments: [] + + Mooc.notifications.SendNewCourseTwit: + class: ../../../../../Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwit + arguments: ["@Mooc.notifications.TwitSender"] + + Mooc.notifications.SendNewCourseTwitOnCourseCreated: + class: ../../../../../Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated + arguments: ["@Mooc.notifications.SendNewCourseTwit"] tags: - { name: 'domainEventSubscriber' } \ No newline at end of file diff --git a/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml b/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml index 05e0acd..f728559 100644 --- a/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml +++ b/src/apps/mooc_backend/config/dependency-injection/apps/application.yaml @@ -4,6 +4,10 @@ services: class: ../../../controllers/CoursePutController arguments: ["@Shared.CommandBus"] + Apps.mooc.controllers.CourseRenameController: + class: ../../../controllers/CourseRenameController + arguments: ["@Shared.CommandBus"] + Apps.mooc.controllers.StatusGetController: class: ../../../controllers/StatusGetController arguments: [] @@ -11,3 +15,15 @@ services: Apps.mooc.controllers.CoursesCounterGetController: class: ../../../controllers/CoursesCounterGetController arguments: ["@Shared.QueryBus"] + + Apps.mooc.controllers.CourseGetController: + class: ../../../controllers/CourseGetController + arguments: ["@Shared.QueryBus"] + + Apps.mooc.controllers.CoursesGetController: + class: ../../../controllers/CoursesGetController + arguments: ["@Shared.QueryBus"] + + Apps.mooc.controllers.CourseLikePostController: + class: ../../../controllers/CourseLikePostController + arguments: ["@Shared.CommandBus"] \ No newline at end of file diff --git a/src/apps/mooc_backend/controllers/CourseGetController.ts b/src/apps/mooc_backend/controllers/CourseGetController.ts new file mode 100644 index 0000000..8a7f59a --- /dev/null +++ b/src/apps/mooc_backend/controllers/CourseGetController.ts @@ -0,0 +1,26 @@ +import { Controller } from './Controller'; +import { Request, Response } from 'express'; +import httpStatus = require('http-status'); + +import { QueryBus } from '../../../Contexts/Shared/domain/QueryBus'; +import { GetCourseQuery } from '../../../Contexts/Mooc/Courses/application/GetCourse/GetCourseQuery'; +import { CourseResponse } from '../../../Contexts/Mooc/Shared/domain/Courses/application/CourseResponse'; +import { CourseNotFound } from '../../../Contexts/Mooc/Courses/domain/CourseNotFound'; + +export class CourseGetController implements Controller { + constructor(private queryBus: QueryBus) {} + async run(req: Request, res: Response): Promise { + try { + const id: string = req.params.id; + const query = new GetCourseQuery({id}); + const course = await this.queryBus.ask(query); + res.status(httpStatus.OK).send(course); + } catch (e) { + if (e instanceof CourseNotFound) { + res.status(httpStatus.NOT_FOUND).send(); + } else { + res.status(httpStatus.INTERNAL_SERVER_ERROR).send(); + } + } + } +} diff --git a/src/apps/mooc_backend/controllers/CourseLikePostController.ts b/src/apps/mooc_backend/controllers/CourseLikePostController.ts new file mode 100644 index 0000000..651f3e7 --- /dev/null +++ b/src/apps/mooc_backend/controllers/CourseLikePostController.ts @@ -0,0 +1,29 @@ +import { Controller } from './Controller'; +import { Request, Response } from 'express'; +import httpStatus from 'http-status'; +import { LikeCourseCommand } from '../../../Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommand'; +import { CourseNotFound } from '../../../Contexts/Mooc/Courses/domain/CourseNotFound'; +import { CommandBus } from '../../../Contexts/Shared/domain/CommandBus'; + +export class CourseLikePostController implements Controller { + constructor(private commandBus: CommandBus) {} + + async run(req: Request, res: Response): Promise { + const id = req.params.id; + const { userId } = req.body; + + const command = new LikeCourseCommand({ id, userId }); + + try { + await this.commandBus.dispatch(command); + } catch (error) { + if (error instanceof CourseNotFound) { + res.status(httpStatus.NOT_FOUND).send(error.message); + } else { + res.status(httpStatus.INTERNAL_SERVER_ERROR).json(error); + } + } + + res.status(httpStatus.NO_CONTENT).send(); + } +} diff --git a/src/apps/mooc_backend/controllers/CoursePutController.ts b/src/apps/mooc_backend/controllers/CoursePutController.ts index 1863ab7..f5a18ac 100644 --- a/src/apps/mooc_backend/controllers/CoursePutController.ts +++ b/src/apps/mooc_backend/controllers/CoursePutController.ts @@ -3,7 +3,7 @@ import httpStatus from 'http-status'; import { Controller } from './Controller'; import { CourseAlreadyExists } from '../../../Contexts/Mooc/Courses/domain/CourseAlreadyExists'; import { CommandBus } from '../../../Contexts/Shared/domain/CommandBus'; -import { CreateCourseCommand } from '../../../Contexts/Mooc/Courses/application/CreateCourseCommand'; +import { CreateCourseCommand } from '../../../Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand'; export class CoursePutController implements Controller { constructor(private commandBus: CommandBus) {} @@ -12,11 +12,13 @@ export class CoursePutController implements Controller { const id: string = req.params.id; const name: string = req.body.name; const duration: string = req.body.duration; - const createCourseCommand = new CreateCourseCommand({ id, name, duration }); + const description: string = req.body.description; + const createCourseCommand = new CreateCourseCommand({ id, name, duration, description }); try { await this.commandBus.dispatch(createCourseCommand); } catch (error) { + console.log(error); if (error instanceof CourseAlreadyExists) { res.status(httpStatus.BAD_REQUEST).send(error.message); } else { diff --git a/src/apps/mooc_backend/controllers/CourseRenameController.ts b/src/apps/mooc_backend/controllers/CourseRenameController.ts new file mode 100644 index 0000000..7e01d89 --- /dev/null +++ b/src/apps/mooc_backend/controllers/CourseRenameController.ts @@ -0,0 +1,28 @@ +import { Request, Response } from 'express'; +import { Controller } from './Controller'; +import { CommandBus } from '../../../Contexts/Shared/domain/CommandBus'; +import { RenameCourseCommand } from '../../../Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand'; +import { CourseNotFound } from '../../../Contexts/Mooc/Courses/domain/CourseNotFound'; +import httpStatus from 'http-status'; + +export class CourseRenameController implements Controller { + constructor(private commandBus: CommandBus) {} + + async run(req: Request, res: Response) { + const id: string = req.params.id; + const name: string = req.body.name; + const renameCourseCommand = new RenameCourseCommand({ id, name }); + + try { + await this.commandBus.dispatch(renameCourseCommand); + } catch (error) { + if (error instanceof CourseNotFound) { + res.status(httpStatus.NOT_FOUND).send(error.message); + } else { + res.status(httpStatus.INTERNAL_SERVER_ERROR).json(error); + } + } + + res.status(httpStatus.NO_CONTENT).send(); + } + } \ No newline at end of file diff --git a/src/apps/mooc_backend/controllers/CoursesGetController.ts b/src/apps/mooc_backend/controllers/CoursesGetController.ts new file mode 100644 index 0000000..c6d9725 --- /dev/null +++ b/src/apps/mooc_backend/controllers/CoursesGetController.ts @@ -0,0 +1,19 @@ +import { CoursesResponse } from './../../../Contexts/Mooc/Courses/application/GetCourses/CoursesResponse'; +import { Controller } from './Controller'; +import { Request, Response } from 'express'; +import httpStatus = require('http-status'); +import { QueryBus } from '../../../Contexts/Shared/domain/QueryBus'; +import { GetCoursesQuery } from '../../../Contexts/Mooc/Courses/application/GetCourses/GetCoursesQuery'; + +export class CoursesGetController implements Controller { + constructor(private queryBus: QueryBus) {} + async run(req: Request, res: Response): Promise { + try { + const query = new GetCoursesQuery(); + const course = await this.queryBus.ask(query); + res.status(httpStatus.OK).send(course); + } catch (e) { + res.status(httpStatus.INTERNAL_SERVER_ERROR).send(); + } + } +} diff --git a/src/apps/mooc_backend/routes/courses.route.ts b/src/apps/mooc_backend/routes/courses.route.ts index c49fce9..a912c38 100644 --- a/src/apps/mooc_backend/routes/courses.route.ts +++ b/src/apps/mooc_backend/routes/courses.route.ts @@ -5,6 +5,18 @@ export const register = (router: Router) => { const coursePutController = container.get('Apps.mooc.controllers.CoursePutController'); router.put('/courses/:id', (req: Request, res: Response) => coursePutController.run(req, res)); + const courseRenameController = container.get('Apps.mooc.controllers.CourseRenameController'); + router.put('/courses/:id/rename', (req: Request, res: Response) => courseRenameController.run(req, res)); + + const courseGetController = container.get('Apps.mooc.controllers.CourseGetController'); + router.get('/courses/:id', (req: Request, res: Response) => courseGetController.run(req, res)); + + const coursesGetController = container.get('Apps.mooc.controllers.CoursesGetController'); + router.get('/courses', (req: Request, res: Response) => coursesGetController.run(req, res)); + + const courseLikePostController = container.get('Apps.mooc.controllers.CourseLikePostController'); + router.post('/courses/:id/like', (req: Request, res: Response) => courseLikePostController.run(req, res)); + const coursesCounterGetController = container.get('Apps.mooc.controllers.CoursesCounterGetController'); router.get('/courses-counter', (req: Request, res: Response) => coursesCounterGetController.run(req, res)); }; diff --git a/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts b/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts index f7e8205..29ad0ec 100644 --- a/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts +++ b/tests/Contexts/Mooc/Courses/__mocks__/CourseRepositoryMock.ts @@ -6,6 +6,9 @@ import { Nullable } from '../../../../../src/Contexts/Shared/domain/Nullable'; export class CourseRepositoryMock implements CourseRepository { private mockSave = jest.fn(); private mockSearch = jest.fn(); + private mockGetAll = jest.fn(); + private course: Nullable = null; + private courses: Course[] = []; async save(course: Course): Promise { this.mockSave(course); @@ -18,8 +21,17 @@ export class CourseRepositoryMock implements CourseRepository { expect(lastSavedCourse.toPrimitives()).toEqual(expected.toPrimitives()); } + assertLastSavedCourseEventIs(expected: Course): void { + const mock = this.mockSave.mock; + const lastSavedCourse = mock.calls[mock.calls.length - 1][0] as Course; + expect(lastSavedCourse).toBeInstanceOf(Course); + expect(lastSavedCourse.toPrimitives()).toEqual(expected.toPrimitives()); + expect(lastSavedCourse.pullDomainEvents()).toEqual(expected.pullDomainEvents()); + } + async search(id: CourseId): Promise> { - return this.mockSearch(id); + this.mockSearch(id); + return this.course; } whenSearchThenReturn(value: Nullable): void { @@ -29,4 +41,26 @@ export class CourseRepositoryMock implements CourseRepository { assertLastSearchedCourseIs(expected: CourseId): void { expect(this.mockSearch).toHaveBeenCalledWith(expected); } + + assertSearch(expected: CourseId) { + expect(this.mockSearch).toHaveBeenCalled(); + expect(this.mockSearch).toHaveBeenCalledWith(expected); + } + + returnOnSearch(course: Course) { + this.course = course; + } + + async getAll(): Promise { + this.mockGetAll(); + return this.courses; + } + + returnOnGetAll(courses: Course[]) { + this.courses = courses; + } + + assertGetAll() { + expect(this.mockGetAll).toHaveBeenCalled(); + } } diff --git a/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts b/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts index a406009..11997e1 100644 --- a/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts +++ b/tests/Contexts/Mooc/Courses/application/CourseCreator.test.ts @@ -1,9 +1,9 @@ -import { CourseCreator } from '../../../../../src/Contexts/Mooc/Courses/application/CourseCreator'; +import { CourseCreator } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourse/CourseCreator'; import { CourseMother } from '../domain/CourseMother'; import { CourseRepositoryMock } from '../__mocks__/CourseRepositoryMock'; import { CreateCourseCommandMother } from './CreateCourseCommandMother'; import EventBusMock from '../__mocks__/EventBusMock'; -import { CreateCourseCommandHandler } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourseCommandHandler'; +import { CreateCourseCommandHandler } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommandHandler'; let repository: CourseRepositoryMock; let handler: CreateCourseCommandHandler; @@ -20,6 +20,6 @@ it('should create a valid course', async () => { const command = CreateCourseCommandMother.random(); await handler.handle(command); - const course = CourseMother.fromCommand(command); + const course = CourseMother.fromCreateCommand(command); repository.assertLastSavedCourseIs(course); }); diff --git a/tests/Contexts/Mooc/Courses/application/CourseRename/CourseRenamer.test.ts b/tests/Contexts/Mooc/Courses/application/CourseRename/CourseRenamer.test.ts new file mode 100644 index 0000000..1d6101c --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/CourseRename/CourseRenamer.test.ts @@ -0,0 +1,40 @@ + +import { CourseRenamer } from '../../../../../../src/Contexts/Mooc/Courses/application/RenameCourse/CourseRenamer'; +import { CourseFinder } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseFinder'; +import { CourseNameMother } from '../../domain/CourseNameMother'; +import { CourseMother } from '../../domain/CourseMother'; +import { CourseRepositoryMock } from '../../__mocks__/CourseRepositoryMock'; +import EventBusMock from '../../__mocks__/EventBusMock'; +import { CourseIdMother } from '../../../Shared/domain/Courses/CourseIdMother'; +import { CourseNotFound } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseNotFound'; + +let repository: CourseRepositoryMock; +let finder: CourseFinder; +let renamer: CourseRenamer; + +const eventBus = new EventBusMock(); + +beforeEach(() => { + repository = new CourseRepositoryMock(); + finder = new CourseFinder(repository); + renamer = new CourseRenamer(finder, repository, eventBus); +}); + +it('should rename a valid course', async () => { + const courseBefore = CourseMother.random(); + const { id, duration, description } = courseBefore; + const newName = CourseNameMother.random(); + const renamedCourse = CourseMother.create(id, newName, duration, description); + + repository.returnOnSearch(courseBefore); + + await renamer.run({ courseId: id, courseName: newName}) + + repository.assertSearch(id); + repository.assertLastSavedCourseIs(renamedCourse); +}); + +it('should get a not found exception', async () => { + const params = { courseId: CourseIdMother.random(), courseName: CourseNameMother.random()}; + await expect(renamer.run(params)).rejects.toBeInstanceOf(CourseNotFound); +}); diff --git a/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandHandler.test.ts b/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandHandler.test.ts new file mode 100644 index 0000000..5628278 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandHandler.test.ts @@ -0,0 +1,58 @@ +import { CourseFinder } from './../../../../../../src/Contexts/Mooc/Courses/domain/CourseFinder'; +import { RenameCourseCommandHandler } from '../../../../../../src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommandHandler'; +import { CourseRepositoryMock } from '../../__mocks__/CourseRepositoryMock'; +import { CourseRenamer } from '../../../../../../src/Contexts/Mooc/Courses/application/RenameCourse/CourseRenamer'; +import EventBusMock from '../../__mocks__/EventBusMock'; +import { RenameCourseCommandMother } from './RenameCourseCommandMother'; +import { CourseMother } from '../../domain/CourseMother'; +import { CourseNameMother } from '../../domain/CourseNameMother'; +import { CourseDurationMother } from '../../domain/CourseDurationMother'; +import { CourseIdMother } from '../../../Shared/domain/Courses/CourseIdMother'; +import { CourseNotFound } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseNotFound'; +import { RenameCourseCommand } from '../../../../../../src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand'; +import { CourseDuration } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseDuration'; +import { CourseName } from '../../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseName'; +import { CourseId } from '../../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseId'; +import { CourseDescriptionMother } from '../../domain/CourseDescriptionMother'; +import { CourseDescription } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseDescription'; + +let repository: CourseRepositoryMock; +let handler: RenameCourseCommandHandler; + +const eventBus = new EventBusMock(); + +beforeEach(() => { + repository = new CourseRepositoryMock(); + const finder = new CourseFinder(repository); + const renamer = new CourseRenamer(finder, repository, eventBus); + handler = new RenameCourseCommandHandler(renamer); +}); + +it('should rename a course', async () => { + const command = RenameCourseCommandMother.random(); + const id = CourseIdMother.create(command.id); + const name = CourseNameMother.create(command.name); + const oldName = CourseNameMother.random(); + const duration = CourseDurationMother.random(); + const description = CourseDescriptionMother.random(); + const courseBefore = CourseMother.create(id, oldName, duration, description); + repository.returnOnSearch(courseBefore); + + await whenRenameCourseIsInvoked(command); + thenTheCourseShouldBeRenamed(id, name, duration, description); +}); + +it('should get an exception', async () => { + const command = RenameCourseCommandMother.random(); + await expect(handler.handle(command)).rejects.toBeInstanceOf(CourseNotFound); +}); + +async function whenRenameCourseIsInvoked(command: RenameCourseCommand) : Promise { + await handler.handle(command); +} + +function thenTheCourseShouldBeRenamed(id: CourseId, name: CourseName, duration: CourseDuration, description: CourseDescription) : void { + const renamedCourse = CourseMother.create(id, name, duration, description); + repository.assertSearch(id); + repository.assertLastSavedCourseIs(renamedCourse); +} \ No newline at end of file diff --git a/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandMother.ts b/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandMother.ts new file mode 100644 index 0000000..0e6c0d9 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/CourseRename/RenameCourseCommandMother.ts @@ -0,0 +1,14 @@ +import { RenameCourseCommand } from '../../../../../../src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand'; +import { CourseIdMother } from '../../../Shared/domain/Courses/CourseIdMother'; +import { CourseNameMother } from '../../domain/CourseNameMother'; + +export class RenameCourseCommandMother { + static create(id: string, name: string): RenameCourseCommand { + return new RenameCourseCommand({ id, name}); + } + + static random(): RenameCourseCommand { + return this.create(CourseIdMother.random().value, CourseNameMother.random().value); + } + } + \ No newline at end of file diff --git a/tests/Contexts/Mooc/Courses/application/CreateCourseCommandMother.ts b/tests/Contexts/Mooc/Courses/application/CreateCourseCommandMother.ts index 02b796d..44e9008 100644 --- a/tests/Contexts/Mooc/Courses/application/CreateCourseCommandMother.ts +++ b/tests/Contexts/Mooc/Courses/application/CreateCourseCommandMother.ts @@ -1,14 +1,15 @@ import { CourseDurationMother } from '../domain/CourseDurationMother'; import { CourseIdMother } from '../../Shared/domain/Courses/CourseIdMother'; import { CourseNameMother } from '../domain/CourseNameMother'; -import { CreateCourseCommand } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourseCommand'; +import { CreateCourseCommand } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand'; +import { CourseDescriptionMother } from '../domain/CourseDescriptionMother'; export class CreateCourseCommandMother { - static create(id: string, name: string, duration: string): CreateCourseCommand { - return new CreateCourseCommand({ id, name, duration }); + static create(id: string, name: string, duration: string, description: string): CreateCourseCommand { + return new CreateCourseCommand({ id, name, duration, description }); } static random(): CreateCourseCommand { - return this.create(CourseIdMother.random().value, CourseNameMother.random().value, CourseDurationMother.random().value); + return this.create(CourseIdMother.random().value, CourseNameMother.random().value, CourseDurationMother.random().value, CourseDescriptionMother.random().value); } } diff --git a/tests/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.test.ts b/tests/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.test.ts new file mode 100644 index 0000000..8b2a4ab --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/GetCourse/CourseFinder.test.ts @@ -0,0 +1,36 @@ +import { CourseFinder } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder'; +import { CourseFinder as DomainCourseFinder } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseFinder'; +import { CourseRepositoryMock } from '../../__mocks__/CourseRepositoryMock'; +import { CourseMother } from '../../domain/CourseMother'; +import { ParamsMother } from './ParamsMother'; +import { CourseIdMother } from '../../../Shared/domain/Courses/CourseIdMother'; +import { CourseNotFound } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseNotFound'; +import { CourseResponse } from '../../../../../../src/Contexts/Mooc/Shared/domain/Courses/application/CourseResponse'; + +let repository: CourseRepositoryMock; +let finder: CourseFinder; +let domainFinder: DomainCourseFinder; + +beforeEach(() => { + repository = new CourseRepositoryMock(); + domainFinder = new DomainCourseFinder(repository); + finder = new CourseFinder(domainFinder); +}); + +it('should get a course', async () => { + const course = CourseMother.random(); + const id = course.id; + repository.returnOnSearch(course); + const params = ParamsMother.create(id); + const response = await finder.run(params); + + repository.assertSearch(id); + const expected = new CourseResponse(course); + expect(expected).toEqual(response); +}); + +it('should throw an exception when courses counter does not exists', async () => { + const id = CourseIdMother.random() + const params = ParamsMother.create(id); + await expect(finder.run(params)).rejects.toBeInstanceOf(CourseNotFound); +}); diff --git a/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.test.ts b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.test.ts new file mode 100644 index 0000000..1de4b02 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler.test.ts @@ -0,0 +1,45 @@ +import { CourseFinder } from './../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder'; +import { CourseFinder as DomainCourseFinder } from './../../../../../../src/Contexts/Mooc/Courses/domain/CourseFinder'; +import { CourseRepositoryMock } from '../../__mocks__/CourseRepositoryMock'; +import { CourseMother } from '../../domain/CourseMother'; +import { GetCourseQuery } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQuery'; +import { CourseIdMother } from '../../../Shared/domain/Courses/CourseIdMother'; +import { GetCourseQueryHandler } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryHandler'; +import { CourseNotFound } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseNotFound'; +import { CourseResponse } from '../../../../../../src/Contexts/Mooc/Shared/domain/Courses/application/CourseResponse'; + +describe('GetCourse QueryHandler', () => { + let repository: CourseRepositoryMock; + let domainCourseFinder: DomainCourseFinder; + + beforeEach(() => { + repository = new CourseRepositoryMock(); + domainCourseFinder = new DomainCourseFinder(repository); + }); + + + it('should find an existing courses counter', async () => { + const course = CourseMother.random(); + const id = course.id; + repository.returnOnSearch(course); + + const handler = new GetCourseQueryHandler(new CourseFinder(domainCourseFinder)); + + const query = new GetCourseQuery({ id: id.value }); + const response = await handler.handle(query); + + repository.assertSearch(id); + + const expected = new CourseResponse(course); + expect(expected).toEqual(response); + }); + + it('should throw an exception when courses counter does not exists', async () => { + const handler = new GetCourseQueryHandler(new CourseFinder(domainCourseFinder)); + + const id = CourseIdMother.random(); + const query = new GetCourseQuery({id : id.value}); + + await expect(handler.handle(query)).rejects.toBeInstanceOf(CourseNotFound); + }); +}); diff --git a/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryMother.ts b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryMother.ts new file mode 100644 index 0000000..00515a7 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseQueryMother.ts @@ -0,0 +1,14 @@ +import { GetCourseQuery } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseQuery'; +import { GetCourseRequestMother } from './GetCourseRequestMother'; + +export class GetCourseQueryMother { + static random(): GetCourseQuery { + const getCourseRequest = GetCourseRequestMother.random() + return new GetCourseQuery(getCourseRequest); + } + + static create(id: string): GetCourseQuery { + const getCourseRequest = GetCourseRequestMother.create(id) + return new GetCourseQuery(getCourseRequest) + } +} diff --git a/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequestMother.ts b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequestMother.ts new file mode 100644 index 0000000..09dbf90 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequestMother.ts @@ -0,0 +1,14 @@ +import { GetCourseRequest } from "../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseRequest"; +import { UuidMother } from '../../../../Shared/domain/UuidMother'; + +export class GetCourseRequestMother { + static random(): GetCourseRequest { + const id = UuidMother.random() + return { id }; + } + + static create(id: string): GetCourseRequest { + return { id }; + } + } + \ No newline at end of file diff --git a/tests/Contexts/Mooc/Courses/application/GetCourse/ParamsMother.ts b/tests/Contexts/Mooc/Courses/application/GetCourse/ParamsMother.ts new file mode 100644 index 0000000..43a2db1 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/GetCourse/ParamsMother.ts @@ -0,0 +1,8 @@ +import { Params } from './../../../../../../src/Contexts/Mooc/Courses/application/GetCourse/CourseFinder'; +import { CourseId } from '../../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseId'; + +export class ParamsMother { + static create(id: CourseId): Params { + return { courseId: id }; + } + } \ No newline at end of file diff --git a/tests/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler.test.ts b/tests/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler.test.ts new file mode 100644 index 0000000..b8eb104 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler.test.ts @@ -0,0 +1,46 @@ +import { CourseRepositoryMock } from '../../__mocks__/CourseRepositoryMock'; +import { CourseMother } from '../../domain/CourseMother'; +import { CoursesSearcher } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourses/CoursesSearcher'; +import { GetCoursesQueryHandler } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQueryHandler'; +import { GetCoursesQuery } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourses/GetCoursesQuery'; +import { CoursesResponse } from '../../../../../../src/Contexts/Mooc/Courses/application/GetCourses/CoursesResponse'; + +describe('GetCourse QueryHandler', () => { + let repository: CourseRepositoryMock; + let coursesSerarcher: CoursesSearcher; + + beforeEach(() => { + repository = new CourseRepositoryMock(); + coursesSerarcher = new CoursesSearcher(repository); + }); + + + it('should search all courses', async () => { + const firstCourse = CourseMother.random(); + const secondCourse = CourseMother.random(); + const courses = [firstCourse, secondCourse]; + repository.returnOnGetAll(courses); + + const handler = new GetCoursesQueryHandler(new CoursesSearcher(repository)); + + const query = new GetCoursesQuery(); + const response = await handler.handle(query); + + repository.assertGetAll(); + + const expected = new CoursesResponse(courses); + expect(expected).toEqual(response); + }); + + it('should get no one course', async () => { + const handler = new GetCoursesQueryHandler(new CoursesSearcher(repository)); + + const query = new GetCoursesQuery(); + const response = await handler.handle(query); + + repository.assertGetAll(); + + const expected = new CoursesResponse([]); + expect(expected).toEqual(response); + }); +}); diff --git a/tests/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler.test.ts b/tests/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler.test.ts new file mode 100644 index 0000000..a22598e --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler.test.ts @@ -0,0 +1,48 @@ +import EventBusMock from '../../__mocks__/EventBusMock'; +import { LikeCourseCommandHandler } from '../../../../../../src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandHandler'; +import { CourseRepositoryMock } from '../../__mocks__/CourseRepositoryMock'; +import { CourseFinder } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseFinder'; +import { CourseLiker } from '../../../../../../src/Contexts/Mooc/Courses/application/LikeCourse/CourseLiker'; +import { LikeCourseCommandMother } from './LikeCourseCommandMother'; +import { CourseMother } from '../../domain/CourseMother'; +import { UserIdMother } from './UserIdMother'; +import { CourseLikedDomainEvent } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseLikedDomainEvent'; +import { Course } from '../../../../../../src/Contexts/Mooc/Courses/domain/Course'; + +let handler: LikeCourseCommandHandler; +let repository: CourseRepositoryMock; + +describe('Like Course command handler tests', () => { + beforeEach(() => { + repository = new CourseRepositoryMock(); + const finder = new CourseFinder(repository); + const liker = new CourseLiker(finder, repository); + handler = new LikeCourseCommandHandler(liker); + }); + it('Should like a video', async () => { + //Given + const course = CourseMother.random(); + const userId = UserIdMother.random(); + repository.returnOnSearch(course); + const command = LikeCourseCommandMother.create(course.id.value, userId.value); + + // when + await handler.handle(command); + + // then + thenCourseHasBeenLiked(course, userId.value); + }); +}); + +function thenCourseHasBeenLiked(course: Course, userId: string): void { + const likedCourseEvent = aLikedCourseDomainEvent(course.id.value, userId); + course.record(likedCourseEvent); + repository.assertLastSavedCourseEventIs(course); +} + +function aLikedCourseDomainEvent(id: string, userId: string): CourseLikedDomainEvent { + return new CourseLikedDomainEvent({ + id, + userId + }); +} diff --git a/tests/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandMother.ts b/tests/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandMother.ts new file mode 100644 index 0000000..eac6341 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommandMother.ts @@ -0,0 +1,13 @@ +import { LikeCourseCommand } from '../../../../../../src/Contexts/Mooc/Courses/application/LikeCourse/LikeCourseCommand'; +import { CourseIdMother } from '../../../Shared/domain/Courses/CourseIdMother'; +import { UserIdMother } from './UserIdMother'; + +export class LikeCourseCommandMother { + static create(id: string, userId: string): LikeCourseCommand { + return new LikeCourseCommand({ id, userId }); + } + + static random(): LikeCourseCommand { + return this.create(CourseIdMother.random().value, UserIdMother.random().value); + } +} diff --git a/tests/Contexts/Mooc/Courses/application/LikeCourse/UserIdMother.ts b/tests/Contexts/Mooc/Courses/application/LikeCourse/UserIdMother.ts new file mode 100644 index 0000000..fa6e2ad --- /dev/null +++ b/tests/Contexts/Mooc/Courses/application/LikeCourse/UserIdMother.ts @@ -0,0 +1,12 @@ +import { UserId } from '../../../../../../src/Contexts/Mooc/Courses/domain/UserId'; +import { UuidMother } from '../../../../Shared/domain/UuidMother'; + +export class UserIdMother { + static create(value: string): UserId { + return new UserId(value); + } + + static random(): UserId { + return this.create(UuidMother.random()); + } +} diff --git a/tests/Contexts/Mooc/Courses/domain/Course.test.ts b/tests/Contexts/Mooc/Courses/domain/Course.test.ts index cdd67c1..0448413 100644 --- a/tests/Contexts/Mooc/Courses/domain/Course.test.ts +++ b/tests/Contexts/Mooc/Courses/domain/Course.test.ts @@ -4,25 +4,39 @@ import { Course } from '../../../../../src/Contexts/Mooc/Courses/domain/Course'; import { CourseIdMother } from '../../Shared/domain/Courses/CourseIdMother'; import { CourseNameMother } from './CourseNameMother'; import { CourseDurationMother } from './CourseDurationMother'; +import { CourseDescriptionMother } from './CourseDescriptionMother'; describe('Course', () => { it('should return a new course instance', () => { const command = CreateCourseCommandMother.random(); - const course = CourseMother.fromCommand(command); + const course = CourseMother.fromCreateCommand(command); expect(course.id.value).toBe(command.id); expect(course.name.value).toBe(command.name); expect(course.duration.value).toBe(command.duration); + expect(course.description.value).toBe(command.description); }); it('should record a CourseCreatedDomainEvent after its creation', () => { - const course = Course.create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random()); + const course = Course.create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random(), CourseDescriptionMother.random()); const events = course.pullDomainEvents(); expect(events).toHaveLength(1); expect(events[0].eventName).toBe('course.created'); }); + + it('should record a CourseRenamedDomainEvent after rename', () => { + const command = CreateCourseCommandMother.random(); + const course = CourseMother.fromCreateCommand(command); + const newName = CourseNameMother.random(); + course.rename(newName); + + const events = course.pullDomainEvents(); + + expect(events).toHaveLength(1); + expect(events[0].eventName).toBe('course.renamed'); + }); }); diff --git a/tests/Contexts/Mooc/Courses/domain/CourseDescriptionMother.ts b/tests/Contexts/Mooc/Courses/domain/CourseDescriptionMother.ts new file mode 100644 index 0000000..f5b5f31 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/domain/CourseDescriptionMother.ts @@ -0,0 +1,12 @@ +import { WordMother } from '../../../Shared/domain/WordMother'; +import { CourseDescription } from '../../../../../src/Contexts/Mooc/Courses/domain/CourseDescription'; + +export class CourseDescriptionMother { + static create(value: string): CourseDescription { + return new CourseDescription(value); + } + + static random(): CourseDescription { + return this.create(WordMother.random()); + } +} diff --git a/tests/Contexts/Mooc/Courses/domain/CourseFinder.test.ts b/tests/Contexts/Mooc/Courses/domain/CourseFinder.test.ts new file mode 100644 index 0000000..ec10945 --- /dev/null +++ b/tests/Contexts/Mooc/Courses/domain/CourseFinder.test.ts @@ -0,0 +1,31 @@ +import { CourseFinder } from './../../../../../src/Contexts/Mooc/Courses/domain/CourseFinder'; +import { CourseRepositoryMock } from '../__mocks__/CourseRepositoryMock'; +import { CourseMother } from './CourseMother'; +import { GetCourseResponse } from '../../../../../src/Contexts/Mooc/Courses/application/GetCourse/GetCourseResponse'; +import { CourseIdMother } from '../../Shared/domain/Courses/CourseIdMother'; +import { CourseNotFound } from '../../../../../src/Contexts/Mooc/Courses/domain/CourseNotFound'; + + +let repository: CourseRepositoryMock; +let finder: CourseFinder; + + +beforeEach(() => { + repository = new CourseRepositoryMock(); + finder = new CourseFinder(repository); +}); + +it('should get a course', async () => { + const course = CourseMother.random(); + const id = course.id; + repository.returnOnSearch(course); + const response = await finder.run(id); + + repository.assertSearch(id); + expect(course).toEqual(response); +}); + +it('should throw an exception when courses counter does not exists', async () => { + const id = CourseIdMother.random() + await expect(finder.run(id)).rejects.toBeInstanceOf(CourseNotFound); +}); diff --git a/tests/Contexts/Mooc/Courses/domain/CourseMother.ts b/tests/Contexts/Mooc/Courses/domain/CourseMother.ts index 4c388be..ad2cd08 100644 --- a/tests/Contexts/Mooc/Courses/domain/CourseMother.ts +++ b/tests/Contexts/Mooc/Courses/domain/CourseMother.ts @@ -1,27 +1,39 @@ import { CourseId } from '../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseId'; -import { CourseName } from '../../../../../src/Contexts/Mooc/Courses/domain/CourseName'; +import { CourseName } from '../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseName'; import { CourseDuration } from '../../../../../src/Contexts/Mooc/Courses/domain/CourseDuration'; import { Course } from '../../../../../src/Contexts/Mooc/Courses/domain/Course'; -import { CreateCourseRequest } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourseRequest'; import { CourseIdMother } from '../../Shared/domain/Courses/CourseIdMother'; import { CourseNameMother } from './CourseNameMother'; import { CourseDurationMother } from './CourseDurationMother'; -import { CreateCourseCommand } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourseCommand'; +import { CreateCourseCommand } from '../../../../../src/Contexts/Mooc/Courses/application/CreateCourse/CreateCourseCommand'; +import { RenameCourseCommand } from '../../../../../src/Contexts/Mooc/Courses/application/RenameCourse/RenameCourseCommand'; +import { CourseDescription } from '../../../../../src/Contexts/Mooc/Courses/domain/CourseDescription'; +import { CourseDescriptionMother } from './CourseDescriptionMother'; export class CourseMother { - static create(id: CourseId, name: CourseName, duration: CourseDuration): Course { - return new Course(id, name, duration); + static create(id: CourseId, name: CourseName, duration: CourseDuration, description: CourseDescription): Course { + return new Course(id, name, duration, description); } - static fromCommand(command: CreateCourseCommand): Course { + static fromCreateCommand(command: CreateCourseCommand): Course { return this.create( CourseIdMother.create(command.id), CourseNameMother.create(command.name), - CourseDurationMother.create(command.duration) + CourseDurationMother.create(command.duration), + CourseDescriptionMother.create(command.description) + ); + } + + static fromRenameCommand(command: RenameCourseCommand): Course { + return this.create( + CourseIdMother.create(command.id), + CourseNameMother.create(command.name), + CourseDurationMother.random(), + CourseDescriptionMother.random() ); } static random(): Course { - return this.create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random()); + return this.create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random(), CourseDescriptionMother.random()); } } diff --git a/tests/Contexts/Mooc/Courses/domain/CourseNameMother.ts b/tests/Contexts/Mooc/Courses/domain/CourseNameMother.ts index 99f1c11..6f63df6 100644 --- a/tests/Contexts/Mooc/Courses/domain/CourseNameMother.ts +++ b/tests/Contexts/Mooc/Courses/domain/CourseNameMother.ts @@ -1,4 +1,4 @@ -import { CourseName } from '../../../../../src/Contexts/Mooc/Courses/domain/CourseName'; +import { CourseName } from '../../../../../src/Contexts/Mooc/Shared/domain/Courses/CourseName'; import { WordMother } from '../../../Shared/domain/WordMother'; export class CourseNameMother { diff --git a/tests/Contexts/Mooc/Courses/infrastructure/persistence/CourseRepository.test.ts b/tests/Contexts/Mooc/Courses/infrastructure/persistence/CourseRepository.test.ts index cd42f72..69026c4 100644 --- a/tests/Contexts/Mooc/Courses/infrastructure/persistence/CourseRepository.test.ts +++ b/tests/Contexts/Mooc/Courses/infrastructure/persistence/CourseRepository.test.ts @@ -1,4 +1,5 @@ import container from '../../../../../../src/apps/mooc_backend/config/dependency-injection'; +import { Course } from '../../../../../../src/Contexts/Mooc/Courses/domain/Course'; import { CourseRepository } from '../../../../../../src/Contexts/Mooc/Courses/domain/CourseRepository'; import { EnvironmentArranger } from '../../../../Shared/infrastructure/arranger/EnvironmentArranger'; import { CourseMother } from '../../domain/CourseMother'; @@ -27,11 +28,33 @@ describe('Search Course', () => { const course = CourseMother.random(); await repository.save(course); + const response = await repository.search(course.id); - expect(course).toEqual(await repository.search(course.id)); + expect(course).toEqual(response); }); it('should not return a non existing course', async () => { expect(await repository.search(CourseMother.random().id)).toBeFalsy(); }); }); + +describe('Get all Courses', () => { + it('should return a list of existing courses', async () => { + const firstCourse = CourseMother.random(); + const secondCourse = CourseMother.random(); + await repository.save(firstCourse); + await repository.save(secondCourse); + + const response = await repository.getAll(); + + const expected = [firstCourse, secondCourse]; + expect(expected).toEqual(response); + }); + + it('should return an empty list', async () => { + const response = await repository.getAll(); + + const expected : Course[] = []; + expect(expected).toEqual(response); + }); +}); \ No newline at end of file diff --git a/tests/Contexts/Mooc/Notifications/__mocks__/TwitSenderMock.ts b/tests/Contexts/Mooc/Notifications/__mocks__/TwitSenderMock.ts new file mode 100644 index 0000000..2dc03b8 --- /dev/null +++ b/tests/Contexts/Mooc/Notifications/__mocks__/TwitSenderMock.ts @@ -0,0 +1,22 @@ +import { TwitSender } from '../../../../../src/Contexts/Mooc/Notifications/domain/TwitSender'; +import { Twit } from '../../../../../src/Contexts/Mooc/Notifications/domain/Twit'; + +export class TwitSenderMock implements TwitSender { + private sendSpy = jest.fn(); + + async send(twit: Twit): Promise { + this.sendSpy(twit); + } + + assertSentTimes(times: number): void { + expect(this.sendSpy.mock.calls.length).toBe(times); + } + + lastTwitSent(): Twit { + const sendCalls = this.sendSpy.mock.calls; + const lastSendCall = sendCalls[sendCalls.length - 1] || []; + const lastTwitSent = lastSendCall[0] as Twit; + + return lastTwitSent; + } +} diff --git a/tests/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated.test.ts b/tests/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated.test.ts new file mode 100644 index 0000000..4dc3a0e --- /dev/null +++ b/tests/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated.test.ts @@ -0,0 +1,66 @@ +import { TwitSenderMock } from '../../__mocks__/TwitSenderMock'; +import SendNewCourseTwit from '../../../../../../src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwit'; +import SendNewCourseTwitOnCourseCreated from '../../../../../../src/Contexts/Mooc/Notifications/application/SendNewCourseTwit/SendNewCourseTwitOnCourseCreated'; +import { UuidMother } from '../../../../Shared/domain/UuidMother'; +import { CourseCreatedDomainEvent } from '../../../../../../src/Contexts/Mooc/Notifications/domain/CourseCreatedDomainEvent'; +import { NewCourseTwit } from '../../../../../../src/Contexts/Mooc/Notifications/domain/NewCourseTwit'; +import { WordMother } from '../../../../Shared/domain/WordMother'; +import { TwitSender } from '../../../../../../src/Contexts/Mooc/Notifications/domain/TwitSender'; +import { Twit } from '../../../../../../src/Contexts/Mooc/Notifications/domain/Twit'; +import { NewCourseTwitError } from '../../../../../../src/Contexts/Mooc/Notifications/domain/NewCourseTwitError'; + +describe('SendNewCourseTwitOnCourseCreated event handler', () => { + it('Send a new course twit', async () => { + const twitSenderMock = new TwitSenderMock(); + const sendNewCourseTwit = new SendNewCourseTwit(twitSenderMock); + const sendNewCourseTwitOnCourseCreated = new SendNewCourseTwitOnCourseCreated(sendNewCourseTwit); + const courseName = aNewCourseName(); + const domainEvent = aNewCourseDomainEvent(courseName); + + await sendNewCourseTwitOnCourseCreated.on(domainEvent); + + const lastTwitSent = twitSenderMock.lastTwitSent(); + twitSenderMock.assertSentTimes(1); + expect(lastTwitSent).toBeInstanceOf(NewCourseTwit); + expect(lastTwitSent.message.value).toEqual(`New course published. ${courseName}`); + }); + + it('throws a NewCourseTwitError if the twitSender fails', async () => { + const failingEmailSender = aFailingTwitSender(); + const sendNewCourseTwit = new SendNewCourseTwit(failingEmailSender); + const sendNewCourseTwitOnCourseCreated = new SendNewCourseTwitOnCourseCreated(sendNewCourseTwit); + + const domainEvent = aNewCourseDomainEvent(aNewCourseName()); + let error; + + try { + await sendNewCourseTwitOnCourseCreated.on(domainEvent); + } catch (e) { + error = e; + } + + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(NewCourseTwitError); + expect(error.message).toBe(`Error twiting new course message New course published. ${domainEvent.name}`); + }); +}); + +function aFailingTwitSender() { + return { + async send(twit: Twit) { + throw new Error('some error'); + } + } as TwitSender; +} + +function aNewCourseName(): string { + return WordMother.random(); +} + +function aNewCourseDomainEvent(courseName: string) { + return new CourseCreatedDomainEvent({ + id: UuidMother.random(), + duration: WordMother.random(), + name: courseName + }); +} diff --git a/tests/Contexts/Mooc/Notifications/domain/Twit.test.ts b/tests/Contexts/Mooc/Notifications/domain/Twit.test.ts new file mode 100644 index 0000000..fdd80c8 --- /dev/null +++ b/tests/Contexts/Mooc/Notifications/domain/Twit.test.ts @@ -0,0 +1,16 @@ +import { TwitIdMother } from "./TwitIdMother"; +import { TwitMessageMother } from "./TwitMessageMother"; +import { TwitMother } from "./TwitMother"; + + +describe('Twit', () => { + + it('should return a new twit instance', () => { + const id = TwitIdMother.random(); + const message = TwitMessageMother.random(); + const twit = TwitMother.create(id, message); + + expect(twit.id.value).toBe(id.value); + expect(twit.message.value).toBe(message.value); + }); +}); diff --git a/tests/Contexts/Mooc/Notifications/domain/TwitIdMother.ts b/tests/Contexts/Mooc/Notifications/domain/TwitIdMother.ts new file mode 100644 index 0000000..9d5c943 --- /dev/null +++ b/tests/Contexts/Mooc/Notifications/domain/TwitIdMother.ts @@ -0,0 +1,16 @@ +import { UuidMother } from "../../../Shared/domain/UuidMother"; +import { TwitId } from "../../../../../src/Contexts/Mooc/Notifications/domain/TwitId"; + +export class TwitIdMother { + static create(value: string): TwitId { + return new TwitId(value); + } + + static creator() { + return () => TwitIdMother.random(); + } + + static random(): TwitId { + return this.create(UuidMother.random()); + } +} diff --git a/tests/Contexts/Mooc/Notifications/domain/TwitMessageMother.ts b/tests/Contexts/Mooc/Notifications/domain/TwitMessageMother.ts new file mode 100644 index 0000000..6e4af00 --- /dev/null +++ b/tests/Contexts/Mooc/Notifications/domain/TwitMessageMother.ts @@ -0,0 +1,12 @@ +import { WordMother } from '../../../Shared/domain/WordMother'; +import { TwitMessage } from "../../../../../src/Contexts/Mooc/Notifications/domain/TwitMessage"; + +export class TwitMessageMother { + static create(value: string): TwitMessage { + return new TwitMessage(value); + } + + static random(): TwitMessage { + return this.create(WordMother.random()); + } +} diff --git a/tests/Contexts/Mooc/Notifications/domain/TwitMother.ts b/tests/Contexts/Mooc/Notifications/domain/TwitMother.ts new file mode 100644 index 0000000..f396328 --- /dev/null +++ b/tests/Contexts/Mooc/Notifications/domain/TwitMother.ts @@ -0,0 +1,15 @@ +import { TwitIdMother } from "./TwitIdMother"; +import { TwitId } from "../../../../../src/Contexts/Mooc/Notifications/domain/TwitId"; +import { TwitMessage } from "../../../../../src/Contexts/Mooc/Notifications/domain/TwitMessage"; +import { Twit } from "../../../../../src/Contexts/Mooc/Notifications/domain/Twit"; +import { TwitMessageMother } from "./TwitMessageMother"; + +export class TwitMother { + static create(id: TwitId, message: TwitMessage): Twit { + return new Twit({id, message}); + } + + static random(): Twit { + return this.create(TwitIdMother.random(), TwitMessageMother.random()); + } +} diff --git a/tests/Contexts/Shared/infrastructure/arranger/EnvironmentArranger.ts b/tests/Contexts/Shared/infrastructure/arranger/EnvironmentArranger.ts index 2f88c0f..c58261c 100644 --- a/tests/Contexts/Shared/infrastructure/arranger/EnvironmentArranger.ts +++ b/tests/Contexts/Shared/infrastructure/arranger/EnvironmentArranger.ts @@ -2,4 +2,6 @@ export abstract class EnvironmentArranger { public abstract arrange(): Promise; public abstract close(): Promise; + + public abstract addCourseWithId(id: string): Promise; } diff --git a/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts b/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts index 7e52276..b79cb00 100644 --- a/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts +++ b/tests/Contexts/Shared/infrastructure/mongo/MongoEnvironmentArranger.ts @@ -28,6 +28,11 @@ export class MongoEnvironmentArranger extends EnvironmentArranger { return collections.map(collection => collection.name); } + public async addCourseWithId(id: string): Promise { + const client = await this.client(); + await client.db().collection("courses").updateOne({ _id: id } ,{$set: { id, name: 'Test Course!', duration: '1', description: 'Trust me, this is a test course' } }, { upsert: true }); + } + protected client(): Promise { return this._client; } diff --git a/tests/apps/mooc_backend/features/courses/course-like.feature b/tests/apps/mooc_backend/features/courses/course-like.feature new file mode 100644 index 0000000..faed05e --- /dev/null +++ b/tests/apps/mooc_backend/features/courses/course-like.feature @@ -0,0 +1,25 @@ +Feature: Like a course + In order to be able to like courses in the platform + As a user + I want to Like an existing course + + Scenario: A valid existing course + Given a previous course has been already created + Given I send a POST request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80b/like" with body: + """ + { + "userId": "ef8ac118-8d7f-49cc-abec-78e0d05af80a" + } + """ + Then the response status code should be 204 + And the response should be empty + + Scenario: A not existing course + Given I send a PUT request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80a/like" with body: + """ + { + "userId": "ef8ac118-8d7f-49cc-abec-78e0d05af80b" + } + """ + Then the response status code should be 404 + And the response should be empty \ No newline at end of file diff --git a/tests/apps/mooc_backend/features/courses/create-course.feature b/tests/apps/mooc_backend/features/courses/create-course.feature index 1d5ca8d..b2f39d5 100644 --- a/tests/apps/mooc_backend/features/courses/create-course.feature +++ b/tests/apps/mooc_backend/features/courses/create-course.feature @@ -8,7 +8,8 @@ Feature: Create a new course """ { "name": "The best course", - "duration": "5 hours" + "duration": "5 hours", + "description": "Trust me, this is the best course." } """ Then the response status code should be 201 diff --git a/tests/apps/mooc_backend/features/courses/get-course.feature b/tests/apps/mooc_backend/features/courses/get-course.feature new file mode 100644 index 0000000..f0aef18 --- /dev/null +++ b/tests/apps/mooc_backend/features/courses/get-course.feature @@ -0,0 +1,23 @@ +Feature: Obtain a course + In order to have a course info + As a user + I want to see the courses + + Scenario: Retrieve a course + Given a previous course has been already created + When I send a GET request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80b" + Then the response status code should be 200 + And the response content should be: + """ + { + "id": "ef8ac118-8d7f-49cc-abec-78e0d05af80b", + "duration": "1", + "name": "Test Course!", + "description": "Trust me, this is a test course" + } + """ + + Scenario: Get a Not found Exception + When I send a GET request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80c" + Then the response status code should be 404 + And the response should be empty \ No newline at end of file diff --git a/tests/apps/mooc_backend/features/courses/get-courses.feature b/tests/apps/mooc_backend/features/courses/get-courses.feature new file mode 100644 index 0000000..5230539 --- /dev/null +++ b/tests/apps/mooc_backend/features/courses/get-courses.feature @@ -0,0 +1,38 @@ +Feature: Obtain a list course + In order to have a course info list + As a user + I want to see the courses + + Scenario: Retrieve a list of courses + Given Previous courses has been already created + When I send a GET request to "/courses" + Then the response status code should be 200 + And the response content should be: + """ + { + "data": [ + { + "id": "ef8ac118-8d7f-49cc-abec-78e0d05af80b", + "duration": "1", + "name": "Test Course!", + "description": "Trust me, this is a test course" + }, + { + "id": "ef8ac118-8d7f-49cc-abec-78e0d05af80c", + "duration": "1", + "name": "Test Course!", + "description": "Trust me, this is a test course" + } + ] + } + """ + + Scenario: Get an empty list + When I send a GET request to "/courses" + Then the response status code should be 200 + And the response content should be: + """ + { + "data": [] + } + """ \ No newline at end of file diff --git a/tests/apps/mooc_backend/features/courses/rename-course.feature b/tests/apps/mooc_backend/features/courses/rename-course.feature new file mode 100644 index 0000000..baf7675 --- /dev/null +++ b/tests/apps/mooc_backend/features/courses/rename-course.feature @@ -0,0 +1,25 @@ +Feature: Rename a new course + In order to be able to rename courses in the platform + As a user with admin permissions + I want to rename an existing course + + Scenario: A valid existing course + Given a previous course has been already created + Given I send a PUT request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80b/rename" with body: + """ + { + "name": "New name course" + } + """ + Then the response status code should be 204 + And the response should be empty + + Scenario: A not existing course + Given I send a PUT request to "/courses/ef8ac118-8d7f-49cc-abec-78e0d05af80a/rename" with body: + """ + { + "name": "New name course" + } + """ + Then the response status code should be 404 + And the response should be empty \ No newline at end of file diff --git a/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts b/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts index a70221f..e432368 100644 --- a/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts +++ b/tests/apps/mooc_backend/features/step_definitions/controller.steps.ts @@ -16,6 +16,10 @@ Given('I send a PUT request to {string} with body:', (route: string, body: strin _request = request(app).put(route).send(JSON.parse(body)); }); +Given('I send a POST request to {string} with body:', (route: string, body: string) => { + _request = request(app).post(route).send(JSON.parse(body)); +}); + Then('the response status code should be {int}', async (status: number) => { _response = await _request.expect(status); }); @@ -24,10 +28,21 @@ Then('the response should be empty', () => { assert.deepEqual(_response.body, {}); }); -Then('the response content should be:', response => { +Then('the response content should be:', (response: any) => { assert.deepEqual(_response.body, JSON.parse(response)); }); +Given('a previous course has been already created', async () => { + const environmentArranger: Promise = container.get('Mooc.EnvironmentArranger'); + await (await environmentArranger).addCourseWithId('ef8ac118-8d7f-49cc-abec-78e0d05af80b'); +}); + +Given('Previous courses has been already created', async () => { + const environmentArranger: Promise = container.get('Mooc.EnvironmentArranger'); + await (await environmentArranger).addCourseWithId('ef8ac118-8d7f-49cc-abec-78e0d05af80b'); + await (await environmentArranger).addCourseWithId('ef8ac118-8d7f-49cc-abec-78e0d05af80c'); +}); + Before(async () => { const environmentArranger: Promise = container.get('Mooc.EnvironmentArranger'); await (await environmentArranger).arrange();