From 7a3433e35c47c9260156562189bea1801c6b4810 Mon Sep 17 00:00:00 2001 From: Krystian Date: Sat, 9 Oct 2021 01:00:49 +0200 Subject: [PATCH 1/4] add api docs, add test for approve email-confirmation --- packages/api/rest-api-docs.yaml | 32 +++++- .../domain/approve-email-confirmation.ts | 8 +- ...email-confirmation.rest-controller.spec.ts | 75 +++++++++++++ .../domain/CompleteUserRegistration.ts | 8 +- packages/api/src/shared/test-utils.ts | 101 +++++++++++++++++- 5 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts diff --git a/packages/api/rest-api-docs.yaml b/packages/api/rest-api-docs.yaml index e671fbf1..6ffa5daa 100644 --- a/packages/api/rest-api-docs.yaml +++ b/packages/api/rest-api-docs.yaml @@ -87,6 +87,25 @@ paths: description: 'Email confirmation was successfully requested. Email was sent.' tags: - Email Confirmation + /email-confirmation/approval: + post: + summary: Confirm email for given user. + operationId: ConfirmUserRegistrationPost + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ApproveEmailConfirmationBody' + responses: + 204: + description: 'Mail for user successfully confirmed' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + tags: + - Email Confirmation /users: get: summary: All users registered in the platform @@ -138,7 +157,7 @@ paths: get: summary: How many course activites was completed by user. operationId: CourseProgress_getCourseProgress - parameters: [ ] + parameters: [] responses: 200: description: '' @@ -259,6 +278,15 @@ components: example: user-registration required: - confirmationFor + ApproveEmailConfirmationBody: + type: object + properties: + confirmationToken: + description: Given confirmation token created meanwhile registration + type: string + example: someExample.Token + required: + - confirmationFor CourseProgressGetResponseBody: description: Current course progress of logged user type: object @@ -398,7 +426,7 @@ components: message: type: string description: Error message - example: "Learning materials url was already generated" + example: 'Learning materials url was already generated' required: - message NestErrorResponseBody: diff --git a/packages/api/src/module/write/email-confirmation/domain/approve-email-confirmation.ts b/packages/api/src/module/write/email-confirmation/domain/approve-email-confirmation.ts index dfae1fc3..55323de5 100644 --- a/packages/api/src/module/write/email-confirmation/domain/approve-email-confirmation.ts +++ b/packages/api/src/module/write/email-confirmation/domain/approve-email-confirmation.ts @@ -1,5 +1,6 @@ import { ApproveEmailConfirmation } from '@/module/commands/approve-email-confirmation'; import { emailConfirmationWasApprovedEvent } from '@/module/events/email-confirmation-was-approved.domain.event'; +import { DomainRuleViolationException } from '@/shared/errors/domain-rule-violation.exception'; import { EmailConfirmationDomainEvent } from './events'; @@ -8,13 +9,14 @@ export const approveEmailConfirmation = (pastEvents: EmailConfirmationDomainEvent[]): EmailConfirmationDomainEvent[] => { const lastPublishedEmailConfirmation = pastEvents[pastEvents.length - 1]; - if (!lastPublishedEmailConfirmation) throw new Error("Couldn't find request which could be approved"); + if (!lastPublishedEmailConfirmation) + throw new DomainRuleViolationException("Couldn't find request which could be approved"); if (lastPublishedEmailConfirmation.type === 'EmailConfirmationWasApproved') - throw new Error('Email confirmation has been already approved'); + throw new DomainRuleViolationException('Email confirmation has been already approved'); if (lastPublishedEmailConfirmation.data.confirmationToken !== command.data.confirmationToken) - throw new Error('An attempt was made on obsolete confirmation token'); + throw new DomainRuleViolationException('An attempt was made on obsolete confirmation token'); return [emailConfirmationWasApprovedEvent(command.data)]; }; diff --git a/packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts b/packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts new file mode 100644 index 00000000..0f7468cf --- /dev/null +++ b/packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts @@ -0,0 +1,75 @@ +import { HttpStatus } from '@nestjs/common'; +import { AsyncReturnType } from 'type-fest'; + +import { APPROVE_ENDPOINT } from '@coderscamp/shared/models/email-confirmation/approve-email-confirmation'; + +import { DomainRuleViolationException } from '@/shared/errors/domain-rule-violation.exception'; +import { initTestModuleRestApi } from '@/shared/test-utils'; + +import { initOpenApiExpect } from '../../../../../../jest-setup'; +import { EmailConfirmationRestController } from './email-confirmation.rest-controller'; + +initOpenApiExpect(); + +describe('email confirmation | REST API', () => { + let restUnderTest: AsyncReturnType; + + beforeAll(async () => { + restUnderTest = await initTestModuleRestApi(EmailConfirmationRestController); + restUnderTest.commandBusExecute.mockClear(); + }); + + afterAll(async () => { + await restUnderTest.close(); + }); + + describe(`/POST email-confirmation${APPROVE_ENDPOINT}`, () => { + it('Failed, user is not authenticated', async () => { + // Given + restUnderTest.commandBusExecute.mockImplementation(() => Promise.resolve()); + + // When + const response = await restUnderTest.http.post(`/api/email-confirmation${APPROVE_ENDPOINT}`).send({ + confirmationToken: 'exampleToken', + }); + + // Then + expect(response.status).toBe(HttpStatus.UNAUTHORIZED); + expect(response.body.message).toEqual('Unauthorized'); + expect(response).toSatisfyApiSpec(); + }); + + it('Failed, trying to approve email without earlier request', async () => { + // Given + restUnderTest.commandBusExecute.mockRejectedValue( + new DomainRuleViolationException("Couldn't find request which could be approved"), + ); + await restUnderTest.loginUser(); + + // When + const response = await restUnderTest.http.post(`/api/email-confirmation${APPROVE_ENDPOINT}`).send({ + confirmationToken: 'exampleToken', + }); + + // Then + expect(response.status).toBe(HttpStatus.BAD_REQUEST); + expect(response.body.message).toBe("Couldn't find request which could be approved"); + }); + + it('Success, email has been approved', async () => { + // Given + restUnderTest.commandBusExecute.mockRejectedValue( + new DomainRuleViolationException("Couldn't find request which could be approved"), + ); + await restUnderTest.loginUser(); + + // When + const response = await restUnderTest.http.post(`/api/email-confirmation${APPROVE_ENDPOINT}`).send({ + confirmationToken: 'exampleToken', + }); + + // Then + expect(response.status).toBe(HttpStatus.NO_CONTENT); + }); + }); +}); diff --git a/packages/api/src/module/write/user-registration/domain/CompleteUserRegistration.ts b/packages/api/src/module/write/user-registration/domain/CompleteUserRegistration.ts index b139cb93..a5b466a2 100644 --- a/packages/api/src/module/write/user-registration/domain/CompleteUserRegistration.ts +++ b/packages/api/src/module/write/user-registration/domain/CompleteUserRegistration.ts @@ -1,5 +1,6 @@ import { CompleteUserRegistration } from '@/commands/complete-user-registration'; import { userRegistrationWasCompletedEvent } from '@/events/user-registration-was-completed.domain-event'; +import { DomainRuleViolationException } from '@/shared/errors/domain-rule-violation.exception'; import { UserRegistrationDomainEvent } from '@/write/user-registration/domain/events'; export const completeUserRegistration = @@ -8,10 +9,13 @@ export const completeUserRegistration = const { data } = command; const lastUserRegistrationEvent = pastEvents[pastEvents.length - 1]; - if (!lastUserRegistrationEvent) throw new Error('Impossible to complete registration while it is not started'); + if (!lastUserRegistrationEvent) + throw new DomainRuleViolationException('Impossible to complete registration while it is not started'); if (lastUserRegistrationEvent.type === 'UserRegistrationWasCompleted') - throw new Error(`Registration for user ${lastUserRegistrationEvent.data.fullName} was already completed`); + throw new DomainRuleViolationException( + `Registration for user ${lastUserRegistrationEvent.data.fullName} was already completed`, + ); const { fullName, emailAddress, hashedPassword } = lastUserRegistrationEvent.data; const userRegistrationWasCompleted = userRegistrationWasCompletedEvent({ diff --git a/packages/api/src/shared/test-utils.ts b/packages/api/src/shared/test-utils.ts index 2d583f70..c5ab78ef 100644 --- a/packages/api/src/shared/test-utils.ts +++ b/packages/api/src/shared/test-utils.ts @@ -2,13 +2,21 @@ import { INestApplication } from '@nestjs/common'; import { Abstract } from '@nestjs/common/interfaces'; import { ModuleMetadata } from '@nestjs/common/interfaces/modules/module-metadata.interface'; import { Type } from '@nestjs/common/interfaces/type.interface'; -import { CommandBus, ICommand } from '@nestjs/cqrs'; +import { CommandBus, CqrsModule, ICommand } from '@nestjs/cqrs'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; import { Test, TestingModule, TestingModuleBuilder } from '@nestjs/testing'; import _ from 'lodash'; +import { env } from 'process'; import supertest from 'supertest'; import { v4 as uuid } from 'uuid'; import waitForExpect from 'wait-for-expect'; +import { AuthController } from '@/crud/auth/auth.controller'; +import { AuthModule } from '@/crud/auth/auth.module'; +import { AuthUserRepository } from '@/crud/auth/auth-user.repository'; +import { JwtStrategy } from '@/crud/auth/jwt/jwt.strategy'; +import { LocalStrategy } from '@/crud/auth/local/local.strategy'; import { ApplicationCommand, ApplicationEvent } from '@/module/application-command-events'; import { DomainCommand } from '@/module/domain.command'; import { DomainEvent } from '@/module/domain.event'; @@ -20,8 +28,13 @@ import { StorableEvent } from '@/write/shared/application/event-repository'; import { EventStreamName } from '@/write/shared/application/event-stream-name.value-object'; import { SubscriptionId } from '@/write/shared/application/events-subscription/events-subscription'; import { ID_GENERATOR, IdGenerator } from '@/write/shared/application/id-generator'; +import { PASSWORD_ENCODER } from '@/write/shared/application/password-encoder'; import { TIME_PROVIDER } from '@/write/shared/application/time-provider.port'; import { UuidGenerator } from '@/write/shared/infrastructure/id-generator/uuid-generator'; +import { + CryptoPasswordEncoder, + hashPassword, +} from '@/write/shared/infrastructure/password-encoder/crypto-password-encoder'; import { FixedTimeProvider } from '@/write/shared/infrastructure/time-provider/fixed-time-provider'; import { SystemTimeProvider } from '@/write/shared/infrastructure/time-provider/system-time-provider'; import { SharedModule } from '@/write/shared/shared.module'; @@ -29,6 +42,7 @@ import { SharedModule } from '@/write/shared/shared.module'; import { setupMiddlewares } from '../app.middlewares'; import { AppModule } from '../app.module'; import { eventEmitterRootModule } from '../event-emitter.root-module'; +import { PrismaModule } from './prisma/prisma.module'; export async function cleanupDatabase(prismaService: PrismaService) { await Promise.all( @@ -374,6 +388,10 @@ export const commandBusNoFailWithoutHandler: Partial = { execute: jest.fn(), }; +const strategies = [JwtStrategy, LocalStrategy]; +const modules = [PrismaModule, JwtModule, AuthModule]; +const services = [PrismaService, AuthUserRepository]; + export async function initTestModuleRestApi( controller: Type, config?: (module: TestingModuleBuilder) => TestingModuleBuilder, @@ -389,22 +407,99 @@ export async function initTestModuleRestApi( provide: ApplicationCommandFactory, useValue: new ApplicationCommandFactory(new UuidGenerator(), new SystemTimeProvider()), }, + { + provide: PASSWORD_ENCODER, + useClass: CryptoPasswordEncoder, + }, + ...strategies, + ...services, + ...modules, + ], + controllers: [controller, AuthController], + imports: [ + JwtModule.register({ + secret: env.JWT_SECRET, + signOptions: { expiresIn: env.TOKEN_EXPIRATION_TIME }, + }), + PassportModule, + CqrsModule, ], - controllers: [controller], }); const moduleRef = await (config ? config(moduleBuilder) : moduleBuilder).compile(); const app: INestApplication = moduleRef.createNestApplication(); + const prismaService = app.get(PrismaService); + setupMiddlewares(app); await app.init(); const http = supertest(app.getHttpServer()); + let exampleUserCreated = false; + let isUserLogged = false; + + const exampleAuthUser = { + id: uuid(), + email: 'example1@email.com', + password: 'stronk', + role: 'User', + } as const; + + const addExampleUser = async () => { + const hashedPassword = await hashPassword(exampleAuthUser.password); + + await prismaService.authUser.create({ + data: { + ...exampleAuthUser, + password: hashedPassword, + }, + }); + + exampleUserCreated = true; + }; + + const removeExampleUser = async () => { + console.log(`removing user -> ${exampleAuthUser.email}`); + + await prismaService.authUser.delete({ + where: { + email: exampleAuthUser.email, + }, + }); + exampleUserCreated = false; + }; + + const loginUser = async () => { + if (!exampleUserCreated) { + await addExampleUser(); + } + + const response = await http.post('/api/auth/login').send(exampleAuthUser); + + if (response.status !== 204) { + throw new Error('Example user login failed'); + } + + isUserLogged = true; + }; + + const logoutUser = async () => { + const response = await http.post('/api/auth/logout').send(); + + if (response.status !== 201) throw new Error('Logout user failed'); + + isUserLogged = false; + }; + async function close() { await app.close(); + + if (isUserLogged && 5 < 4) await logoutUser(); + + if (exampleUserCreated) await removeExampleUser(); } - return { http, close, commandBusExecute }; + return { http, close, commandBusExecute, loginUser, logoutUser, isUserLogged }; } From 8ea20e61cfdd1ef8878bcd40774ad0df70306ebc Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Fri, 22 Oct 2021 10:44:22 +0200 Subject: [PATCH 2/4] #378 getting access_token from set-cookies response header --- ...email-confirmation.rest-controller.spec.ts | 34 +++++++++++-------- packages/api/src/shared/test-utils.ts | 4 +++ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts b/packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts index 0f7468cf..9322a249 100644 --- a/packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts +++ b/packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts @@ -23,7 +23,7 @@ describe('email confirmation | REST API', () => { await restUnderTest.close(); }); - describe(`/POST email-confirmation${APPROVE_ENDPOINT}`, () => { + describe(`POST /email-confirmation${APPROVE_ENDPOINT}`, () => { it('Failed, user is not authenticated', async () => { // Given restUnderTest.commandBusExecute.mockImplementation(() => Promise.resolve()); @@ -39,34 +39,40 @@ describe('email confirmation | REST API', () => { expect(response).toSatisfyApiSpec(); }); - it('Failed, trying to approve email without earlier request', async () => { + it('Failed, trying to approve email confirmation without earlier request', async () => { // Given restUnderTest.commandBusExecute.mockRejectedValue( new DomainRuleViolationException("Couldn't find request which could be approved"), ); - await restUnderTest.loginUser(); + + const cookie = await restUnderTest.loginUser(); // When - const response = await restUnderTest.http.post(`/api/email-confirmation${APPROVE_ENDPOINT}`).send({ - confirmationToken: 'exampleToken', - }); + const response = await restUnderTest.http + .post(`/api/email-confirmation${APPROVE_ENDPOINT}`) + .set('Cookie', cookie) + .send({ + confirmationToken: 'exampleToken', + }); // Then expect(response.status).toBe(HttpStatus.BAD_REQUEST); expect(response.body.message).toBe("Couldn't find request which could be approved"); }); - it('Success, email has been approved', async () => { + it('Success, email confirmation has been approved', async () => { // Given - restUnderTest.commandBusExecute.mockRejectedValue( - new DomainRuleViolationException("Couldn't find request which could be approved"), - ); - await restUnderTest.loginUser(); + restUnderTest.commandBusExecute.mockImplementation(() => Promise.resolve()); + + const cookie = await restUnderTest.loginUser(); // When - const response = await restUnderTest.http.post(`/api/email-confirmation${APPROVE_ENDPOINT}`).send({ - confirmationToken: 'exampleToken', - }); + const response = await restUnderTest.http + .post(`/api/email-confirmation${APPROVE_ENDPOINT}`) + .set('Cookie', cookie) + .send({ + confirmationToken: 'exampleToken', + }); // Then expect(response.status).toBe(HttpStatus.NO_CONTENT); diff --git a/packages/api/src/shared/test-utils.ts b/packages/api/src/shared/test-utils.ts index c5ab78ef..93ea49b7 100644 --- a/packages/api/src/shared/test-utils.ts +++ b/packages/api/src/shared/test-utils.ts @@ -482,7 +482,11 @@ export async function initTestModuleRestApi( throw new Error('Example user login failed'); } + const cookie = response.get('set-cookie'); + isUserLogged = true; + + return cookie; }; const logoutUser = async () => { From 6ef96676661ad2c12bd683a3e6490a917b579cf9 Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Fri, 22 Oct 2021 11:03:52 +0200 Subject: [PATCH 3/4] #378 move rest api test utils to dedicated file --- .eslintrc.js | 1 + ...email-confirmation.rest-controller.spec.ts | 2 +- .../user-registration.rest-controller.spec.ts | 2 +- .../api/src/shared/rest-api-test-utils.ts | 116 ++++++++++++++ packages/api/src/shared/test-utils.ts | 141 +----------------- 5 files changed, 120 insertions(+), 142 deletions(-) create mode 100644 packages/api/src/shared/rest-api-test-utils.ts diff --git a/.eslintrc.js b/.eslintrc.js index 6a416bb4..97e9c75f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -175,6 +175,7 @@ module.exports = { 'testHelpers.tsx', '*.test-module.ts', 'test-utils.ts', + 'rest-api-test-utils.ts', '*Handlers.ts', '**/mocks/*', 'jest-setup.ts', diff --git a/packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts b/packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts index 9322a249..76924369 100644 --- a/packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts +++ b/packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts @@ -4,7 +4,7 @@ import { AsyncReturnType } from 'type-fest'; import { APPROVE_ENDPOINT } from '@coderscamp/shared/models/email-confirmation/approve-email-confirmation'; import { DomainRuleViolationException } from '@/shared/errors/domain-rule-violation.exception'; -import { initTestModuleRestApi } from '@/shared/test-utils'; +import { initTestModuleRestApi } from '@/shared/rest-api-test-utils'; import { initOpenApiExpect } from '../../../../../../jest-setup'; import { EmailConfirmationRestController } from './email-confirmation.rest-controller'; diff --git a/packages/api/src/module/write/user-registration/presentation/rest/user-registration.rest-controller.spec.ts b/packages/api/src/module/write/user-registration/presentation/rest/user-registration.rest-controller.spec.ts index 35345be0..420d6a16 100644 --- a/packages/api/src/module/write/user-registration/presentation/rest/user-registration.rest-controller.spec.ts +++ b/packages/api/src/module/write/user-registration/presentation/rest/user-registration.rest-controller.spec.ts @@ -4,7 +4,7 @@ import { AsyncReturnType } from 'type-fest'; import { registerError } from '@coderscamp/shared/models/auth/register'; import { DomainRuleViolationException } from '@/shared/errors/domain-rule-violation.exception'; -import { initTestModuleRestApi } from '@/shared/test-utils'; +import { initTestModuleRestApi } from '@/shared/rest-api-test-utils'; import { UserRegistrationRestController } from '@/write/user-registration/presentation/rest/user-registration.rest-controller'; import { initOpenApiExpect } from '../../../../../../jest-setup'; diff --git a/packages/api/src/shared/rest-api-test-utils.ts b/packages/api/src/shared/rest-api-test-utils.ts new file mode 100644 index 00000000..7686652f --- /dev/null +++ b/packages/api/src/shared/rest-api-test-utils.ts @@ -0,0 +1,116 @@ +import { INestApplication } from '@nestjs/common'; +import { Type } from '@nestjs/common/interfaces/type.interface'; +import { CommandBus } from '@nestjs/cqrs'; +import { Test, TestingModuleBuilder } from '@nestjs/testing'; +import supertest from 'supertest'; +import { v4 as uuid } from 'uuid'; + +import { AuthModule } from '@/crud/auth/auth.module'; +import { PrismaService } from '@/prisma/prisma.service'; +import { ApplicationCommandFactory } from '@/write/shared/application/application-command.factory'; +import { UuidGenerator } from '@/write/shared/infrastructure/id-generator/uuid-generator'; +import { hashPassword } from '@/write/shared/infrastructure/password-encoder/crypto-password-encoder'; +import { SystemTimeProvider } from '@/write/shared/infrastructure/time-provider/system-time-provider'; + +import { setupMiddlewares } from '../app.middlewares'; +import { eventEmitterRootModule } from '../event-emitter.root-module'; + +export async function initTestModuleRestApi( + controller: Type, + config?: (module: TestingModuleBuilder) => TestingModuleBuilder, +) { + const commandBusExecute = jest.fn(); + const moduleBuilder = await Test.createTestingModule({ + providers: [ + { + provide: CommandBus, + useValue: { execute: commandBusExecute, register: jest.fn() }, + }, + { + provide: ApplicationCommandFactory, + useValue: new ApplicationCommandFactory(new UuidGenerator(), new SystemTimeProvider()), + }, + ], + controllers: [controller], + imports: [eventEmitterRootModule, AuthModule], + }); + const moduleRef = await (config ? config(moduleBuilder) : moduleBuilder).compile(); + + const app: INestApplication = moduleRef.createNestApplication(); + + const prismaService = app.get(PrismaService); + + setupMiddlewares(app); + + await app.init(); + + const http = supertest(app.getHttpServer()); + + let exampleUserCreated = false; + let isUserLogged = false; + + const exampleAuthUser = { + id: uuid(), + email: 'example1@email.com', + password: 'stronk', + role: 'User', + } as const; + + const addExampleUser = async () => { + const hashedPassword = await hashPassword(exampleAuthUser.password); + + await prismaService.authUser.create({ + data: { + ...exampleAuthUser, + password: hashedPassword, + }, + }); + + exampleUserCreated = true; + }; + + const removeExampleUser = async () => { + await prismaService.authUser.delete({ + where: { + email: exampleAuthUser.email, + }, + }); + exampleUserCreated = false; + }; + + const loginUser = async () => { + if (!exampleUserCreated) { + await addExampleUser(); + } + + const response = await http.post('/api/auth/login').send(exampleAuthUser); + + if (response.status !== 204) { + throw new Error('Example user login failed'); + } + + const cookie = response.get('set-cookie'); + + isUserLogged = true; + + return cookie; + }; + + const logoutUser = async () => { + const response = await http.post('/api/auth/logout').send(); + + if (response.status !== 201) throw new Error('Logout user failed'); + + isUserLogged = false; + }; + + async function close() { + await app.close(); + + if (isUserLogged && 5 < 4) await logoutUser(); + + if (exampleUserCreated) await removeExampleUser(); + } + + return { http, close, commandBusExecute, loginUser, logoutUser, isUserLogged }; +} diff --git a/packages/api/src/shared/test-utils.ts b/packages/api/src/shared/test-utils.ts index 93ea49b7..d5ee93da 100644 --- a/packages/api/src/shared/test-utils.ts +++ b/packages/api/src/shared/test-utils.ts @@ -1,22 +1,12 @@ -import { INestApplication } from '@nestjs/common'; import { Abstract } from '@nestjs/common/interfaces'; import { ModuleMetadata } from '@nestjs/common/interfaces/modules/module-metadata.interface'; import { Type } from '@nestjs/common/interfaces/type.interface'; -import { CommandBus, CqrsModule, ICommand } from '@nestjs/cqrs'; -import { JwtModule } from '@nestjs/jwt'; -import { PassportModule } from '@nestjs/passport'; +import { CommandBus, ICommand } from '@nestjs/cqrs'; import { Test, TestingModule, TestingModuleBuilder } from '@nestjs/testing'; import _ from 'lodash'; -import { env } from 'process'; -import supertest from 'supertest'; import { v4 as uuid } from 'uuid'; import waitForExpect from 'wait-for-expect'; -import { AuthController } from '@/crud/auth/auth.controller'; -import { AuthModule } from '@/crud/auth/auth.module'; -import { AuthUserRepository } from '@/crud/auth/auth-user.repository'; -import { JwtStrategy } from '@/crud/auth/jwt/jwt.strategy'; -import { LocalStrategy } from '@/crud/auth/local/local.strategy'; import { ApplicationCommand, ApplicationEvent } from '@/module/application-command-events'; import { DomainCommand } from '@/module/domain.command'; import { DomainEvent } from '@/module/domain.event'; @@ -28,21 +18,12 @@ import { StorableEvent } from '@/write/shared/application/event-repository'; import { EventStreamName } from '@/write/shared/application/event-stream-name.value-object'; import { SubscriptionId } from '@/write/shared/application/events-subscription/events-subscription'; import { ID_GENERATOR, IdGenerator } from '@/write/shared/application/id-generator'; -import { PASSWORD_ENCODER } from '@/write/shared/application/password-encoder'; import { TIME_PROVIDER } from '@/write/shared/application/time-provider.port'; -import { UuidGenerator } from '@/write/shared/infrastructure/id-generator/uuid-generator'; -import { - CryptoPasswordEncoder, - hashPassword, -} from '@/write/shared/infrastructure/password-encoder/crypto-password-encoder'; import { FixedTimeProvider } from '@/write/shared/infrastructure/time-provider/fixed-time-provider'; -import { SystemTimeProvider } from '@/write/shared/infrastructure/time-provider/system-time-provider'; import { SharedModule } from '@/write/shared/shared.module'; -import { setupMiddlewares } from '../app.middlewares'; import { AppModule } from '../app.module'; import { eventEmitterRootModule } from '../event-emitter.root-module'; -import { PrismaModule } from './prisma/prisma.module'; export async function cleanupDatabase(prismaService: PrismaService) { await Promise.all( @@ -387,123 +368,3 @@ export const commandBusNoFailWithoutHandler: Partial = { register: jest.fn(), execute: jest.fn(), }; - -const strategies = [JwtStrategy, LocalStrategy]; -const modules = [PrismaModule, JwtModule, AuthModule]; -const services = [PrismaService, AuthUserRepository]; - -export async function initTestModuleRestApi( - controller: Type, - config?: (module: TestingModuleBuilder) => TestingModuleBuilder, -) { - const commandBusExecute = jest.fn(); - const moduleBuilder = await Test.createTestingModule({ - providers: [ - { - provide: CommandBus, - useValue: { execute: commandBusExecute, register: jest.fn() }, - }, - { - provide: ApplicationCommandFactory, - useValue: new ApplicationCommandFactory(new UuidGenerator(), new SystemTimeProvider()), - }, - { - provide: PASSWORD_ENCODER, - useClass: CryptoPasswordEncoder, - }, - ...strategies, - ...services, - ...modules, - ], - controllers: [controller, AuthController], - imports: [ - JwtModule.register({ - secret: env.JWT_SECRET, - signOptions: { expiresIn: env.TOKEN_EXPIRATION_TIME }, - }), - PassportModule, - CqrsModule, - ], - }); - const moduleRef = await (config ? config(moduleBuilder) : moduleBuilder).compile(); - - const app: INestApplication = moduleRef.createNestApplication(); - - const prismaService = app.get(PrismaService); - - setupMiddlewares(app); - - await app.init(); - - const http = supertest(app.getHttpServer()); - - let exampleUserCreated = false; - let isUserLogged = false; - - const exampleAuthUser = { - id: uuid(), - email: 'example1@email.com', - password: 'stronk', - role: 'User', - } as const; - - const addExampleUser = async () => { - const hashedPassword = await hashPassword(exampleAuthUser.password); - - await prismaService.authUser.create({ - data: { - ...exampleAuthUser, - password: hashedPassword, - }, - }); - - exampleUserCreated = true; - }; - - const removeExampleUser = async () => { - console.log(`removing user -> ${exampleAuthUser.email}`); - - await prismaService.authUser.delete({ - where: { - email: exampleAuthUser.email, - }, - }); - exampleUserCreated = false; - }; - - const loginUser = async () => { - if (!exampleUserCreated) { - await addExampleUser(); - } - - const response = await http.post('/api/auth/login').send(exampleAuthUser); - - if (response.status !== 204) { - throw new Error('Example user login failed'); - } - - const cookie = response.get('set-cookie'); - - isUserLogged = true; - - return cookie; - }; - - const logoutUser = async () => { - const response = await http.post('/api/auth/logout').send(); - - if (response.status !== 201) throw new Error('Logout user failed'); - - isUserLogged = false; - }; - - async function close() { - await app.close(); - - if (isUserLogged && 5 < 4) await logoutUser(); - - if (exampleUserCreated) await removeExampleUser(); - } - - return { http, close, commandBusExecute, loginUser, logoutUser, isUserLogged }; -} From 0ab4809d6628d1a751ef4ed5c089b3532a6be2f5 Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Fri, 22 Oct 2021 11:33:25 +0200 Subject: [PATCH 4/4] #378 Add asLoggedUser api util. --- ...email-confirmation.rest-controller.spec.ts | 24 +++--- .../api/src/shared/rest-api-test-utils.ts | 81 +++++++++---------- 2 files changed, 50 insertions(+), 55 deletions(-) diff --git a/packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts b/packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts index 76924369..b3259478 100644 --- a/packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts +++ b/packages/api/src/module/write/email-confirmation/presentation/rest/email-confirmation.rest-controller.spec.ts @@ -45,37 +45,33 @@ describe('email confirmation | REST API', () => { new DomainRuleViolationException("Couldn't find request which could be approved"), ); - const cookie = await restUnderTest.loginUser(); - // When - const response = await restUnderTest.http - .post(`/api/email-confirmation${APPROVE_ENDPOINT}`) - .set('Cookie', cookie) - .send({ + const response = await restUnderTest.asLoggedUser((http) => + http.post(`/api/email-confirmation${APPROVE_ENDPOINT}`).send({ confirmationToken: 'exampleToken', - }); + }), + ); // Then expect(response.status).toBe(HttpStatus.BAD_REQUEST); expect(response.body.message).toBe("Couldn't find request which could be approved"); + expect(response).toSatisfyApiSpec(); }); it('Success, email confirmation has been approved', async () => { // Given restUnderTest.commandBusExecute.mockImplementation(() => Promise.resolve()); - const cookie = await restUnderTest.loginUser(); - // When - const response = await restUnderTest.http - .post(`/api/email-confirmation${APPROVE_ENDPOINT}`) - .set('Cookie', cookie) - .send({ + const response = await restUnderTest.asLoggedUser((http) => + http.post(`/api/email-confirmation${APPROVE_ENDPOINT}`).send({ confirmationToken: 'exampleToken', - }); + }), + ); // Then expect(response.status).toBe(HttpStatus.NO_CONTENT); + expect(response).toSatisfyApiSpec(); }); }); }); diff --git a/packages/api/src/shared/rest-api-test-utils.ts b/packages/api/src/shared/rest-api-test-utils.ts index 7686652f..f9386769 100644 --- a/packages/api/src/shared/rest-api-test-utils.ts +++ b/packages/api/src/shared/rest-api-test-utils.ts @@ -5,8 +5,11 @@ import { Test, TestingModuleBuilder } from '@nestjs/testing'; import supertest from 'supertest'; import { v4 as uuid } from 'uuid'; +import { AuthUser } from '@coderscamp/shared/models/auth'; + import { AuthModule } from '@/crud/auth/auth.module'; import { PrismaService } from '@/prisma/prisma.service'; +import { cleanupDatabase } from '@/shared/test-utils'; import { ApplicationCommandFactory } from '@/write/shared/application/application-command.factory'; import { UuidGenerator } from '@/write/shared/infrastructure/id-generator/uuid-generator'; import { hashPassword } from '@/write/shared/infrastructure/password-encoder/crypto-password-encoder'; @@ -15,6 +18,8 @@ import { SystemTimeProvider } from '@/write/shared/infrastructure/time-provider/ import { setupMiddlewares } from '../app.middlewares'; import { eventEmitterRootModule } from '../event-emitter.root-module'; +const DEFAULT_TEST_PASSWORD = 'stronk'; + export async function initTestModuleRestApi( controller: Type, config?: (module: TestingModuleBuilder) => TestingModuleBuilder, @@ -40,77 +45,71 @@ export async function initTestModuleRestApi( const prismaService = app.get(PrismaService); + await cleanupDatabase(prismaService); + setupMiddlewares(app); await app.init(); const http = supertest(app.getHttpServer()); - let exampleUserCreated = false; - let isUserLogged = false; + const randomUser = () => { + const id = uuid(); - const exampleAuthUser = { - id: uuid(), - email: 'example1@email.com', - password: 'stronk', - role: 'User', - } as const; - - const addExampleUser = async () => { - const hashedPassword = await hashPassword(exampleAuthUser.password); + return { + id, + email: `${id}@email.com`, + password: DEFAULT_TEST_PASSWORD, + }; + }; - await prismaService.authUser.create({ + const registerUser = async (userToCreate: Partial = randomUser()) => { + const id = userToCreate.id ?? uuid(); + const authUser = { + id, + email: `${id}@email.com`, + password: DEFAULT_TEST_PASSWORD, + ...userToCreate, + }; + const hashedPassword = await hashPassword(authUser.password); + + return prismaService.authUser.create({ data: { - ...exampleAuthUser, + ...authUser, password: hashedPassword, }, }); - - exampleUserCreated = true; - }; - - const removeExampleUser = async () => { - await prismaService.authUser.delete({ - where: { - email: exampleAuthUser.email, - }, - }); - exampleUserCreated = false; }; - const loginUser = async () => { - if (!exampleUserCreated) { - await addExampleUser(); - } + const loginUser = async (request: { email: string } = randomUser()) => { + await registerUser({ email: request.email, password: DEFAULT_TEST_PASSWORD }); - const response = await http.post('/api/auth/login').send(exampleAuthUser); + const response = await http.post('/api/auth/login').send(request); if (response.status !== 204) { throw new Error('Example user login failed'); } - const cookie = response.get('set-cookie'); - - isUserLogged = true; - - return cookie; + return response.get('set-cookie'); }; const logoutUser = async () => { const response = await http.post('/api/auth/logout').send(); - if (response.status !== 201) throw new Error('Logout user failed'); + if (response.status !== 201) { + throw new Error('Logout user failed'); + } + }; + + const asLoggedUser = async (request: (http: supertest.SuperTest) => supertest.Test) => { + const cookie = await loginUser(); - isUserLogged = false; + return request(http).set('Cookie', cookie); }; async function close() { await app.close(); - - if (isUserLogged && 5 < 4) await logoutUser(); - - if (exampleUserCreated) await removeExampleUser(); } - return { http, close, commandBusExecute, loginUser, logoutUser, isUserLogged }; + return { http, close, commandBusExecute, loginUser, logoutUser, asLoggedUser }; }