From 8ba3198989ea5506445a13d436bbc6fa18afb8e2 Mon Sep 17 00:00:00 2001 From: Eric Cabrel TIOGO Date: Thu, 6 Jun 2024 23:35:48 +0200 Subject: [PATCH 1/6] test(backend): improvements on integration tests related to auth --- .../src/features/app/app.service.spec.ts | 4 +- .../auth/graphql/auth.integration.spec.ts | 176 +++--------------- apps/backend/src/utils/tests/helpers.ts | 134 ++++++++----- 3 files changed, 111 insertions(+), 203 deletions(-) diff --git a/apps/backend/src/features/app/app.service.spec.ts b/apps/backend/src/features/app/app.service.spec.ts index 03241601..d1b70839 100644 --- a/apps/backend/src/features/app/app.service.spec.ts +++ b/apps/backend/src/features/app/app.service.spec.ts @@ -9,6 +9,8 @@ const prismaServiceMock = mock(); const roleServiceMock = mock(); const userServiceMock = mock(); +const { ADMIN_PASSWORD } = process.env; + describe('Test App Service', () => { let appService: AppService; let roleService: RoleService; @@ -46,6 +48,6 @@ describe('Test App Service', () => { expect(roleService.loadRoles).toHaveBeenCalledTimes(1); expect(userService.loadAdminUser).toHaveBeenCalledTimes(1); - expect(userService.loadAdminUser).toHaveBeenCalledWith(role, 'qwerty'); + expect(userService.loadAdminUser).toHaveBeenCalledWith(role, ADMIN_PASSWORD); }); }); diff --git a/apps/backend/src/features/auth/graphql/auth.integration.spec.ts b/apps/backend/src/features/auth/graphql/auth.integration.spec.ts index e2bff88c..f9e7207e 100644 --- a/apps/backend/src/features/auth/graphql/auth.integration.spec.ts +++ b/apps/backend/src/features/auth/graphql/auth.integration.spec.ts @@ -1,5 +1,5 @@ -import { PrismaService, RoleService, SessionService, UserService } from '@snipcode/domain'; -import { generateJwtToken, isValidUUIDV4 } from '@snipcode/utils'; +import { SessionService } from '@snipcode/domain'; +import { isValidUUIDV4 } from '@snipcode/utils'; import request from 'supertest'; import { TestHelper } from '../../../utils/tests/helpers'; @@ -10,20 +10,14 @@ const graphqlEndpoint = '/graphql'; describe('Test Authentication', () => { let server: TestServer; let testHelper: TestHelper; - let prismaService: PrismaService; - let roleService: RoleService; let sessionService: SessionService; - let userService: UserService; beforeAll(async () => { server = await startTestServer(); - prismaService = server.app.get(PrismaService); - roleService = server.app.get(RoleService); - userService = server.app.get(UserService); sessionService = server.app.get(SessionService); - testHelper = new TestHelper(prismaService, roleService, userService); + testHelper = new TestHelper(server.app, graphqlEndpoint); }); beforeEach(async () => { @@ -81,7 +75,7 @@ describe('Test Authentication', () => { }, }; - await testHelper.createTestUser({ email: variables.input.email }); + await testHelper.signupUser({ email: variables.input.email }); const response = await request(server.app.getHttpServer()) .post(graphqlEndpoint) @@ -127,11 +121,10 @@ describe('Test Authentication', () => { } `; - await testHelper.createTestUser({ + await testHelper.signupUser({ email: 'jane.doe@snipcode.dev', isEnabled: true, password: 'password', - role: 'user', }); const variables = { @@ -163,11 +156,10 @@ describe('Test Authentication', () => { } `; - await testHelper.createTestUser({ + await testHelper.signupUser({ email: 'disabled.user@snipcode.dev', isEnabled: false, password: 'password', - role: 'user', }); const variables = { @@ -186,14 +178,14 @@ describe('Test Authentication', () => { expect(error.message).toEqual('Your account is disabled!'); }); - test('Returns when retrieving the authenticated user without an authentication token', async () => { + test('Returns an error when retrieving the authenticated user without an authentication token', async () => { const authenticatedUserQuery = ` - query AuthenticatedUser { - authenticatedUser { - id - } + query AuthenticatedUser { + authenticatedUser { + id } - `; + } + `; const response = await request(server.app.getHttpServer()) .post(graphqlEndpoint) @@ -207,71 +199,13 @@ describe('Test Authentication', () => { }); test('Retrieve the authenticated user', async () => { - const signUpQuery = ` - mutation SignupUser($input: SignupUserInput!) { - signupUser(input: $input) { - __typename - message - userId - } - } - `; - - const signUpVariables = { - input: { - email: 'jon.doe@snipcode.dev', - name: 'John Doe', - password: 'password', - }, - }; - - const signUpResponse = await request(server.app.getHttpServer()) - .post(graphqlEndpoint) - .send({ query: signUpQuery, variables: signUpVariables }) - .expect(200); - - const confirmationToken = generateJwtToken({ - expiresIn: '1h', - payload: { userId: signUpResponse.body.data.signupUser.userId }, - secret: process.env.JWT_SECRET, - }); - - const confirmUserQuery = ` - mutation ConfirmUser($token: String!) { - confirmUser(token: $token) { - message - } - } - `; - - const confirmUserVariables = { - token: confirmationToken, + const input = { + email: 'jon.doe@snipcode.dev', + name: 'John Doe', + password: 'password', }; - await request(server.app.getHttpServer()) - .post(graphqlEndpoint) - .send({ query: confirmUserQuery, variables: confirmUserVariables }) - .expect(200); - - const loginQuery = ` - mutation LoginUser($email: String!, $password: String!) { - loginUser(email: $email, password: $password) { - token - } - } - `; - - const loginVariables = { - email: signUpVariables.input.email, - password: signUpVariables.input.password, - }; - - const loginResponse = await request(server.app.getHttpServer()) - .post(graphqlEndpoint) - .send({ query: loginQuery, variables: loginVariables }) - .expect(200); - - const authToken = loginResponse.body.data.loginUser.token; + const { authToken, userId } = await testHelper.createAuthenticatedUser({ ...input }); const authenticatedUserQuery = ` query AuthenticatedUser { @@ -306,10 +240,10 @@ describe('Test Authentication', () => { expect(authenticatedUser).toMatchObject({ createdAt: expect.any(Number), - email: loginVariables.email, - id: signUpResponse.body.data.signupUser.userId, + email: input.email, + id: userId, isEnabled: true, - name: signUpVariables.input.name, + name: input.name, oauthProvider: 'email', pictureUrl: null, role: { @@ -325,72 +259,12 @@ describe('Test Authentication', () => { }); test('Log out the authenticated user', async () => { - const signUpQuery = ` - mutation SignupUser($input: SignupUserInput!) { - signupUser(input: $input) { - __typename - message - userId - } - } - `; - - const signUpVariables = { - input: { - email: 'jane.doe@snipcode.dev', - name: 'Jane Doe', - password: 'password', - }, - }; - - const signUpResponse = await request(server.app.getHttpServer()) - .post(graphqlEndpoint) - .send({ query: signUpQuery, variables: signUpVariables }) - .expect(200); - - const confirmationToken = generateJwtToken({ - expiresIn: '1h', - payload: { userId: signUpResponse.body.data.signupUser.userId }, - secret: process.env.JWT_SECRET, + const { authToken, userId } = await testHelper.createAuthenticatedUser({ + email: 'jane.doe@snipcode.dev', + name: 'Jane Doe', + password: 'password', }); - const confirmUserQuery = ` - mutation ConfirmUser($token: String!) { - confirmUser(token: $token) { - message - } - } - `; - - const confirmUserVariables = { - token: confirmationToken, - }; - - await request(server.app.getHttpServer()) - .post(graphqlEndpoint) - .send({ query: confirmUserQuery, variables: confirmUserVariables }) - .expect(200); - - const loginQuery = ` - mutation LoginUser($email: String!, $password: String!) { - loginUser(email: $email, password: $password) { - token - } - } - `; - - const loginVariables = { - email: signUpVariables.input.email, - password: signUpVariables.input.password, - }; - - const loginResponse = await request(server.app.getHttpServer()) - .post(graphqlEndpoint) - .send({ query: loginQuery, variables: loginVariables }) - .expect(200); - - const authToken = loginResponse.body.data.loginUser.token; - const authenticatedUserQuery = ` query AuthenticatedUser { authenticatedUser { @@ -407,7 +281,7 @@ describe('Test Authentication', () => { const { authenticatedUser } = response.body.data; - expect(authenticatedUser.id).toEqual(signUpResponse.body.data.signupUser.userId); + expect(authenticatedUser.id).toEqual(userId); const logoutQuery = ` mutation LogoutUser { diff --git a/apps/backend/src/utils/tests/helpers.ts b/apps/backend/src/utils/tests/helpers.ts index 902c8a88..efcf5076 100644 --- a/apps/backend/src/utils/tests/helpers.ts +++ b/apps/backend/src/utils/tests/helpers.ts @@ -1,74 +1,106 @@ -import { randEmail, randFullName, randImg, randNumber, randPassword, randTimeZone, randUserName } from '@ngneat/falso'; -import { - CreateUserInput, - OauthProvider, - PrismaService, - Role, - RoleName, - RoleService, - User, - UserService, -} from '@snipcode/domain'; +import { INestApplication } from '@nestjs/common'; +import { randEmail, randFullName, randPassword } from '@ngneat/falso'; +import { PrismaService, RoleName } from '@snipcode/domain'; +import { generateJwtToken } from '@snipcode/utils'; +import request from 'supertest'; export type CreateUserInputArgs = { email: string; isEnabled: boolean; name: string; - oauthProvider: OauthProvider; - password?: string | null; - pictureUrl: string | null; + password: string | null; role: RoleName; - roleId: string; - timezone: string | null; - username: string | null; }; export class TestHelper { constructor( - private readonly prismaService: PrismaService, - private readonly roleService: RoleService, - private readonly userService: UserService, + private readonly app: INestApplication, + private readonly graphqlEndpoint: string, ) {} - static createTestUserInput(override: Partial): CreateUserInput { - const input = new CreateUserInput({ - email: randEmail(), - name: randFullName(), - oauthProvider: 'github', - password: randPassword(), - pictureUrl: randImg(), - roleId: 'roleId', - timezone: randTimeZone(), - username: randUserName(), - ...override, - }); - - input.isEnabled = Boolean(override.isEnabled ?? randNumber({ max: 1, min: 0 })); - - return input; + async cleanDatabase(): Promise { + const prismaService = this.app.get(PrismaService); + + await prismaService.snippet.deleteMany(); + await prismaService.folder.deleteMany(); + await prismaService.session.deleteMany(); + await prismaService.user.deleteMany(); } - async findTestRole(name: RoleName): Promise { - const role = await this.roleService.findByName(name); + async signupUser(input: Partial): Promise { + const query = ` + mutation SignupUser($input: SignupUserInput!) { + signupUser(input: $input) { + __typename + message + userId + } + } + `; + const variables = { + input: { + email: input.email ?? randEmail(), + name: input.name ?? randFullName(), + password: input.password ?? randPassword(), + }, + }; + + const response = await request(this.app.getHttpServer()).post(this.graphqlEndpoint).send({ query, variables }); + + if (input.isEnabled) { + const confirmationToken = generateJwtToken({ + expiresIn: '1h', + payload: { userId: response.body.data.signupUser.userId }, + secret: process.env.JWT_SECRET, + }); + + const confirmUserQuery = ` + mutation ConfirmUser($token: String!) { + confirmUser(token: $token) { + message + } + } + `; - if (!role) { - throw new Error(`Role with the name "${name}" not found!`); + const confirmUserVariables = { + token: confirmationToken, + }; + + await request(this.app.getHttpServer()) + .post(this.graphqlEndpoint) + .send({ query: confirmUserQuery, variables: confirmUserVariables }); } - return role; + return response.body.data.signupUser.userId; } - async createTestUser(input: Partial): Promise { - const role = await this.findTestRole(input.role ?? 'user'); + async createAuthenticatedUser(input: Partial): Promise<{ authToken: string; userId: string }> { + const createUserInput: Partial = { + ...input, + email: input.email ?? randEmail(), + isEnabled: input.isEnabled ?? true, + password: input.password ?? randPassword(), + }; - const createUserInput = TestHelper.createTestUserInput({ ...input, roleId: role.id }); + const userId = await this.signupUser(createUserInput); - return this.userService.create(createUserInput); - } + const query = ` + mutation LoginUser($email: String!, $password: String!) { + loginUser(email: $email, password: $password) { + token + } + } + `; - async cleanDatabase(): Promise { - await this.prismaService.snippet.deleteMany(); - await this.prismaService.folder.deleteMany(); - await this.prismaService.session.deleteMany(); - await this.prismaService.user.deleteMany(); + const variables = { + email: createUserInput.email, + password: createUserInput.password, + }; + + const response = await request(this.app.getHttpServer()).post(this.graphqlEndpoint).send({ query, variables }); + + return { + authToken: response.body.data.loginUser.token, + userId, + }; } } From a2625c3f16830ae0d71b2d09b3a89b212ca036cf Mon Sep 17 00:00:00 2001 From: Eric Cabrel TIOGO Date: Fri, 7 Jun 2024 23:31:10 +0200 Subject: [PATCH 2/6] test(backend): write integration tests on folder --- .../auth/graphql/auth.integration.spec.ts | 8 +- .../features/auth/graphql/auth.resolvers.ts | 2 - .../graphql/folder.integration.spec.ts | 175 ++++++++++++++++++ .../folders/graphql/folder.resolvers.ts | 8 +- apps/backend/src/utils/tests/helpers.ts | 102 ++++++++-- 5 files changed, 275 insertions(+), 20 deletions(-) create mode 100644 apps/backend/src/features/folders/graphql/folder.integration.spec.ts diff --git a/apps/backend/src/features/auth/graphql/auth.integration.spec.ts b/apps/backend/src/features/auth/graphql/auth.integration.spec.ts index f9e7207e..93d6fdcd 100644 --- a/apps/backend/src/features/auth/graphql/auth.integration.spec.ts +++ b/apps/backend/src/features/auth/graphql/auth.integration.spec.ts @@ -205,7 +205,7 @@ describe('Test Authentication', () => { password: 'password', }; - const { authToken, userId } = await testHelper.createAuthenticatedUser({ ...input }); + const { authToken, user } = await testHelper.createAuthenticatedUser({ ...input }); const authenticatedUserQuery = ` query AuthenticatedUser { @@ -241,7 +241,7 @@ describe('Test Authentication', () => { expect(authenticatedUser).toMatchObject({ createdAt: expect.any(Number), email: input.email, - id: userId, + id: user.id, isEnabled: true, name: input.name, oauthProvider: 'email', @@ -259,7 +259,7 @@ describe('Test Authentication', () => { }); test('Log out the authenticated user', async () => { - const { authToken, userId } = await testHelper.createAuthenticatedUser({ + const { authToken, user } = await testHelper.createAuthenticatedUser({ email: 'jane.doe@snipcode.dev', name: 'Jane Doe', password: 'password', @@ -281,7 +281,7 @@ describe('Test Authentication', () => { const { authenticatedUser } = response.body.data; - expect(authenticatedUser.id).toEqual(userId); + expect(authenticatedUser.id).toEqual(user.id); const logoutQuery = ` mutation LogoutUser { diff --git a/apps/backend/src/features/auth/graphql/auth.resolvers.ts b/apps/backend/src/features/auth/graphql/auth.resolvers.ts index b66ceda4..2d2f1cfb 100644 --- a/apps/backend/src/features/auth/graphql/auth.resolvers.ts +++ b/apps/backend/src/features/auth/graphql/auth.resolvers.ts @@ -69,8 +69,6 @@ export class AuthResolvers { @Mutation('logoutUser') @UseGuards(AuthGuard) async logoutUser(@UserId() userId: string | undefined): Promise { - console.log('user logged out', userId); - if (!userId) { return false; } diff --git a/apps/backend/src/features/folders/graphql/folder.integration.spec.ts b/apps/backend/src/features/folders/graphql/folder.integration.spec.ts new file mode 100644 index 00000000..c05126d0 --- /dev/null +++ b/apps/backend/src/features/folders/graphql/folder.integration.spec.ts @@ -0,0 +1,175 @@ +import request from 'supertest'; + +import { TestHelper } from '../../../utils/tests/helpers'; +import { TestServer, startTestServer } from '../../../utils/tests/server'; + +const graphqlEndpoint = '/graphql'; + +describe('Test Folder', () => { + let server: TestServer; + let testHelper: TestHelper; + + beforeAll(async () => { + server = await startTestServer(); + + testHelper = new TestHelper(server.app, graphqlEndpoint); + }); + + beforeEach(async () => { + await testHelper.cleanDatabase(); + }); + + afterAll(async () => { + await server.close(); + }); + + test("Fail to create a folder when the parent folder doesn't exist", async () => { + const { authToken } = await testHelper.createAuthenticatedUser({}); + + const query = ` + mutation CreateFolder($input: CreateFolderInput!) { + createFolder(input: $input) { + id + } + } + `; + + const variables = { + input: { + name: 'My First Folder', + parentId: 'non-existent-folder-id', + }, + }; + + const response = await request(server.app.getHttpServer()) + .post(graphqlEndpoint) + .set('Authorization', authToken) + .send({ query, variables }) + .expect(200); + + const [error] = response.body.errors; + + expect(error.extensions.code).toEqual('FOLDER_NOT_FOUND'); + expect(error.message).toEqual('The folder with the id non-existent-folder-id not found'); + }); + + test('Fail to create a folder when a folder with the same name already exists in the parent folder', async () => { + const { authToken, user } = await testHelper.createAuthenticatedUser({}); + + await testHelper.createFolder(authToken, { + name: 'My First Folder', + parentId: user.rootFolderId, + }); + + const query = ` + mutation CreateFolder($input: CreateFolderInput!) { + createFolder(input: $input) { + id + } + } + `; + + const variables = { + input: { + name: 'My First Folder', + parentId: user.rootFolderId, + }, + }; + + const response = await request(server.app.getHttpServer()) + .post(graphqlEndpoint) + .set('Authorization', authToken) + .send({ query, variables }) + .expect(200); + + const [error] = response.body.errors; + + expect(error.extensions.code).toEqual('FOLDER_ALREADY_EXIST'); + expect(error.message).toEqual('A folder named "My First Folder" already exists'); + }); + + // eslint-disable-next-line jest/no-disabled-tests + test.skip("Fail to create a folder when the parent folder doesn't belong to the authenticated user", async () => { + const { authToken } = await testHelper.createAuthenticatedUser({}); + const { user: user2 } = await testHelper.createAuthenticatedUser({}); + + const query = ` + mutation CreateFolder($input: CreateFolderInput!) { + createFolder(input: $input) { + id + } + } + `; + + const variables = { + input: { + name: 'My First Folder', + parentId: user2.rootFolderId, + }, + }; + + const response = await request(server.app.getHttpServer()) + .post(graphqlEndpoint) + .set('Authorization', authToken) + .send({ query, variables }) + .expect(200); + + const [error] = response.body.errors; + + expect(error.extensions.code).toEqual('FOLDER_NOT_BELONG_TO_USER'); + expect(error.message).toEqual( + `The folder with the id ${user2.rootFolderId} does not belong to the authenticated user`, + ); + }); + + test('Successfully create a folder', async () => { + const { authToken, user } = await testHelper.createAuthenticatedUser({}); + + const query = ` + mutation CreateFolder($input: CreateFolderInput!) { + createFolder(input: $input) { + __typename + id + name + isFavorite + subFolders { + id + } + subFoldersCount + parent { + id + } + user { + id + } + } + } + `; + + const variables = { + input: { + name: 'My First Folder', + parentId: user.rootFolderId, + }, + }; + + const response = await request(server.app.getHttpServer()) + .post(graphqlEndpoint) + .set('Authorization', authToken) + .send({ query, variables }) + .expect(200); + + const { createFolder } = response.body.data; + + expect(createFolder).toMatchObject({ + __typename: 'Folder', + id: expect.any(String), + isFavorite: false, + name: 'My First Folder', + parent: { id: user.rootFolderId }, + subFolders: [], + subFoldersCount: 0, + user: { id: user.id }, + }); + }); +}); diff --git a/apps/backend/src/features/folders/graphql/folder.resolvers.ts b/apps/backend/src/features/folders/graphql/folder.resolvers.ts index fa16251f..08c93a4f 100644 --- a/apps/backend/src/features/folders/graphql/folder.resolvers.ts +++ b/apps/backend/src/features/folders/graphql/folder.resolvers.ts @@ -93,8 +93,12 @@ export class FolderResolvers { } @ResolveField() - async parent(@Parent() folder: Folder): Promise { - return this.folderService.findById(folder.id); + async parent(@Parent() folder: Folder): Promise { + if (!folder.parentId) { + return null; + } + + return this.folderService.findById(folder.parentId); } @ResolveField() diff --git a/apps/backend/src/utils/tests/helpers.ts b/apps/backend/src/utils/tests/helpers.ts index efcf5076..e74f7abb 100644 --- a/apps/backend/src/utils/tests/helpers.ts +++ b/apps/backend/src/utils/tests/helpers.ts @@ -1,16 +1,30 @@ import { INestApplication } from '@nestjs/common'; -import { randEmail, randFullName, randPassword } from '@ngneat/falso'; +import { randEmail, randFullName, randPassword, randWord } from '@ngneat/falso'; import { PrismaService, RoleName } from '@snipcode/domain'; import { generateJwtToken } from '@snipcode/utils'; import request from 'supertest'; -export type CreateUserInputArgs = { +type CreateUserInputArgs = { email: string; isEnabled: boolean; name: string; password: string | null; role: RoleName; }; + +type CreateAuthenticatedUserResult = { + authToken: string; + user: { + id: string; + rootFolderId: string; + }; +}; + +type CreateFolderArgs = { + name: string; + parentId: string; +}; + export class TestHelper { constructor( private readonly app: INestApplication, @@ -21,8 +35,15 @@ export class TestHelper { const prismaService = this.app.get(PrismaService); await prismaService.snippet.deleteMany(); + + const childFolders = await prismaService.folder.findMany({ where: { NOT: { parent: null } } }); + + await prismaService.folder.deleteMany({ where: { id: { in: childFolders.map((folder) => folder.id) } } }); + await prismaService.folder.deleteMany(); + await prismaService.session.deleteMany(); + await prismaService.user.deleteMany(); } @@ -73,17 +94,17 @@ export class TestHelper { return response.body.data.signupUser.userId; } - async createAuthenticatedUser(input: Partial): Promise<{ authToken: string; userId: string }> { + async createAuthenticatedUser(args: Partial): Promise { const createUserInput: Partial = { - ...input, - email: input.email ?? randEmail(), - isEnabled: input.isEnabled ?? true, - password: input.password ?? randPassword(), + ...args, + email: args.email ?? randEmail(), + isEnabled: args.isEnabled ?? true, + password: args.password ?? randPassword(), }; - const userId = await this.signupUser(createUserInput); + await this.signupUser(createUserInput); - const query = ` + const loginQuery = ` mutation LoginUser($email: String!, $password: String!) { loginUser(email: $email, password: $password) { token @@ -96,11 +117,68 @@ export class TestHelper { password: createUserInput.password, }; - const response = await request(this.app.getHttpServer()).post(this.graphqlEndpoint).send({ query, variables }); + const loginResponse = await request(this.app.getHttpServer()) + .post(this.graphqlEndpoint) + .send({ query: loginQuery, variables }); + + const authToken = loginResponse.body.data.loginUser.token; + + const authenticatedUserQuery = ` + query AuthenticatedUser { + authenticatedUser { + id + name + rootFolder { + id + name + } + } + } + `; + + const authenticatedUserResponse = await request(this.app.getHttpServer()) + .post(this.graphqlEndpoint) + .set('Authorization', authToken) + .send({ query: authenticatedUserQuery }) + .expect(200); + + const { authenticatedUser } = authenticatedUserResponse.body.data; return { - authToken: response.body.data.loginUser.token, - userId, + authToken, + user: { + id: authenticatedUser.id, + rootFolderId: authenticatedUser.rootFolder.id, + }, + }; + } + + async createFolder(authToken: string, args: Partial): Promise { + const createFolderInput: Partial = { + ...args, + name: args.name ?? randWord(), + }; + + const query = ` + mutation CreateFolder($input: CreateFolderInput!) { + createFolder(input: $input) { + id + } + } + `; + + const variables = { + input: { + name: createFolderInput.name, + parentId: createFolderInput.parentId, + }, }; + + const response = await request(this.app.getHttpServer()) + .post(this.graphqlEndpoint) + .set('Authorization', authToken) + .send({ query, variables }); + + return response.body.data.createFolder.id; } } From 3c844844f7728b73267a63184b66c6c15a9124f0 Mon Sep 17 00:00:00 2001 From: Eric Cabrel TIOGO Date: Sun, 9 Jun 2024 05:39:40 +0200 Subject: [PATCH 3/6] test(backend): write integration tests on snippet --- .../graphql/snippet.integration.spec.ts | 352 ++++++++++++++++++ apps/backend/src/utils/tests/helpers.ts | 50 ++- .../services/snippets/snippet.service.test.ts | 16 + .../src/services/snippets/snippet.service.ts | 6 + packages/domain/tests/helpers.ts | 14 + packages/utils/src/error/messages.ts | 8 +- 6 files changed, 437 insertions(+), 9 deletions(-) create mode 100644 apps/backend/src/features/snippets/graphql/snippet.integration.spec.ts diff --git a/apps/backend/src/features/snippets/graphql/snippet.integration.spec.ts b/apps/backend/src/features/snippets/graphql/snippet.integration.spec.ts new file mode 100644 index 00000000..e4ca4cc5 --- /dev/null +++ b/apps/backend/src/features/snippets/graphql/snippet.integration.spec.ts @@ -0,0 +1,352 @@ +import request from 'supertest'; + +import { TestHelper } from '../../../utils/tests/helpers'; +import { TestServer, startTestServer } from '../../../utils/tests/server'; + +const graphqlEndpoint = '/graphql'; + +describe('Test Snippet Feature', () => { + let server: TestServer; + let testHelper: TestHelper; + + beforeAll(async () => { + server = await startTestServer(); + + testHelper = new TestHelper(server.app, graphqlEndpoint); + }); + + beforeEach(async () => { + await testHelper.cleanDatabase(); + }); + + afterAll(async () => { + await server.close(); + }); + + test('Fail to create a snippet when the folder does not exist', async () => { + const { authToken } = await testHelper.createAuthenticatedUser({}); + const query = ` + mutation CreateSnippet($input: CreateSnippetInput!) { + createSnippet(input: $input) { + id + } + } + `; + + const variables = { + input: { + content: 'const a = 1;', + contentHighlighted: 'const a = 1;', + description: 'This is a description', + folderId: 'non-existent-folder-id', + language: 'javascript', + lineHighlight: '[]', + name: 'Snippet 1', + theme: 'github-dark-dimmed', + visibility: 'public', + }, + }; + + const response = await request(server.app.getHttpServer()) + .post(graphqlEndpoint) + .set('Authorization', authToken) + .send({ query, variables }) + .expect(200); + + const [error] = response.body.errors; + + expect(error.extensions.code).toEqual('FOLDER_NOT_FOUND'); + expect(error.message).toEqual('The folder with the id "non-existent-folder-id" not found'); + }); + + test('Fail to create a snippet when a snippet with the same name already exists in the folder', async () => { + const { authToken, user } = await testHelper.createAuthenticatedUser({}); + const folderId = await testHelper.createFolder(authToken, { name: 'JS Code', parentId: user.rootFolderId }); + + await testHelper.createSnippet(authToken, { folderId, name: 'code-snippet.js' }); + + const query = ` + mutation CreateSnippet($input: CreateSnippetInput!) { + createSnippet(input: $input) { + id + } + } + `; + + const variables = { + input: { + content: 'const a = 1;', + contentHighlighted: 'const a = 1;', + description: 'This is a description', + folderId, + language: 'javascript', + lineHighlight: '[]', + name: 'code-snippet.js', + theme: 'github-dark-dimmed', + visibility: 'public', + }, + }; + + const response = await request(server.app.getHttpServer()) + .post(graphqlEndpoint) + .set('Authorization', authToken) + .send({ query, variables }) + .expect(200); + + const [error] = response.body.errors; + + expect(error.extensions.code).toEqual('SNIPPET_ALREADY_EXIST'); + expect(error.message).toEqual('A snippet named "code-snippet.js" already exists'); + }); + + test('Create a snippet', async () => { + const { authToken, user } = await testHelper.createAuthenticatedUser({}); + const folderId = await testHelper.createFolder(authToken, { name: 'JS Code', parentId: user.rootFolderId }); + + const query = ` + mutation CreateSnippet($input: CreateSnippetInput!) { + createSnippet(input: $input) { + id + name + content + contentHighlighted + language + lineHighlight + size + visibility + description + theme + createdAt + updatedAt + folder { + id + } + user { + id + } + } + } + `; + + const variables = { + input: { + content: 'const a = 1;', + contentHighlighted: 'const a = 1;', + description: 'This is a description', + folderId, + language: 'javascript', + lineHighlight: '[]', + name: 'code-snippet.js', + theme: 'github-dark-dimmed', + visibility: 'public', + }, + }; + + const response = await request(server.app.getHttpServer()) + .post(graphqlEndpoint) + .set('Authorization', authToken) + .send({ query, variables }) + .expect(200); + + const { createSnippet } = response.body.data; + + expect(createSnippet).toMatchObject({ + content: 'const a = 1;', + contentHighlighted: 'const a = 1;', + createdAt: expect.any(Number), + description: 'This is a description', + folder: { id: folderId }, + language: 'javascript', + lineHighlight: '[]', + name: 'code-snippet.js', + size: expect.any(Number), + theme: 'github-dark-dimmed', + updatedAt: expect.any(Number), + user: { id: user.id }, + visibility: 'public', + }); + }); + + test('Retrieve snippets of the authenticated user', async () => { + const { authToken: user1AuthToken, user: user1 } = await testHelper.createAuthenticatedUser({}); + const { authToken: user2AuthToken, user: user2 } = await testHelper.createAuthenticatedUser({}); + + const user1FolderId = await testHelper.createFolder(user1AuthToken, { + name: 'TS Code', + parentId: user1.rootFolderId, + }); + const user2FolderId = await testHelper.createFolder(user2AuthToken, { + name: 'JS Code', + parentId: user2.rootFolderId, + }); + + await testHelper.createSnippet(user1AuthToken, { folderId: user1FolderId, name: 'code-snippet.ts' }); + const user2SnippetId = await testHelper.createSnippet(user2AuthToken, { + folderId: user2FolderId, + name: 'code-snippet.js', + }); + + const query = ` + query MySnippets { + mySnippets { + id + name + user { + id + } + } + } + `; + + const response = await request(server.app.getHttpServer()) + .post(graphqlEndpoint) + .set('Authorization', user2AuthToken) + .send({ query }) + .expect(200); + + const { mySnippets } = response.body.data; + + expect(mySnippets).toHaveLength(1); + expect(mySnippets[0]).toMatchObject({ + id: user2SnippetId, + name: 'code-snippet.js', + user: { id: user2.id }, + }); + }); + + test('Retrieve a snippet', async () => { + const { authToken, user } = await testHelper.createAuthenticatedUser({}); + + const folderId = await testHelper.createFolder(authToken, { + name: 'All Codes', + parentId: user.rootFolderId, + }); + + const subFolderId = await testHelper.createFolder(authToken, { name: 'TS Code', parentId: folderId }); + + const snippetId = await testHelper.createSnippet(authToken, { folderId: subFolderId, name: 'code-snippet.ts' }); + + const query = ` + query findSnippet($snippetId: String!) { + findSnippet(snippetId: $snippetId) { + snippet { + id + name + user { + id + } + } + paths { + id + name + } + } + } + `; + + const variables = { snippetId }; + + const response = await request(server.app.getHttpServer()) + .post(graphqlEndpoint) + .set('Authorization', authToken) + .send({ query, variables }) + .expect(200); + + const { findSnippet } = response.body.data; + + expect(findSnippet).toMatchObject({ + paths: [ + { id: folderId, name: 'All Codes' }, + { id: subFolderId, name: 'TS Code' }, + ], + snippet: { + id: snippetId, + name: 'code-snippet.ts', + user: { id: user.id }, + }, + }); + }); + + test('Retrieve public snippets', async () => { + const { authToken: user1AuthToken, user: user1 } = await testHelper.createAuthenticatedUser({}); + const { authToken: user2AuthToken, user: user2 } = await testHelper.createAuthenticatedUser({}); + + const user1FolderId = await testHelper.createFolder(user1AuthToken, { + name: 'TS Code', + parentId: user1.rootFolderId, + }); + const user2FolderId = await testHelper.createFolder(user2AuthToken, { + name: 'JS Code', + parentId: user2.rootFolderId, + }); + + const user1SnippetId = await testHelper.createSnippet(user1AuthToken, { + folderId: user1FolderId, + name: 'code-snippet.ts', + }); + + await testHelper.createSnippet(user1AuthToken, { + folderId: user1FolderId, + name: 'code-snippet-private.ts', + visibility: 'private', + }); + const user2SnippetId = await testHelper.createSnippet(user2AuthToken, { + folderId: user2FolderId, + name: 'code-snippet.js', + }); + + await testHelper.createSnippet(user2AuthToken, { + folderId: user2FolderId, + name: 'code-snippet-private.js', + visibility: 'private', + }); + + const query = ` + query PublicSnippets($input: PublicSnippetsInput!) { + publicSnippets(input: $input) { + items { + id + name + user { + id + } + } + hasMore + itemPerPage + nextToken + } + } + `; + + const variables = { + input: { + itemPerPage: 10, + keyword: null, + nextToken: null, + sortMethod: 'recently_created', + }, + }; + + const response = await request(server.app.getHttpServer()) + .post(graphqlEndpoint) + .set('Authorization', user2AuthToken) + .send({ query, variables }); + + const { publicSnippets } = response.body.data; + + expect(publicSnippets).toHaveProperty('hasMore', false); + expect(publicSnippets).toHaveProperty('itemPerPage', 10); + expect(publicSnippets).toHaveProperty('nextToken', null); + expect(publicSnippets.items).toHaveLength(2); + expect(publicSnippets.items[0]).toMatchObject({ + id: user2SnippetId, + name: 'code-snippet.js', + user: { id: user2.id }, + }); + expect(publicSnippets.items[1]).toMatchObject({ + id: user1SnippetId, + name: 'code-snippet.ts', + user: { id: user1.id }, + }); + }); +}); diff --git a/apps/backend/src/utils/tests/helpers.ts b/apps/backend/src/utils/tests/helpers.ts index e74f7abb..822c5bec 100644 --- a/apps/backend/src/utils/tests/helpers.ts +++ b/apps/backend/src/utils/tests/helpers.ts @@ -25,6 +25,12 @@ type CreateFolderArgs = { parentId: string; }; +type CreateSnippetArgs = { + folderId: string; + name: string; + visibility: 'public' | 'private'; +}; + export class TestHelper { constructor( private readonly app: INestApplication, @@ -36,11 +42,8 @@ export class TestHelper { await prismaService.snippet.deleteMany(); - const childFolders = await prismaService.folder.findMany({ where: { NOT: { parent: null } } }); - - await prismaService.folder.deleteMany({ where: { id: { in: childFolders.map((folder) => folder.id) } } }); - - await prismaService.folder.deleteMany(); + // Recursive relationship between folders makes it hard to delete all folders using folder.deleteMany() + await prismaService.$executeRaw`TRUNCATE TABLE folders;`; await prismaService.session.deleteMany(); @@ -181,4 +184,41 @@ export class TestHelper { return response.body.data.createFolder.id; } + + async createSnippet(authToken: string, args: Partial): Promise { + const createSnippetInput: Partial = { + ...args, + name: args.name ?? randWord(), + visibility: args.visibility ?? 'public', + }; + + const query = ` + mutation CreateSnippet($input: CreateSnippetInput!) { + createSnippet(input: $input) { + id + } + } + `; + + const variables = { + input: { + content: 'const a = 1;', + contentHighlighted: 'const a = 1;', + description: 'This is a description', + folderId: createSnippetInput.folderId, + language: 'javascript', + lineHighlight: '[]', + name: createSnippetInput.name, + theme: 'github-dark-dimmed', + visibility: createSnippetInput.visibility, + }, + }; + + const response = await request(this.app.getHttpServer()) + .post(this.graphqlEndpoint) + .set('Authorization', authToken) + .send({ query, variables }); + + return response.body.data.createSnippet.id; + } } diff --git a/packages/domain/src/services/snippets/snippet.service.test.ts b/packages/domain/src/services/snippets/snippet.service.test.ts index 8408a11f..64717db5 100644 --- a/packages/domain/src/services/snippets/snippet.service.test.ts +++ b/packages/domain/src/services/snippets/snippet.service.test.ts @@ -30,6 +30,7 @@ describe('Test Snippet service', () => { testHelper = new TestHelper(prismaService); + await testHelper.cleanDatabase(); await roleService.loadRoles(); }); @@ -64,6 +65,21 @@ describe('Test Snippet service', () => { await testHelper.deleteTestUsersById([user.id]); }); + it('should not create a snippet because the specified folder does not exist', async () => { + // GIVEN + const [user, rootFolder] = await testHelper.createUserWithRootFolder(); + const createSnippetInput = TestHelper.createTestSnippetInput({ folderId: generateRandomId(), userId: user.id }); + + // WHEN + // THEN + await expect(async () => { + await snippetService.create(createSnippetInput); + }).rejects.toThrow(new AppError(errors.FOLDER_NOT_FOUND(createSnippetInput.folderId), 'FOLDER_NOT_FOUND')); + + await testHelper.deleteTestFoldersById([rootFolder.id]); + await testHelper.deleteTestUsersById([user.id]); + }); + it('should not create a snippet because it already exists in the specified folder', async () => { // GIVEN const [user, rootFolder] = await testHelper.createUserWithRootFolder(); diff --git a/packages/domain/src/services/snippets/snippet.service.ts b/packages/domain/src/services/snippets/snippet.service.ts index 6276aae6..94973e5d 100644 --- a/packages/domain/src/services/snippets/snippet.service.ts +++ b/packages/domain/src/services/snippets/snippet.service.ts @@ -19,6 +19,12 @@ export class SnippetService { constructor(private readonly prisma: PrismaService) {} async create(createSnippetInput: CreateSnippetInput): Promise { + const folder = await this.prisma.folder.findUnique({ where: { id: createSnippetInput.folderId } }); + + if (!folder) { + throw new AppError(errors.FOLDER_NOT_FOUND(createSnippetInput.folderId), 'FOLDER_NOT_FOUND'); + } + const isSnippetExist = await this.isSnippetExistInFolder(createSnippetInput.folderId, createSnippetInput.name); if (isSnippetExist) { diff --git a/packages/domain/tests/helpers.ts b/packages/domain/tests/helpers.ts index d0d55ef8..24d1a2db 100644 --- a/packages/domain/tests/helpers.ts +++ b/packages/domain/tests/helpers.ts @@ -44,6 +44,20 @@ type CreateTestUserArgs = { export class TestHelper { constructor(private readonly prisma: PrismaService) {} + async cleanDatabase(): Promise { + await this.prisma.snippet.deleteMany(); + + const childFolders = await this.prisma.folder.findMany({ where: { NOT: { parent: null } } }); + + await this.prisma.folder.deleteMany({ where: { id: { in: childFolders.map((folder) => folder.id) } } }); + + await this.prisma.folder.deleteMany(); + + await this.prisma.session.deleteMany(); + + await this.prisma.user.deleteMany(); + } + static createTestUserInput({ email, isEnabled, diff --git a/packages/utils/src/error/messages.ts b/packages/utils/src/error/messages.ts index 39f41a26..1aba17fe 100644 --- a/packages/utils/src/error/messages.ts +++ b/packages/utils/src/error/messages.ts @@ -13,12 +13,12 @@ export const LOGIN_FAILED = 'Invalid email address or password.'; export const EMAIL_ALREADY_TAKEN = 'The email address is already taken'; export const USERNAME_ALREADY_TAKEN = 'The username is already taken'; export const ACCOUNT_DISABLED = 'Your account is disabled!'; -export const FOLDER_NOT_FOUND = (folderId: string) => `The folder with the id ${folderId} not found`; -export const SNIPPET_NOT_FOUND = (snippetId: string) => `The folder with the id ${snippetId} not found`; +export const FOLDER_NOT_FOUND = (folderId: string) => `The folder with the id "${folderId}" not found`; +export const SNIPPET_NOT_FOUND = (snippetId: string) => `The folder with the id "${snippetId}" not found`; export const CANT_EDIT_SNIPPET = (userId: string, snippetId: string) => - `The user with id ${userId} cannot edit the snippet ${snippetId}`; + `The user with id "${userId}" cannot edit the snippet "${snippetId}"`; export const CANT_EDIT_FOLDER = (userId: string, folderId: string) => - `The user with id ${userId} cannot edit the folder ${folderId}`; + `The user with id "${userId}" cannot edit the folder "${folderId}"`; export const CANT_RENAME_ROOT_FOLDER = 'The root folder cannot be renamed.'; export const INVALID_CONFIRMATION_TOKEN = 'Invalid confirmation token'; export const USER_NOT_FOUND_FROM_TOKEN = 'No user associated with this token'; From 599373c6f710f52bf82ca1c903d264345c875d3a Mon Sep 17 00:00:00 2001 From: Eric Cabrel TIOGO Date: Sun, 9 Jun 2024 09:09:28 +0200 Subject: [PATCH 4/6] test(backend): more integration tests on folder --- .../graphql/folder.integration.spec.ts | 119 +++++++++++++++++- .../services/folders/folder.service.test.ts | 103 ++++++--------- .../src/services/folders/folder.service.ts | 11 +- packages/utils/src/error/error.ts | 3 +- packages/utils/src/error/messages.ts | 2 + 5 files changed, 162 insertions(+), 76 deletions(-) diff --git a/apps/backend/src/features/folders/graphql/folder.integration.spec.ts b/apps/backend/src/features/folders/graphql/folder.integration.spec.ts index c05126d0..6325b3f9 100644 --- a/apps/backend/src/features/folders/graphql/folder.integration.spec.ts +++ b/apps/backend/src/features/folders/graphql/folder.integration.spec.ts @@ -5,7 +5,7 @@ import { TestServer, startTestServer } from '../../../utils/tests/server'; const graphqlEndpoint = '/graphql'; -describe('Test Folder', () => { +describe('Test Folder Feature', () => { let server: TestServer; let testHelper: TestHelper; @@ -50,7 +50,7 @@ describe('Test Folder', () => { const [error] = response.body.errors; expect(error.extensions.code).toEqual('FOLDER_NOT_FOUND'); - expect(error.message).toEqual('The folder with the id non-existent-folder-id not found'); + expect(error.message).toEqual('The folder with the id "non-existent-folder-id" not found'); }); test('Fail to create a folder when a folder with the same name already exists in the parent folder', async () => { @@ -88,8 +88,7 @@ describe('Test Folder', () => { expect(error.message).toEqual('A folder named "My First Folder" already exists'); }); - // eslint-disable-next-line jest/no-disabled-tests - test.skip("Fail to create a folder when the parent folder doesn't belong to the authenticated user", async () => { + test("Fail to create a folder when the parent folder doesn't belong to the authenticated user", async () => { const { authToken } = await testHelper.createAuthenticatedUser({}); const { user: user2 } = await testHelper.createAuthenticatedUser({}); @@ -116,7 +115,7 @@ describe('Test Folder', () => { const [error] = response.body.errors; - expect(error.extensions.code).toEqual('FOLDER_NOT_BELONG_TO_USER'); + expect(error.extensions.code).toEqual('FOLDER_NOT_BELONGING_TO_USER'); expect(error.message).toEqual( `The folder with the id ${user2.rootFolderId} does not belong to the authenticated user`, ); @@ -172,4 +171,114 @@ describe('Test Folder', () => { user: { id: user.id }, }); }); + + test('Display the directory of a user', async () => { + const { authToken, user } = await testHelper.createAuthenticatedUser({}); + const blogFolderId = await testHelper.createFolder(authToken, { + name: 'Blog', + parentId: user.rootFolderId, + }); + + const blogPostFolderId = await testHelper.createFolder(authToken, { + name: 'Blog post 1', + parentId: blogFolderId, + }); + + const codingFolderId = await testHelper.createFolder(authToken, { + name: 'Coding', + parentId: user.rootFolderId, + }); + + const snippetId = await testHelper.createSnippet(authToken, { + folderId: user.rootFolderId, + name: 'My first snippet', + }); + + const blogSnippetId = await testHelper.createSnippet(authToken, { + folderId: blogFolderId, + name: 'auth.ts', + }); + + const query = ` + query ListDirectory($folderId: String!) { + listDirectory(folderId: $folderId) { + folders { + id + name + } + paths { + id + name + } + snippets { + id + name + } + } + } + `; + + const variables = { + folderId: user.rootFolderId, + }; + + const response = await request(server.app.getHttpServer()) + .post(graphqlEndpoint) + .set('Authorization', authToken) + .send({ query, variables }) + .expect(200); + + const { listDirectory } = response.body.data; + + expect(listDirectory).toMatchObject({ + folders: [ + { + id: blogFolderId, + name: 'Blog', + }, + { + id: codingFolderId, + name: 'Coding', + }, + ], + paths: [], + snippets: [ + { + id: snippetId, + name: 'My first snippet', + }, + ], + }); + + const subFolderVariables = { + folderId: blogFolderId, + }; + + const subFolderResponse = await request(server.app.getHttpServer()) + .post(graphqlEndpoint) + .set('Authorization', authToken) + .send({ query, variables: subFolderVariables }) + .expect(200); + + expect(subFolderResponse.body.data.listDirectory).toMatchObject({ + folders: [ + { + id: blogPostFolderId, + name: 'Blog post 1', + }, + ], + paths: [ + { + id: blogFolderId, + name: 'Blog', + }, + ], + snippets: [ + { + id: blogSnippetId, + name: 'auth.ts', + }, + ], + }); + }); }); diff --git a/packages/domain/src/services/folders/folder.service.test.ts b/packages/domain/src/services/folders/folder.service.test.ts index fa09f411..5e93f9c7 100644 --- a/packages/domain/src/services/folders/folder.service.test.ts +++ b/packages/domain/src/services/folders/folder.service.test.ts @@ -35,24 +35,20 @@ describe('Test Folder service', () => { await roleService.loadRoles(); }); - it('should create a root folder for a user', async () => { - // GIVEN + test('Create a root folder for a user', async () => { const user = await testHelper.createTestUser({}); const creatUserRootFolderInput = new CreateUserRootFolderInput(user.id); - // WHEN const expectedFolder = await folderService.createUserRootFolder(creatUserRootFolderInput); - // THEN expect(expectedFolder?.id).toEqual(creatUserRootFolderInput.toFolder().id); await testHelper.deleteTestFoldersById([expectedFolder?.id]); await testHelper.deleteTestUsersById([user.id]); }); - it('should create folder for the specified user', async () => { - // GIVEN + test('Create folder for the specified user', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const createFolderInput = new CreateFolderInput({ @@ -61,10 +57,8 @@ describe('Test Folder service', () => { userId: user.id, }); - // WHEN const expectedFolder = await folderService.create(createFolderInput); - // THEN expect(expectedFolder).toMatchObject({ id: createFolderInput.toFolder().id, isFavorite: false, @@ -78,8 +72,7 @@ describe('Test Folder service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should not create the folder cause it already exists', async () => { - // GIVEN + test('Can not create the folder because a folder with the same name already exists', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const [firstFolder, secondFolder] = await testHelper.createManyTestFolders({ @@ -94,8 +87,6 @@ describe('Test Folder service', () => { userId: user.id, }); - // WHEN - // THEN await expect(() => folderService.create(createFolderInput)).rejects.toThrow( new AppError(errors.FOLDER_ALREADY_EXIST(createFolderInput.name), 'FOLDER_ALREADY_EXIST'), ); @@ -104,8 +95,25 @@ describe('Test Folder service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it("should find the user's root folder", async () => { - // GIVEN + test("Can not create a folder when the parent folder doesn't belong to the current creator", async () => { + const [user1] = await testHelper.createUserWithRootFolder(); + const [, rootFolder2] = await testHelper.createUserWithRootFolder(); + + const createFolderInput = new CreateFolderInput({ + name: 'My gist', + parentId: rootFolder2.id, + userId: user1.id, + }); + + await expect(() => folderService.create(createFolderInput)).rejects.toThrow( + new AppError( + errors.FOLDER_NOT_BELONGING_TO_USER(createFolderInput.parentFolderId), + 'FOLDER_NOT_BELONGING_TO_USER', + ), + ); + }); + + test("Retrieve the user's root folder", async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const [firstFolder, secondFolder] = await testHelper.createManyTestFolders({ @@ -114,22 +122,17 @@ describe('Test Folder service', () => { userId: user.id, }); - // WHEN const userRootFolder = await folderService.findUserRootFolder(user.id); - // THEN expect(userRootFolder?.name).toEqual(`__${user.id}__`); await testHelper.deleteTestFoldersById([firstFolder.id, secondFolder.id, rootFolder.id]); await testHelper.deleteTestUsersById([user.id]); }); - it("should not find the user's root folder", async () => { - // GIVEN + test("Can not retrieve the user's root folder because it doesn't exist", async () => { const user = await testHelper.createTestUser({}); - // WHEN - // THEN await expect(() => folderService.findUserRootFolder(user.id)).rejects.toThrow( new AppError(errors.USER_ROOT_FOLDER_NOT_FOUND(user.id), 'USER_ROOT_FOLDER_NOT_FOUND'), ); @@ -137,8 +140,7 @@ describe('Test Folder service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should find sub folders of the root user folder', async () => { - // GIVEN + test('Retrieve sub folders of the root user folder', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const [gistFolder, blogsFolder] = await testHelper.createManyTestFolders({ @@ -153,11 +155,9 @@ describe('Test Folder service', () => { userId: user.id, }); - // WHEN const userRootFolders1 = await folderService.findSubFolders(user.id); const userRootFolders2 = await folderService.findSubFolders(user.id, rootFolder.id); - // THEN expect(userRootFolders1).toHaveLength(2); expect(userRootFolders1).toEqual(userRootFolders2); @@ -171,8 +171,7 @@ describe('Test Folder service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should find the sub folders of a folder', async () => { - // GIVEN + test('Retrieve the sub folders of a folder', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const [firstFolder, secondFolder] = await testHelper.createManyTestFolders({ @@ -193,10 +192,8 @@ describe('Test Folder service', () => { userId: user.id, }); - // WHEN const subFolders = await folderService.findSubFolders(user.id, firstFolder.id); - // THEN expect(subFolders).toHaveLength(2); expect(subFolders.map((folder) => folder.name)).toEqual(['java', 'node.js']); @@ -212,8 +209,7 @@ describe('Test Folder service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should delete folders belonging to the user', async () => { - // GIVEN + test('Delete folders belonging to the user', async () => { const [user1, rootFolder1] = await testHelper.createUserWithRootFolder(); const [myGistFolder, blogsFolder] = await testHelper.createManyTestFolders({ folderNames: ['My gist', 'Blogs'], @@ -228,11 +224,9 @@ describe('Test Folder service', () => { userId: user2.id, }); - // WHEN await folderService.deleteMany([myGistFolder.id, blogsFolder.id], user1.id); const subFolders = await folderService.findSubFolders(user1.id); - // THEN expect(subFolders).toHaveLength(0); await testHelper.deleteTestFoldersById([projectFolder.id, snippetFolder.id, rootFolder1.id, rootFolder2.id]); @@ -240,8 +234,7 @@ describe('Test Folder service', () => { await testHelper.deleteTestUsersById([user2.id]); }); - it('should delete folders belonging to the user - validation check', async () => { - // GIVEN + test('Delete folders belonging to the user - validation check', async () => { const [user1, rootFolder1] = await testHelper.createUserWithRootFolder(); const [myGistFolder, blogsFolder] = await testHelper.createManyTestFolders({ folderNames: ['My gist', 'Blogs'], @@ -256,10 +249,8 @@ describe('Test Folder service', () => { userId: user2.id, }); - // WHEN await folderService.deleteMany([myGistFolder.id, projectFolder.id], user1.id); - // THEN const user1SubFolders = await folderService.findSubFolders(user1.id); const user2SubFolders = await folderService.findSubFolders(user2.id); @@ -280,8 +271,7 @@ describe('Test Folder service', () => { await testHelper.deleteTestUsersById([user2.id]); }); - it('should not delete folders because we cannot delete a root folder', async () => { - // GIVEN + test('Can not delete folders because one of them is a root folder and cannot be deleted', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const [myGistFolder] = await testHelper.createManyTestFolders({ folderNames: ['My gist'], @@ -289,8 +279,6 @@ describe('Test Folder service', () => { userId: user.id, }); - // WHEN - // THEN await expect(async () => { await folderService.deleteMany([myGistFolder.id, rootFolder.id], user.id); }).rejects.toThrow(new AppError(errors.CANT_DELETE_ROOT_FOLDER, 'CANT_DELETE_ROOT_FOLDER')); @@ -299,8 +287,7 @@ describe('Test Folder service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should generate the breadcrumb path of a folder', async () => { - // GIVEN + test('Generate the breadcrumb path of a folder', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const gistFolderInput = TestHelper.createTestFolderInput({ @@ -317,10 +304,8 @@ describe('Test Folder service', () => { }); const nodeFolder = await folderService.create(nodeFolderInput); - // WHEN const subFolders = await folderService.generateBreadcrumb(nodeFolder.id); - // THEN expect(subFolders).toHaveLength(2); expect(subFolders.map((folder) => folder.name)).toEqual(['My gist', 'Node.js']); @@ -328,33 +313,26 @@ describe('Test Folder service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should generate the breadcrumb path of the root folder', async () => { - // GIVEN + test('Generate the breadcrumb path of the root folder', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); - // WHEN const subFolders = await folderService.generateBreadcrumb(rootFolder.id); - // THEN expect(subFolders).toHaveLength(0); await testHelper.deleteTestFoldersById([rootFolder.id]); await testHelper.deleteTestUsersById([user.id]); }); - it('should found no folder given the ID provided', async () => { - // GIVEN + test('Can not find a folder given the ID provided', async () => { const folderId = generateRandomId(); - // WHEN - // THEN await expect(async () => { await folderService.findById(folderId); }).rejects.toThrow(new AppError(errors.FOLDER_NOT_FOUND(folderId), 'FOLDER_NOT_FOUND')); }); - it('should update an existing folder in the specified folder', async () => { - // GIVEN + test('Update an existing folder in the specified folder', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const [folder] = await testHelper.createManyTestFolders({ folderNames: ['My gist'], @@ -368,10 +346,8 @@ describe('Test Folder service', () => { userId: user.id, }); - // WHEN const updatedFolder = await folderService.update(updateFolderInput); - // THEN const folderToUpdate = updateFolderInput.toFolder(folder); expect(updatedFolder).toMatchObject({ @@ -389,8 +365,7 @@ describe('Test Folder service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should not update an existing folder in the specified folder because another folder with the updated name already exists in the folder', async () => { - // GIVEN + test('Can not update an existing folder in the specified folder because another folder with the updated name already exists in the folder', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const [folder1, folder2] = await testHelper.createManyTestFolders({ folderNames: ['folder-one', 'folder-two'], @@ -404,8 +379,6 @@ describe('Test Folder service', () => { userId: user.id, }); - // WHEN - // THEN await expect(async () => { await folderService.update(updateFolderInput); }).rejects.toThrow(new AppError(errors.FOLDER_ALREADY_EXIST(updateFolderInput.name), 'FOLDER_ALREADY_EXIST')); @@ -415,8 +388,7 @@ describe('Test Folder service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should not update an existing folder in the specified folder because it belongs to other user', async () => { - // GIVEN + test('Can not update an existing folder in the specified folder because it belongs to other user', async () => { const [user1, rootFolder1] = await testHelper.createUserWithRootFolder(); const [user2, rootFolder2] = await testHelper.createUserWithRootFolder(); const [folderUser2] = await testHelper.createManyTestFolders({ @@ -427,8 +399,6 @@ describe('Test Folder service', () => { const updateFolderInput = TestHelper.updateTestFolderInput({ folderId: folderUser2.id, userId: user1.id }); - // WHEN - // THEN await expect(async () => { await folderService.update(updateFolderInput); }).rejects.toThrow( @@ -440,8 +410,7 @@ describe('Test Folder service', () => { await testHelper.deleteTestUsersById([user1.id, user2.id]); }); - it('should not update the user root folder', async () => { - // GIVEN + test('Can not not update the user root folder', async () => { const [user1, rootFolder] = await testHelper.createUserWithRootFolder(); const updateFolderInput = TestHelper.updateTestFolderInput({ @@ -450,8 +419,6 @@ describe('Test Folder service', () => { userId: user1.id, }); - // WHEN - // THEN await expect(async () => { await folderService.update(updateFolderInput); }).rejects.toThrow(new AppError(errors.CANT_RENAME_ROOT_FOLDER, 'CANT_RENAME_ROOT_FOLDER')); diff --git a/packages/domain/src/services/folders/folder.service.ts b/packages/domain/src/services/folders/folder.service.ts index ac7151d8..1a862f74 100644 --- a/packages/domain/src/services/folders/folder.service.ts +++ b/packages/domain/src/services/folders/folder.service.ts @@ -26,6 +26,15 @@ export class FolderService { } async create(createFolderInput: CreateFolderInput): Promise { + const parentFolder = await this.findById(createFolderInput.parentFolderId); + + if (parentFolder.userId !== createFolderInput.user) { + throw new AppError( + errors.FOLDER_NOT_BELONGING_TO_USER(createFolderInput.parentFolderId), + 'FOLDER_NOT_BELONGING_TO_USER', + ); + } + const isFolderExist = await this.isFolderExistInParentFolder({ folderName: createFolderInput.name, parentFolderId: createFolderInput.parentFolderId, @@ -38,8 +47,6 @@ export class FolderService { const input = createFolderInput.toFolder(); - const parentFolder = await this.findById(createFolderInput.parentFolderId); - return this.prisma.folder.create({ data: { id: input.id, diff --git a/packages/utils/src/error/error.ts b/packages/utils/src/error/error.ts index 14679aa8..d2556df0 100644 --- a/packages/utils/src/error/error.ts +++ b/packages/utils/src/error/error.ts @@ -18,7 +18,8 @@ export type AppErrorCode = | 'CANT_EDIT_FOLDER' | 'CANT_RENAME_ROOT_FOLDER' | 'INVALID_CONFIRMATION_TOKEN' - | 'USER_NOT_FOUND'; + | 'USER_NOT_FOUND' + | 'FOLDER_NOT_BELONGING_TO_USER'; export class AppError extends Error { constructor( diff --git a/packages/utils/src/error/messages.ts b/packages/utils/src/error/messages.ts index 1aba17fe..596a834c 100644 --- a/packages/utils/src/error/messages.ts +++ b/packages/utils/src/error/messages.ts @@ -1,6 +1,8 @@ export const NEWSLETTER_SUBSCRIBE_FAILED = 'Failed to subscribe to the newsletter.'; export const NOT_AUTHENTICATED = 'You must be authenticated to access to this resource.'; export const FOLDER_ALREADY_EXIST = (folderName: string) => `A folder named "${folderName}" already exists`; +export const FOLDER_NOT_BELONGING_TO_USER = (folderId: string) => + `The folder with the id ${folderId} does not belong to the authenticated user`; export const FOLDERS_DONT_BELONG_TO_USER = "One or may folders don't belong to the current user"; export const CANT_DELETE_ROOT_FOLDER = 'The root folder cannot be deleted.'; export const SNIPPET_ALREADY_EXIST = (snippetName: string) => `A snippet named "${snippetName}" already exists`; From dc2f7b01c8b3a9a479d6151f82fe85ab28328f88 Mon Sep 17 00:00:00 2001 From: Eric Cabrel TIOGO Date: Sun, 9 Jun 2024 11:30:05 +0200 Subject: [PATCH 5/6] test: minor configuration changes --- .eslintrc.json | 3 +- .github/workflows/build.yml | 1 - package.json | 1 + packages/domain/package.json | 2 +- .../inputs/create-folder-input.test.ts | 12 --- .../services/folders/utils/folders.test.ts | 6 -- .../newsletters/newsletter.service.test.ts | 6 -- .../services/sessions/session.service.test.ts | 15 +--- .../inputs/create-snippet-input.test.ts | 3 - .../inputs/delete-snippet-input.test.ts | 3 - .../services/snippets/snippet.service.test.ts | 79 +++++-------------- .../src/services/users/user.service.test.ts | 61 ++++---------- packages/domain/src/utils/db-id.test.ts | 6 +- packages/domain/tsconfig.json | 1 + .../oembed/generate-metadata.test.ts | 3 - .../renderer/content/html-generator.test.ts | 5 +- .../renderer/content/preview-template.test.ts | 15 ++-- .../__tests__/renderer/content/utils.test.ts | 21 ----- packages/embed/jest.config.ts | 1 + packages/embed/package.json | 1 + packages/embed/tsconfig.json | 2 +- turbo.json | 38 ++++++++- yarn.lock | 49 +++++++++--- 23 files changed, 132 insertions(+), 202 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 44b94ec2..afa848f6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,7 +5,8 @@ "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "plugin:typescript-sort-keys/recommended", - "plugin:jest/recommended" + "plugin:jest/recommended", + "turbo" ], "plugins": [ "sort-destructure-keys", diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64207018..a836f893 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,6 @@ jobs: MYSQL_DATABASE: test MYSQL_PORT: 3306 DATABASE_URL: mysql://root:root@127.0.0.1:3306/test - TEST_DATABASE_URL: mysql://root:root@127.0.0.1:3306/test CONVERTKIT_API_KEY: api_key CONVERTKIT_FORM_ID: form_id outputs: diff --git a/package.json b/package.json index c0dc8f0f..5f53da6d 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "dotenv-cli": "7.4.2", "eslint": "8.56.0", "eslint-config-prettier": "9.1.0", + "eslint-config-turbo": "2.0.3", "eslint-plugin-import": "2.29.1", "eslint-plugin-jest": "27.9.0", "eslint-plugin-prettier": "5.1.3", diff --git a/packages/domain/package.json b/packages/domain/package.json index a3d65229..f77cef5c 100644 --- a/packages/domain/package.json +++ b/packages/domain/package.json @@ -25,7 +25,7 @@ "db:seed": "yarn env -- prisma db seed", "db:test": "zx tests/database.mjs", "db:test:stop": "docker container kill snipcode-test-db && docker container prune -f", - "test": "NODE_ENV=test yarn db:test && dotenv -e .env.test -- jest" + "test": "NODE_ENV=test yarn db:test && dotenv -e .env.test -- jest --runInBand" }, "dependencies": { "@bugsnag/cuid": "3.1.1", diff --git a/packages/domain/src/services/folders/inputs/create-folder-input.test.ts b/packages/domain/src/services/folders/inputs/create-folder-input.test.ts index 357fd9cd..63785735 100644 --- a/packages/domain/src/services/folders/inputs/create-folder-input.test.ts +++ b/packages/domain/src/services/folders/inputs/create-folder-input.test.ts @@ -3,17 +3,14 @@ import { Folder } from '../folder.entity'; describe('Test Create Folder Input', () => { it('should create a valid folder object', () => { - // GIVEN const input = new CreateFolderInput({ name: 'blogs', parentId: 'cl23rzwe5000002czaedc8sll', userId: 'dm34saxf6111113dabfed9tmm', }); - // WHEN const folder = input.toFolder(); - // THEN expect(folder).toMatchObject({ createdAt: expect.any(Date), id: expect.any(String), @@ -27,7 +24,6 @@ describe('Test Create Folder Input', () => { }); it('should create the valid folder name', () => { - // GIVEN const input = new CreateFolderInput({ name: 'blogs', parentId: 'cl23rzwe5000002czaedc8sll', @@ -35,13 +31,10 @@ describe('Test Create Folder Input', () => { }); const expectedFolderName = 'blogs'; - // WHEN - // THEN expect(input.name).toEqual(expectedFolderName); }); it('should create the valid folder parent id', () => { - // GIVEN const input = new CreateFolderInput({ name: 'blogs', parentId: 'cl23rzwe5000002czaedc8sll', @@ -49,13 +42,10 @@ describe('Test Create Folder Input', () => { }); const expectedParentId = 'cl23rzwe5000002czaedc8sll'; - // WHEN - // THEN expect(input.parentFolderId).toEqual(expectedParentId); }); it('should create the valid folder user id', () => { - // GIVEN const input = new CreateFolderInput({ name: 'blogs', parentId: 'cl23rzwe5000002czaedc8sll', @@ -63,8 +53,6 @@ describe('Test Create Folder Input', () => { }); const expectedUserId = 'dm34saxf6111113dabfed9tmm'; - // WHEN - // THEN expect(input.user).toEqual(expectedUserId); }); }); diff --git a/packages/domain/src/services/folders/utils/folders.test.ts b/packages/domain/src/services/folders/utils/folders.test.ts index d7659522..15c6ef72 100644 --- a/packages/domain/src/services/folders/utils/folders.test.ts +++ b/packages/domain/src/services/folders/utils/folders.test.ts @@ -4,7 +4,6 @@ import { Folder } from '../folder.entity'; describe('Test folders utilities', () => { it('should assert the folders contain the root folder', () => { - // GIVEN const userId = TestHelper.generateTestId(); const rootFolder = TestHelper.createTestFolderInput({ userId }).toFolder(); @@ -13,15 +12,12 @@ describe('Test folders utilities', () => { const foldersToDelete: Folder[] = [TestHelper.createTestFolderInput({ userId }).toFolder(), rootFolder]; - // WHEN const isValid = isFoldersContainRoot(foldersToDelete); - // THEN expect(isValid).toEqual(true); }); it("should assert the folders doesn't contain the root folder", () => { - // GIVEN const userId = TestHelper.generateTestId(); const foldersToDelete: Folder[] = [ @@ -29,10 +25,8 @@ describe('Test folders utilities', () => { TestHelper.createTestFolderInput({ userId }).toFolder(), ]; - // WHEN const isValid = isFoldersContainRoot(foldersToDelete); - // THEN expect(isValid).toEqual(false); }); }); diff --git a/packages/domain/src/services/newsletters/newsletter.service.test.ts b/packages/domain/src/services/newsletters/newsletter.service.test.ts index 20ec9eaa..8b065adb 100644 --- a/packages/domain/src/services/newsletters/newsletter.service.test.ts +++ b/packages/domain/src/services/newsletters/newsletter.service.test.ts @@ -26,7 +26,6 @@ describe('Newsletter service', () => { }); test('Add the email address to the newsletter subscribers', async () => { - // GIVEN const emailToSubscribe = 'user@email.com'; const tags = ['snipcode']; const formId = 'formId'; @@ -43,17 +42,14 @@ describe('Newsletter service', () => { }, }); - // WHEN await newsletterService.subscribe(emailToSubscribe, tags); - // THEN expect(scope.isDone()).toBe(true); nock.cleanAll(); }); test('Handle HTTP error when the request to add the email address to the newsletter subscribers fails', async () => { - // GIVEN const emailToSubscribe = 'user@email.com'; const tags = ['snipcode']; const formId = 'formId'; @@ -68,8 +64,6 @@ describe('Newsletter service', () => { message: 'Wrong api key provided!', }); - // WHEN - // THEN const caughtErrorsFormatted = { data: { message: 'Wrong api key provided!', diff --git a/packages/domain/src/services/sessions/session.service.test.ts b/packages/domain/src/services/sessions/session.service.test.ts index b4cdd4d9..54b7f7cd 100644 --- a/packages/domain/src/services/sessions/session.service.test.ts +++ b/packages/domain/src/services/sessions/session.service.test.ts @@ -26,36 +26,29 @@ describe('Test Session Service', function () { testHelper = new TestHelper(prismaService); }); - it('should create a session', async () => { - // GIVEN + test('Create a session', async () => { const userId = TestHelper.generateTestId(); const input = TestHelper.createTestSessionInput(userId); - // WHEN const sessionCreated = await sessionService.create(input); - // THEN expect(sessionCreated).toMatchObject(input.toSession()); await testHelper.deleteTestUserSessions(sessionCreated.userId); }); - it('should find a session by token', async () => { - // GIVEN + test('Retrieve a session by the token attached to it', async () => { const userId = TestHelper.generateTestId(); const session = await testHelper.createTestSession({ userId }); - // WHEN const sessionFound = await sessionService.findByToken(session.token); - // THEN expect(session).toEqual(sessionFound); await testHelper.deleteTestUserSessions(session.userId); }); - it('should delete all session of a user', async () => { - // GIVEN + test('Delete all sessions of a user', async () => { const userId = TestHelper.generateTestId(); const sessionsCreated = await Promise.all([ @@ -64,10 +57,8 @@ describe('Test Session Service', function () { testHelper.createTestSession({ userId }), ]); - // WHEN await sessionService.deleteUserSessions(userId); - // THEN const userSessions = await Promise.all(sessionsCreated.map(({ token }) => sessionService.findByToken(token))); expect(userSessions.every((session) => !session)).toBe(true); diff --git a/packages/domain/src/services/snippets/inputs/create-snippet-input.test.ts b/packages/domain/src/services/snippets/inputs/create-snippet-input.test.ts index d9bdd802..d07b514d 100644 --- a/packages/domain/src/services/snippets/inputs/create-snippet-input.test.ts +++ b/packages/domain/src/services/snippets/inputs/create-snippet-input.test.ts @@ -7,7 +7,6 @@ describe('Test Create Snippet Input', () => { const folderId = TestHelper.generateTestId(); const userId = TestHelper.generateTestId(); - // GIVEN const input = new CreateSnippetInput({ content: 'import React from "react";\n\nexport const App = () => {\n\n\treturn(\n\t\t
Hello
\n\t);\n};', contentHighlighted: @@ -22,10 +21,8 @@ describe('Test Create Snippet Input', () => { visibility: 'public', }); - // WHEN const folder = input.toSnippet(); - // THEN expect(folder).toMatchObject({ content: 'import React from "react";\n\nexport const App = () => {\n\n\treturn(\n\t\t
Hello
\n\t);\n};', contentHtml: diff --git a/packages/domain/src/services/snippets/inputs/delete-snippet-input.test.ts b/packages/domain/src/services/snippets/inputs/delete-snippet-input.test.ts index 86f3993f..fbdbde96 100644 --- a/packages/domain/src/services/snippets/inputs/delete-snippet-input.test.ts +++ b/packages/domain/src/services/snippets/inputs/delete-snippet-input.test.ts @@ -6,14 +6,11 @@ describe('Test Delete Snippet Input', () => { const snippetId = TestHelper.generateTestId(); const userId = TestHelper.generateTestId(); - // GIVEN const input = new DeleteSnippetInput({ creatorId: userId, snippetId, }); - // WHEN - // THEN expect(input.snippetId).toEqual(snippetId); expect(input.creatorId).toEqual(userId); }); diff --git a/packages/domain/src/services/snippets/snippet.service.test.ts b/packages/domain/src/services/snippets/snippet.service.test.ts index 64717db5..d48f48df 100644 --- a/packages/domain/src/services/snippets/snippet.service.test.ts +++ b/packages/domain/src/services/snippets/snippet.service.test.ts @@ -34,15 +34,12 @@ describe('Test Snippet service', () => { await roleService.loadRoles(); }); - it('should create a snippet in the specified folder', async () => { - // GIVEN + test('Create a snippet in a folder', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const createSnippetInput = TestHelper.createTestSnippetInput({ folderId: rootFolder.id, userId: user.id }); - // WHEN const expectedSnippet = await snippetService.create(createSnippetInput); - // THEN expect(expectedSnippet).toMatchObject({ content: createSnippetInput.toSnippet().content, contentHtml: createSnippetInput.toSnippet().contentHtml, @@ -65,13 +62,10 @@ describe('Test Snippet service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should not create a snippet because the specified folder does not exist', async () => { - // GIVEN + test('Can not create a snippet because the specified folder does not exist', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const createSnippetInput = TestHelper.createTestSnippetInput({ folderId: generateRandomId(), userId: user.id }); - // WHEN - // THEN await expect(async () => { await snippetService.create(createSnippetInput); }).rejects.toThrow(new AppError(errors.FOLDER_NOT_FOUND(createSnippetInput.folderId), 'FOLDER_NOT_FOUND')); @@ -80,8 +74,7 @@ describe('Test Snippet service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should not create a snippet because it already exists in the specified folder', async () => { - // GIVEN + test('Can not create a snippet because it already exists in the specified folder', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const snippet = await testHelper.createTestSnippet({ folderId: rootFolder.id, name: 'app.tsx', userId: user.id }); @@ -91,8 +84,6 @@ describe('Test Snippet service', () => { userId: user.id, }); - // WHEN - // THEN await expect(() => snippetService.create(sameCreateSnippetInput)).rejects.toThrow( new AppError(errors.SNIPPET_ALREADY_EXIST(sameCreateSnippetInput.name), 'SNIPPET_ALREADY_EXIST'), ); @@ -102,8 +93,7 @@ describe('Test Snippet service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should retrieve all public snippets', async () => { - // GIVEN + test('Retrieve all the public snippets', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const existingSnippets = await Promise.all([ testHelper.createTestSnippet({ folderId: rootFolder.id, userId: user.id, visibility: 'public' }), @@ -114,21 +104,18 @@ describe('Test Snippet service', () => { testHelper.createTestSnippet({ folderId: rootFolder.id, userId: user.id, visibility: 'public' }), ]); - // WHEN const publicSnippets = await snippetService.findPublicSnippet({ itemPerPage: 10 }); - // THEN - await expect(publicSnippets.hasMore).toEqual(false); - await expect(publicSnippets.nextCursor).toEqual(null); - await expect(publicSnippets.items).toHaveLength(4); + expect(publicSnippets.hasMore).toEqual(false); + expect(publicSnippets.nextCursor).toEqual(null); + expect(publicSnippets.items).toHaveLength(4); await testHelper.deleteTestSnippetsById(existingSnippets.map((snippet) => snippet.id)); await testHelper.deleteTestFoldersById([rootFolder.id]); await testHelper.deleteTestUsersById([user.id]); }); - it('should retrieve a subset of public snippets', async () => { - // GIVEN + test('Retrieve three public snippets per page', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const existingSnippets = await Promise.all([ testHelper.createTestSnippet({ folderId: rootFolder.id, userId: user.id, visibility: 'public' }), @@ -139,21 +126,18 @@ describe('Test Snippet service', () => { testHelper.createTestSnippet({ folderId: rootFolder.id, userId: user.id, visibility: 'public' }), ]); - // WHEN const publicSnippets = await snippetService.findPublicSnippet({ itemPerPage: 3 }); - // THEN - await expect(publicSnippets.hasMore).toEqual(true); - await expect(publicSnippets.nextCursor).toEqual(expect.any(String)); - await expect(publicSnippets.items).toHaveLength(3); + expect(publicSnippets.hasMore).toEqual(true); + expect(publicSnippets.nextCursor).toEqual(expect.any(String)); + expect(publicSnippets.items).toHaveLength(3); await testHelper.deleteTestSnippetsById(existingSnippets.map((snippet) => snippet.id)); await testHelper.deleteTestFoldersById([rootFolder.id]); await testHelper.deleteTestUsersById([user.id]); }); - it('should find all snippets of a user', async () => { - // GIVEN + test('Retrieve all snippets belonging to a user', async () => { const [user1, rootFolder1] = await testHelper.createUserWithRootFolder(); const [user2, rootFolder2] = await testHelper.createUserWithRootFolder(); @@ -166,19 +150,16 @@ describe('Test Snippet service', () => { testHelper.createTestSnippet({ folderId: rootFolder2.id, userId: user2.id, visibility: 'private' }), ]); - // WHEN const userSnippets = await snippetService.findByUser(user2.id); - // THEN - await expect(userSnippets).toHaveLength(3); + expect(userSnippets).toHaveLength(3); await testHelper.deleteTestSnippetsById(existingSnippets.map((snippet) => snippet.id)); await testHelper.deleteTestFoldersById([rootFolder1.id, rootFolder2.id]); await testHelper.deleteTestUsersById([user1.id, user2.id]); }); - it('should retrieve a snippet by its ID', async () => { - // GIVEN + test('Retrieve a snippet by its ID', async () => { const [user1, rootFolder1] = await testHelper.createUserWithRootFolder(); const snippet = await testHelper.createTestSnippet({ @@ -187,10 +168,8 @@ describe('Test Snippet service', () => { visibility: 'public', }); - // WHEN const snippetFound = await snippetService.findById(snippet.id); - // THEN expect(snippetFound).toMatchObject({ folderId: rootFolder1.id, id: snippet.id, @@ -203,19 +182,15 @@ describe('Test Snippet service', () => { await testHelper.deleteTestUsersById([user1.id]); }); - it('should found no snippet given the ID provided', async () => { - // GIVEN + test("Can not find a snippet by the ID because it doesn't exists", async () => { const snippetId = generateRandomId(); - // WHEN - // THEN await expect(async () => { await snippetService.findById(snippetId); }).rejects.toThrow(new AppError(errors.SNIPPET_NOT_FOUND(snippetId), 'SNIPPET_NOT_FOUND')); }); - it('should delete an existing snippet belonging to a user', async () => { - // GIVEN + test('Delete a snippet belonging to a user', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const [snippet1, snippet2] = await Promise.all([ @@ -223,12 +198,10 @@ describe('Test Snippet service', () => { testHelper.createTestSnippet({ folderId: rootFolder.id, userId: user.id, visibility: 'private' }), ]); - // WHEN const deleteSnippetInput = TestHelper.deleteTestSnippetInput({ snippetId: snippet1.id, userId: snippet1.userId }); await snippetService.delete(deleteSnippetInput); - // THEN const folderSnippets = await snippetService.findByFolder(rootFolder.id); expect(folderSnippets).toHaveLength(1); @@ -238,8 +211,7 @@ describe('Test Snippet service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should not delete an existing snippet because it belongs to other user', async () => { - // GIVEN + test('Can not delete a snippet belonging to other user', async () => { const [user1, rootFolder1] = await testHelper.createUserWithRootFolder(); const [user2, rootFolder2] = await testHelper.createUserWithRootFolder(); @@ -249,10 +221,8 @@ describe('Test Snippet service', () => { testHelper.createTestSnippet({ folderId: rootFolder1.id, userId: user1.id, visibility: 'private' }), ]); - // WHEN const deleteSnippetInput = TestHelper.deleteTestSnippetInput({ snippetId: snippet1.id, userId: user2.id }); - // THEN await expect(async () => { await snippetService.delete(deleteSnippetInput); }).rejects.toThrow( @@ -270,8 +240,7 @@ describe('Test Snippet service', () => { await testHelper.deleteTestUsersById([user1.id, user2.id]); }); - it('should update an existing snippet in the specified folder', async () => { - // GIVEN + test('Update a snippet in a folder', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const snippet = await testHelper.createTestSnippet({ folderId: rootFolder.id, @@ -281,10 +250,8 @@ describe('Test Snippet service', () => { const updateSnippetInput = TestHelper.updateTestSnippetInput({ snippetId: snippet.id, userId: user.id }); - // WHEN const updatedSnippet = await snippetService.update(updateSnippetInput); - // THEN const snippetToUpdate = updateSnippetInput.toSnippet(snippet); expect(updatedSnippet).toMatchObject({ @@ -309,8 +276,7 @@ describe('Test Snippet service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should not update an existing snippet in the specified folder because another snippet with the updated name already exists in the folder', async () => { - // GIVEN + test('Can not update a snippet in a folder because another snippet with the updated name already exists inside', async () => { const [user, rootFolder] = await testHelper.createUserWithRootFolder(); const [snippet] = await Promise.all([ testHelper.createTestSnippet({ folderId: rootFolder.id, name: 'snippet-one.java', userId: user.id }), @@ -328,8 +294,6 @@ describe('Test Snippet service', () => { userId: user.id, }); - // WHEN - // THEN await expect(async () => { await snippetService.update(updateSnippetInput); }).rejects.toThrow(new AppError(errors.SNIPPET_ALREADY_EXIST(updateSnippetInput.name), 'SNIPPET_ALREADY_EXIST')); @@ -339,8 +303,7 @@ describe('Test Snippet service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should not update an existing snippet in the specified folder because because it belongs to other user', async () => { - // GIVEN + test('Can not update a snippet in a folder belonging to other user', async () => { const [user1, rootFolder1] = await testHelper.createUserWithRootFolder(); const [user2, rootFolder2] = await testHelper.createUserWithRootFolder(); const snippet = await testHelper.createTestSnippet({ @@ -351,8 +314,6 @@ describe('Test Snippet service', () => { const updateSnippetInput = TestHelper.updateTestSnippetInput({ snippetId: snippet.id, userId: user2.id }); - // WHEN - // THEN await expect(async () => { await snippetService.update(updateSnippetInput); }).rejects.toThrow( diff --git a/packages/domain/src/services/users/user.service.test.ts b/packages/domain/src/services/users/user.service.test.ts index ad0f0385..fb1462ad 100644 --- a/packages/domain/src/services/users/user.service.test.ts +++ b/packages/domain/src/services/users/user.service.test.ts @@ -33,9 +33,9 @@ describe('Test User service', () => { await roleService.loadRoles(); }); - it('should load users in the database', async () => { + test('Load users in the database', async () => { const [roleAdmin] = await roleService.findAll(); - const adminPassword = 'VerStrongPassword'; + const adminPassword = 'VeryStrongPassword'; await userService.loadAdminUser(roleAdmin, adminPassword); @@ -46,15 +46,12 @@ describe('Test User service', () => { await testHelper.deleteTestUsersById([adminUser?.id]); }); - it('should create a user', async () => { - // GIVEN + test('Create a user', async () => { const role = await testHelper.findTestRole('user'); const createUserInput = TestHelper.createTestUserInput({ roleId: role.id }); - // WHEN const createdUser = await userService.create(createUserInput); - // THEN expect(createdUser).toMatchObject({ createdAt: expect.any(Date), email: createUserInput.email, @@ -73,15 +70,12 @@ describe('Test User service', () => { await testHelper.deleteTestUsersById([createdUser.id]); }); - it('should create a user with no username', async () => { - // GIVEN + test('Create a user with no username', async () => { const role = await testHelper.findTestRole('user'); const createUserInput = TestHelper.createTestUserInput({ roleId: role.id, username: null }); - // WHEN const createdUser = await userService.create(createUserInput); - // THEN expect(createdUser).toMatchObject({ createdAt: expect.any(Date), email: createUserInput.email, @@ -100,33 +94,27 @@ describe('Test User service', () => { await testHelper.deleteTestUsersById([createdUser.id]); }); - it('should create a user with a username that already exists', async () => { - // GIVEN + test('Create a user with a username that already exists will generate a new one for him', async () => { const role = await testHelper.findTestRole('user'); const user = await testHelper.createTestUser({ username: 'roloto' }); - const createUserInput = await TestHelper.createTestUserInput({ roleId: role.id, username: 'roloto' }); + const createUserInput = TestHelper.createTestUserInput({ roleId: role.id, username: 'roloto' }); - // WHEN const createdUser = await userService.create(createUserInput); - // THEN expect(createdUser.username).not.toEqual('roloto'); await testHelper.deleteTestUsersById([user.id, createdUser.id]); }); - it('should create a user - validation check', async () => { - // GIVEN + test('Create a user - validation check', async () => { const role = await testHelper.findTestRole('user'); const createUserInput = TestHelper.createTestUserInput({ roleId: role.id }); createUserInput.isEnabled = true; - // WHEN const createdUser = await userService.create(createUserInput); - // THEN expect(createdUser).toMatchObject({ createdAt: expect.any(Date), email: createUserInput.email, @@ -145,14 +133,11 @@ describe('Test User service', () => { await testHelper.deleteTestUsersById([createdUser.id]); }); - it('should fail create a user because the email address already exists', async () => { - // GIVEN + test('Can not create a user because the email address already exists', async () => { const user = await testHelper.createTestUser({ email: 'user@email.com' }); const role = await testHelper.findTestRole('user'); const createUserInput = TestHelper.createTestUserInput({ email: 'user@email.com', roleId: role.id }); - // WHEN - // THEN await expect(async () => { await userService.create(createUserInput); }).rejects.toThrow(new AppError(errors.EMAIL_ALREADY_TAKEN, 'EMAIL_ALREADY_TAKEN')); @@ -160,17 +145,14 @@ describe('Test User service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should update user information', async () => { - // GIVEN + test('Update user information', async () => { const currentUser = await testHelper.createTestUser({}); const role = await testHelper.findTestRole('admin'); const updateUserInput = TestHelper.updateTestUserInput(role.id); - // WHEN const updatedUser = await userService.update(currentUser, updateUserInput); - // THEN expect(updatedUser).toMatchObject({ createdAt: expect.any(Date), email: currentUser.email, @@ -189,26 +171,20 @@ describe('Test User service', () => { await testHelper.deleteTestUsersById([currentUser.id]); }); - it("should fail to authenticate the user because the email doesn't exists", async () => { - // GIVEN + test("Fail to authenticate the user because the email doesn't exists", async () => { const userEmail = 'email@test.com'; const userPassword = 'strongPassword'; - // WHEN - // THEN await expect(() => userService.login(userEmail, userPassword)).rejects.toThrow( new AppError(errors.LOGIN_FAILED, 'LOGIN_FAILED'), ); }); - it('should fail to authenticate the user because the password is not correct', async () => { - // GIVEN + test('Fail to authenticate the user because the password is not correct', async () => { const userPassword = 'strongPassword'; const userBadPassword = 'badPassword'; const user = await testHelper.createTestUser({ oauthProvider: 'email', password: userPassword }); - // WHEN - // THEN await expect(() => userService.login(user.email, userBadPassword)).rejects.toThrow( new AppError(errors.LOGIN_FAILED, 'LOGIN_FAILED'), ); @@ -216,13 +192,10 @@ describe('Test User service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should fail to authenticate the user because the user is disabled', async () => { - // GIVEN + test('Fail to authenticate the user because the user is disabled', async () => { const userPassword = 'strongPassword'; const user = await testHelper.createTestUser({ oauthProvider: 'email', password: userPassword }); - // WHEN - // THEN await expect(() => userService.login(user.email, userPassword)).rejects.toThrow( new AppError(errors.ACCOUNT_DISABLED, 'ACCOUNT_DISABLED'), ); @@ -230,8 +203,7 @@ describe('Test User service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should successfully authenticate the user', async () => { - // GIVEN + test('Authenticate a user', async () => { const userPassword = 'strongPassword'; const user = await testHelper.createTestUser({ isEnabled: true, @@ -239,10 +211,8 @@ describe('Test User service', () => { password: userPassword, }); - // WHEN const authenticatedUser = await userService.login(user.email, userPassword); - // THEN expect(user).toMatchObject({ email: authenticatedUser.email, id: authenticatedUser.id, @@ -255,14 +225,11 @@ describe('Test User service', () => { await testHelper.deleteTestUsersById([user.id]); }); - it('should found no user given the ID provided', async () => { - // GIVEN + test("Can not find the user by its ID because it doesn't exist", async () => { const snippetId = generateRandomId(); - // WHEN const user = await userService.findById(snippetId); - // THEN expect(user).toBeNull(); }); }); diff --git a/packages/domain/src/utils/db-id.test.ts b/packages/domain/src/utils/db-id.test.ts index ae680c62..50e92c10 100644 --- a/packages/domain/src/utils/db-id.test.ts +++ b/packages/domain/src/utils/db-id.test.ts @@ -1,17 +1,17 @@ import { dbID } from './db-id'; describe('Test Database ID generator', () => { - test('generate a valid id', () => { + it('should generate a valid id', () => { const id = dbID.generate(); expect(dbID.isValid(id)).toEqual(true); }); - test.each([ + it.each([ ['myinvalidid', false], ['111111111111', false], ['cl1fny73o0000e7czbglkhv0p', true], - ])('detect validity of id %s', (id, expected) => { + ])('should validate the ID "%s" as "%s"', (id, expected) => { expect(dbID.isValid(id)).toEqual(expected); }); }); diff --git a/packages/domain/tsconfig.json b/packages/domain/tsconfig.json index d5304608..c4e94c18 100644 --- a/packages/domain/tsconfig.json +++ b/packages/domain/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "declaration": true, "composite": true, + "sourceMap": true, }, "exclude": ["node_modules", "dist"], "files": ["env.d.ts"], diff --git a/packages/embed/__tests__/oembed/generate-metadata.test.ts b/packages/embed/__tests__/oembed/generate-metadata.test.ts index b439bc44..888511f9 100644 --- a/packages/embed/__tests__/oembed/generate-metadata.test.ts +++ b/packages/embed/__tests__/oembed/generate-metadata.test.ts @@ -2,19 +2,16 @@ import { generateOembedMetadata } from '../../src/oembed'; describe('Test Generate Oembed metadata', () => { it('should generate Oembed metadata for a code snippet', () => { - // GIVEN const snippet = { id: 'snippet_id', name: 'snippet-name.java' }; const SNIPPET_RENDERER_URL = 'https://embed.snipcode.dev'; const WEB_APP_URL = 'https://snipcode.dev'; - // WHEN const result = generateOembedMetadata({ snippet, snippetRendererURL: SNIPPET_RENDERER_URL, webAppURL: WEB_APP_URL, }); - // THEN expect(result).toMatchInlineSnapshot(` { "height": 500, diff --git a/packages/embed/__tests__/renderer/content/html-generator.test.ts b/packages/embed/__tests__/renderer/content/html-generator.test.ts index 0ac2f1dd..f583314d 100644 --- a/packages/embed/__tests__/renderer/content/html-generator.test.ts +++ b/packages/embed/__tests__/renderer/content/html-generator.test.ts @@ -1,14 +1,11 @@ import { generateNoSnippetHtmlContent } from '../../../src/renderer/content/html-generator'; describe('Test HTML generator functions', () => { - it.only('should generates html content for a non existing code snippet', () => { - // GIVEN + it('should generates html content for a non existing code snippet', () => { const WEB_APP_URL = 'https://snipcode.dev'; - // WHEN const result = generateNoSnippetHtmlContent(WEB_APP_URL); - // THEN expect(result).toMatchInlineSnapshot(` "

Oops! Snippet not found!

diff --git a/packages/embed/__tests__/renderer/content/preview-template.test.ts b/packages/embed/__tests__/renderer/content/preview-template.test.ts index 3014debb..e12262ab 100644 --- a/packages/embed/__tests__/renderer/content/preview-template.test.ts +++ b/packages/embed/__tests__/renderer/content/preview-template.test.ts @@ -7,8 +7,11 @@ jest.mock('../../../src/renderer/content/utils', () => { }); describe('Test generateHTMLPreview()', () => { + afterAll(() => { + jest.clearAllMocks(); + }); + it('should generates the html preview for a code snippet', () => { - // GIVEN const args: Args = { code: 'export const hashPassword = (password: string): string => {\n' + @@ -23,16 +26,14 @@ describe('Test generateHTMLPreview()', () => { '\n' + ' return bcrypt.hashSync(password, SALT_ROUNDS);\n' + '};', - scriptUrl: 'https://cdn.com/sharigan/script.js', - styleUrl: 'https://cdn.com/sharigan/style.css', + scriptUrl: 'https://cdn.com/snipcode/script.js', + styleUrl: 'https://cdn.com/snipcode/style.css', title: 'helpers.ts', webAppUrl: 'https://snipcode.dev', }; - // WHEN const result = generateHTMLPreview(args); - // THEN expect(result).toMatchInlineSnapshot(` " @@ -42,7 +43,7 @@ describe('Test generateHTMLPreview()', () => { Snipcode - helpers.ts - +
@@ -71,7 +72,7 @@ describe('Test generateHTMLPreview()', () => { };
- + " diff --git a/packages/embed/__tests__/renderer/content/utils.test.ts b/packages/embed/__tests__/renderer/content/utils.test.ts index 55c7b0db..6265dd31 100644 --- a/packages/embed/__tests__/renderer/content/utils.test.ts +++ b/packages/embed/__tests__/renderer/content/utils.test.ts @@ -7,34 +7,25 @@ import { describe('Test utils functions', () => { describe('Test addWhitespaceForEmptyLine()', () => { it('should create not whitespace for a simple span tag', () => { - // GIVEN const line = ``; - // WHEN const result = addWhitespaceForEmptyLine(line); - // THEN expect(result).toEqual(``); }); it('should create whitespace for a code line', () => { - // GIVEN const line = ``; - // WHEN const result = addWhitespaceForEmptyLine(line); - // THEN expect(result).toEqual(`  `); }); it('should create whitespace for a code line with highlight', () => { - // GIVEN const line = ``; - // WHEN const result = addWhitespaceForEmptyLine(line); - // THEN expect(result).toEqual(`  `); }); @@ -42,35 +33,26 @@ describe('Test utils functions', () => { describe('Test generateLineHighlightOptions()', () => { it('should generate no line highlight options from a null string', () => { - // GIVEN const lineHighlight: string | null = null; - // WHEN const result = generateLineHighlightOptions(lineHighlight); - // THEN expect(result).toMatchObject([]); }); it('should generate no line highlight options from an empty string', () => { - // GIVEN const lineHighlight = `[]`; - // WHEN const result = generateLineHighlightOptions(lineHighlight); - // THEN expect(result).toMatchObject([]); }); it('should generate line highlight options from string', () => { - // GIVEN const lineHighlight = `[[3,"delete"],[6,"blur"],[7,"add"]]`; - // WHEN const result = generateLineHighlightOptions(lineHighlight); - // THEN const expectedResult = [ { classes: ['line-diff line-diff-delete'], line: 3 }, { classes: ['line-diff line-diff-blur'], line: 6 }, @@ -83,13 +65,10 @@ describe('Test utils functions', () => { describe('Test parseHTMLSnippetCode()', () => { it('should parse html snippet code', () => { - // GIVEN const htmlCode = `
line code 1\n\nline code 3
`; - // WHEN const result = parseHTMLSnippetCode(htmlCode); - // THEN expect(result).toEqual( `1line code 1\n2  \n3line code 3`, ); diff --git a/packages/embed/jest.config.ts b/packages/embed/jest.config.ts index afeaf065..c0f92394 100644 --- a/packages/embed/jest.config.ts +++ b/packages/embed/jest.config.ts @@ -7,6 +7,7 @@ const config: Config.InitialOptions = { testEnvironment: 'node', clearMocks: true, maxWorkers: 1, + prettierPath: require.resolve('prettier-2'), // Waiting for the release of v30 to use Prettier 3 snapshotFormat: { printBasicPrototype: false, }, diff --git a/packages/embed/package.json b/packages/embed/package.json index 4bd1c5ae..5994b785 100644 --- a/packages/embed/package.json +++ b/packages/embed/package.json @@ -25,6 +25,7 @@ "@types/express": "4.17.21", "express": "4.19.1", "nodemon": "2.0.22", + "prettier-2": "npm:prettier@^2", "serve": "14.2.1", "shiki": "0.14.7", "tsup": "8.0.2" diff --git a/packages/embed/tsconfig.json b/packages/embed/tsconfig.json index 6836d362..d33d3889 100644 --- a/packages/embed/tsconfig.json +++ b/packages/embed/tsconfig.json @@ -5,7 +5,7 @@ "outDir": "./dist", "declaration": true, "composite": true, - "sourceMap": false, + "sourceMap": true, "lib": ["es2021", "dom"], "target": "es2021", "emitDecoratorMetadata": false diff --git a/turbo.json b/turbo.json index aa568ee4..40563492 100644 --- a/turbo.json +++ b/turbo.json @@ -18,7 +18,7 @@ "dist/**" ] }, - "@snipcode/core#build": { + "@snipcode/backend#build": { "dependsOn": [ "^build" ], @@ -35,13 +35,24 @@ ], "env": [ "NODE_ENV", - "DATABASE_URL" + "DATABASE_URL", + "SENTRY_ENABLED", + "SENTRY_DSN", + "APP_VERSION", + "ADMIN_PASSWORD", + "JWT_SECRET" ] }, "@snipcode/domain#build": { "dependsOn": [ "^build", "prebuild" + ], + "outputs": [ + "dist/**" + ], + "env": [ + "DATABASE_URL" ] }, "@snipcode/web#build": { @@ -66,7 +77,28 @@ ], "env": [ "NODE_ENV", - "NEXT_PUBLIC_SERVER_URL" + "NEXT_PUBLIC_SERVER_URL", + "NEXT_PUBLIC_APP_URL", + "NEXT_PUBLIC_APP_ENV" + ] + }, + "@snipcode/embed#build": { + "dependsOn": [ + "^build" + ], + "outputs": [ + "dist/**" + ], + "env": [ + "EMBED_JS_URL", + "EMBED_STYLE_URL", + "WEB_APP_URL", + "WEB_APP_SNIPPET_VIEW_URL" + ] + }, + "@snipcode/backend#test": { + "dependsOn": [ + "@snipcode/domain#test" ] }, "test": { diff --git a/yarn.lock b/yarn.lock index 25c909cf..8e96ce6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4514,6 +4514,7 @@ __metadata: "@types/express": "npm:4.17.21" express: "npm:4.19.1" nodemon: "npm:2.0.22" + prettier-2: "npm:prettier@^2" serve: "npm:14.2.1" shiki: "npm:0.14.7" tsup: "npm:8.0.2" @@ -8227,6 +8228,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:16.0.3": + version: 16.0.3 + resolution: "dotenv@npm:16.0.3" + checksum: 10/d6788c8e40b35ad9a9ca29249dccf37fa6b3ad26700fcbc87f2f41101bf914f5193a04e36a3d23de70b1dcb8e5d5a3b21e151debace2c4cd08d868be500a1b29 + languageName: node + linkType: hard + "dotenv@npm:16.4.5, dotenv@npm:^16.3.0": version: 16.4.5 resolution: "dotenv@npm:16.4.5" @@ -8696,6 +8704,17 @@ __metadata: languageName: node linkType: hard +"eslint-config-turbo@npm:2.0.3": + version: 2.0.3 + resolution: "eslint-config-turbo@npm:2.0.3" + dependencies: + eslint-plugin-turbo: "npm:2.0.3" + peerDependencies: + eslint: ">6.6.0" + checksum: 10/55f8169dfdc82308c3eb176b5c45fb3cf0cee40e390a837bd4a8db0fe83070c5f50af806d3c47f0ed534a6668519fedcf67bc1bf4033684d5bf4f185238b0a9e + languageName: node + linkType: hard + "eslint-import-resolver-node@npm:^0.3.6, eslint-import-resolver-node@npm:^0.3.9": version: 0.3.9 resolution: "eslint-import-resolver-node@npm:0.3.9" @@ -8913,6 +8932,17 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-turbo@npm:2.0.3": + version: 2.0.3 + resolution: "eslint-plugin-turbo@npm:2.0.3" + dependencies: + dotenv: "npm:16.0.3" + peerDependencies: + eslint: ">6.6.0" + checksum: 10/4ae4896be69cacca0c5b93fd4688b8feca74dc86910f275ce64dde9f5c58e55b99f90c938a97e7b7cce7f81e1684878b5d4e9e7904b82ab0707fde38a6c62b9c + languageName: node + linkType: hard + "eslint-plugin-typescript-sort-keys@npm:3.2.0": version: 3.2.0 resolution: "eslint-plugin-typescript-sort-keys@npm:3.2.0" @@ -13922,6 +13952,15 @@ __metadata: languageName: node linkType: hard +"prettier-2@npm:prettier@^2, prettier@npm:^2.7.1": + version: 2.8.8 + resolution: "prettier@npm:2.8.8" + bin: + prettier: bin-prettier.js + checksum: 10/00cdb6ab0281f98306cd1847425c24cbaaa48a5ff03633945ab4c701901b8e96ad558eb0777364ffc312f437af9b5a07d0f45346266e8245beaf6247b9c62b24 + languageName: node + linkType: hard + "prettier-linter-helpers@npm:^1.0.0": version: 1.0.0 resolution: "prettier-linter-helpers@npm:1.0.0" @@ -13940,15 +13979,6 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^2.7.1": - version: 2.8.8 - resolution: "prettier@npm:2.8.8" - bin: - prettier: bin-prettier.js - checksum: 10/00cdb6ab0281f98306cd1847425c24cbaaa48a5ff03633945ab4c701901b8e96ad558eb0777364ffc312f437af9b5a07d0f45346266e8245beaf6247b9c62b24 - languageName: node - linkType: hard - "pretty-format@npm:^27.0.2": version: 27.5.1 resolution: "pretty-format@npm:27.5.1" @@ -15325,6 +15355,7 @@ __metadata: dotenv-cli: "npm:7.4.2" eslint: "npm:8.56.0" eslint-config-prettier: "npm:9.1.0" + eslint-config-turbo: "npm:2.0.3" eslint-plugin-import: "npm:2.29.1" eslint-plugin-jest: "npm:27.9.0" eslint-plugin-prettier: "npm:5.1.3" From e6c311021a49f9efcecbf070d6921b7f8adfce4d Mon Sep 17 00:00:00 2001 From: Eric Cabrel TIOGO Date: Sun, 9 Jun 2024 16:20:08 +0200 Subject: [PATCH 6/6] test(backend): integration test for github authentication --- apps/backend/src/configs/exception.filter.ts | 12 +- apps/backend/src/features/auth/auth.module.ts | 3 +- .../src/features/auth/rest/auth.controller.ts | 15 +- .../rest/auth.provider.integration.spec.ts | 109 ++++++++ .../auth/services/github.service.test.ts | 159 ++++++++++++ .../features/auth/services/github.service.ts | 27 +- apps/backend/src/utils/tests/helpers.ts | 34 ++- apps/backend/tsconfig.json | 1 + package.json | 1 + packages/utils/src/error/error.ts | 2 +- yarn.lock | 234 +++++++++++++++++- 11 files changed, 571 insertions(+), 26 deletions(-) create mode 100644 apps/backend/src/features/auth/rest/auth.provider.integration.spec.ts create mode 100644 apps/backend/src/features/auth/services/github.service.test.ts diff --git a/apps/backend/src/configs/exception.filter.ts b/apps/backend/src/configs/exception.filter.ts index e92739bf..0e1b4271 100644 --- a/apps/backend/src/configs/exception.filter.ts +++ b/apps/backend/src/configs/exception.filter.ts @@ -2,6 +2,7 @@ import { ArgumentsHost, Catch } from '@nestjs/common'; import { AbstractHttpAdapter, BaseExceptionFilter } from '@nestjs/core'; import { GqlArgumentsHost, GqlContextType } from '@nestjs/graphql'; import { isAppError } from '@snipcode/utils'; +import { Response } from 'express'; import { GraphQLError } from 'graphql'; @Catch() @@ -23,7 +24,16 @@ export class ApplicationExceptionFilter extends BaseExceptionFilter { }); } } else { - // Handle HTTP exceptions + if (isAppError(exception)) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + response.status(400).json({ + code: exception.code, + message: exception.message, + timestamp: new Date().toISOString(), + }); + } super.catch(exception, host); } } diff --git a/apps/backend/src/features/auth/auth.module.ts b/apps/backend/src/features/auth/auth.module.ts index 26d50e0c..27bdfa9b 100644 --- a/apps/backend/src/features/auth/auth.module.ts +++ b/apps/backend/src/features/auth/auth.module.ts @@ -5,6 +5,7 @@ import { AuthController } from './rest/auth.controller'; import { GithubService } from './services/github.service'; @Module({ - providers: [AuthResolvers, GithubService, AuthController], + controllers: [AuthController], + providers: [AuthResolvers, GithubService], }) export class AuthFeatureModule {} diff --git a/apps/backend/src/features/auth/rest/auth.controller.ts b/apps/backend/src/features/auth/rest/auth.controller.ts index 7a17e46c..4c1de5fa 100644 --- a/apps/backend/src/features/auth/rest/auth.controller.ts +++ b/apps/backend/src/features/auth/rest/auth.controller.ts @@ -34,13 +34,11 @@ export class AuthController { const webAuthSuccessUrl = this.configService.get('WEB_AUTH_SUCCESS_URL'); const webAuthErrorUrl = this.configService.get('WEB_AUTH_ERROR_URL'); - const authResponse = await this.githubService.requestAccessTokenFromCode(requestToken); + const accessToken = await this.githubService.requestAccessTokenFromCode(requestToken); - const { access_token } = authResponse.data; + const githubUserData = await this.githubService.retrieveGitHubUserData(accessToken); - const userResponse = await this.githubService.retrieveGitHubUserData(access_token); - - const userExist = await this.userService.findByEmail(userResponse.data.email); + const userExist = await this.userService.findByEmail(githubUserData.email); if (userExist) { const sessionInput = new CreateSessionInput({ @@ -49,7 +47,7 @@ export class AuthController { }); const session = await this.sessionService.create(sessionInput); - const updateUserInput = this.githubService.generateUserUpdateInputFromGitHubData(userExist, userResponse.data); + const updateUserInput = this.githubService.generateUserUpdateInputFromGitHubData(userExist, githubUserData); await this.userService.update(userExist, updateUserInput); @@ -64,10 +62,7 @@ export class AuthController { return res.redirect(webAuthErrorUrl); } - const createUserInput = this.githubService.generateUserRegistrationInputFromGitHubData( - userResponse.data, - roleUser.id, - ); + const createUserInput = this.githubService.generateUserRegistrationInputFromGitHubData(githubUserData, roleUser.id); const createdUser = await this.userService.create(createUserInput); diff --git a/apps/backend/src/features/auth/rest/auth.provider.integration.spec.ts b/apps/backend/src/features/auth/rest/auth.provider.integration.spec.ts new file mode 100644 index 00000000..f79a013b --- /dev/null +++ b/apps/backend/src/features/auth/rest/auth.provider.integration.spec.ts @@ -0,0 +1,109 @@ +import * as url from 'node:url'; + +import { SessionService } from '@snipcode/domain'; +import { HttpResponse, http } from 'msw'; +import { setupServer } from 'msw/node'; +import request from 'supertest'; + +import { TestHelper } from '../../../utils/tests/helpers'; +import { TestServer, startTestServer } from '../../../utils/tests/server'; +import { GitHubUserResponse } from '../types'; + +const mockServer = setupServer( + http.post('https://github.com/login/oauth/access_token', ({ request }) => { + const url = new URL(request.url); + + const code = url.searchParams.get('code'); + const clientId = url.searchParams.get('client_id'); + const clientSecret = url.searchParams.get('client_secret'); + + if (!code || !clientId || !clientSecret) { + return HttpResponse.json({ message: 'Invalid request' }, { status: 400 }); + } + + if (code === 'valid_code') { + const data = { + access_token: 'valid_token', + }; + + return HttpResponse.json(data); + } + + return HttpResponse.json({ message: 'Invalid token' }, { status: 401 }); + }), + + http.get('https://api.github.com/user', ({ request }) => { + const authHeader = request.headers.get('Authorization'); + + if (authHeader === 'token valid_token') { + const user: GitHubUserResponse = { + avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4', + email: 'octocat@email.com', + login: 'octocat', + name: 'monalisa octocat', + }; + + return HttpResponse.json(user); + } + + return HttpResponse.json({ message: 'Invalid token' }, { status: 401 }); + }), +); + +describe('Test Authentication controller', () => { + let server: TestServer; + let testHelper: TestHelper; + let sessionService: SessionService; + + beforeAll(async () => { + server = await startTestServer(); + + sessionService = server.app.get(SessionService); + + testHelper = new TestHelper(server.app); + + mockServer.listen({ + onUnhandledRequest: (req) => { + if (req.url.includes('127.0.0.1') || req.url.includes('localhost')) { + return; + } + console.error(`No request mock for [${req.method}] ${req.url}`); + }, + }); + }); + + beforeEach(async () => { + await testHelper.cleanDatabase(); + }); + + afterEach(() => mockServer.resetHandlers()); + + afterAll(async () => { + mockServer.close(); + await server.close(); + }); + + test('Authenticate with GitHub', async () => { + const response = await request(server.app.getHttpServer()) + .get('/auth/github/callback?code=valid_code') + .send({}) + .expect(302); + + const parsedUrl = url.parse(response.headers.location, true); + + expect(`${parsedUrl.protocol}//${parsedUrl.host}${parsedUrl.pathname}`).toBe('http://localhost:7500/auth/success'); + expect(parsedUrl.query.token).toBeDefined(); + + const sessionToken = parsedUrl.query.token as string; + const session = await sessionService.findByToken(sessionToken); + + expect(session).toBeDefined(); + + const user = await testHelper.getAuthenticatedUser(sessionToken); + + expect(user).toMatchObject({ + id: expect.any(String), + rootFolderId: expect.any(String), + }); + }); +}); diff --git a/apps/backend/src/features/auth/services/github.service.test.ts b/apps/backend/src/features/auth/services/github.service.test.ts new file mode 100644 index 00000000..14aedc45 --- /dev/null +++ b/apps/backend/src/features/auth/services/github.service.test.ts @@ -0,0 +1,159 @@ +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { User } from '@snipcode/domain'; +import { HttpResponse, http } from 'msw'; +import { setupServer } from 'msw/node'; + +import { GithubService } from './github.service'; +import { GitHubUserResponse } from '../types'; + +const server = setupServer( + http.get('https://api.github.com/user', ({ request }) => { + const authHeader = request.headers.get('Authorization'); + + if (authHeader === 'token valid_token') { + const user: GitHubUserResponse = { + avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4', + email: 'octocat@email.com', + login: 'octocat', + name: 'monalisa octocat', + }; + + return HttpResponse.json(user); + } + + return HttpResponse.json({ message: 'Invalid token' }, { status: 401 }); + }), + + http.post('https://github.com/login/oauth/access_token', ({ request }) => { + const url = new URL(request.url); + + const code = url.searchParams.get('code'); + const clientId = url.searchParams.get('client_id'); + const clientSecret = url.searchParams.get('client_secret'); + + if (!code || !clientId || !clientSecret) { + return HttpResponse.json({ message: 'Invalid request' }, { status: 400 }); + } + + if (code === 'valid_code') { + const data = { + access_token: 'valid_token', + }; + + return HttpResponse.json(data); + } + + return HttpResponse.json({ message: 'Invalid token' }, { status: 401 }); + }), +); + +describe('Test GithubService', () => { + let githubService: GithubService; + + beforeAll(() => + server.listen({ + onUnhandledRequest: (req) => console.error(`No request mock for [${req.method}] ${req.url}`), + }), + ); + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + providers: [ConfigService, GithubService], + }).compile(); + + githubService = app.get(GithubService); + }); + + afterEach(() => server.resetHandlers()); + + afterAll(() => server.close()); + + it('should return user data when access token is valid', async () => { + const result = await githubService.retrieveGitHubUserData('valid_token'); + + expect(result).toEqual({ + avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4', + email: 'octocat@email.com', + login: 'octocat', + name: 'monalisa octocat', + }); + }); + + it('should throw an error when access token is invalid', async () => { + await expect(githubService.retrieveGitHubUserData('invalid_token')).rejects.toThrow(); + }); + + it('should return access token when code is valid', async () => { + const result = await githubService.requestAccessTokenFromCode('valid_code'); + + expect(result).toEqual('valid_token'); + }); + + it('should throw an error when code is invalid', async () => { + await expect(githubService.requestAccessTokenFromCode('invalid_code')).rejects.toThrow(); + }); + + it('should generate user registration input from GitHub data', () => { + const data: GitHubUserResponse = { + avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4', + email: 'octocat@email.com', + login: 'octocat', + name: 'monalisa octocat', + }; + + const result = githubService.generateUserRegistrationInputFromGitHubData(data, 'role_id'); + + expect(result).toMatchObject({ + _input: { + email: 'octocat@email.com', + name: 'monalisa octocat', + oauthProvider: 'github', + pictureUrl: 'https://avatars.githubusercontent.com/u/1?v=4', + roleId: 'role_id', + timezone: null, + username: 'octocat', + }, + enabled: true, + hashedPassword: null, + userId: expect.any(String), + }); + }); + + it('should generate user update input from GitHub data', () => { + const data: GitHubUserResponse = { + avatar_url: 'https://avatars.githubusercontent.com/u/2?v=4', + email: 'octocat@email.com', + login: 'octocat', + name: 'monalisa octocat 2', + }; + + const user: User = { + createdAt: new Date(), + email: 'octocat@email.com', + id: 'userId', + isEnabled: true, + name: 'octocat', + oauthProvider: 'github', + password: 'password', + pictureUrl: 'https://avatars.githubusercontent.com/u/1?v=4', + roleId: 'roleId', + timezone: null, + updatedAt: new Date(), + username: 'octocat', + }; + + const result = githubService.generateUserUpdateInputFromGitHubData(user, data); + + expect(result).toMatchObject({ + _input: { + name: 'monalisa octocat 2', + oauthProvider: 'github', + pictureUrl: 'https://avatars.githubusercontent.com/u/2?v=4', + roleId: 'roleId', + timezone: null, + }, + enabled: true, + }); + }); +}); diff --git a/apps/backend/src/features/auth/services/github.service.ts b/apps/backend/src/features/auth/services/github.service.ts index 66cbef3a..8621cea6 100644 --- a/apps/backend/src/features/auth/services/github.service.ts +++ b/apps/backend/src/features/auth/services/github.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { CreateUserInput, UpdateUserInput, User } from '@snipcode/domain'; +import { AppError } from '@snipcode/utils'; import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; import { EnvironmentVariables } from '../../../configs/environment'; @@ -15,7 +16,7 @@ export class GithubService { constructor(private readonly configService: ConfigService) {} - async requestAccessTokenFromCode(code: string) { + async requestAccessTokenFromCode(code: string): Promise { const authQueryObject = { client_id: this.configService.get('GITHUB_CLIENT_ID'), client_secret: this.configService.get('GITHUB_CLIENT_SECRET'), @@ -30,17 +31,29 @@ export class GithubService { const authQueryString = new URLSearchParams(Object.entries(authQueryObject)).toString(); - return this.httpClient.post(`${GITHUB_AUTH_URL}?${authQueryString}`, requestBody, requestConfig); + const response = await this.httpClient + .post<{ access_token: string }>(`${GITHUB_AUTH_URL}?${authQueryString}`, requestBody, requestConfig) + .catch((error) => { + throw new AppError(`Failed to authenticate with GitHub: ${error.response.data.message}`, 'LOGIN_FAILED'); + }); + + return response.data.access_token; } - async retrieveGitHubUserData(accessToken: string) { + async retrieveGitHubUserData(accessToken: string): Promise { const requestConfig: AxiosRequestConfig = { headers: { Authorization: `token ${accessToken}`, }, }; - return this.httpClient.get(GITHUB_API_USER_PROFILE_URL, requestConfig); + const response = await this.httpClient + .get(GITHUB_API_USER_PROFILE_URL, requestConfig) + .catch((error) => { + throw new AppError(`Failed to retrieve GitHub user data: ${error.response.data.message}`, 'LOGIN_FAILED'); + }); + + return response.data; } generateUserRegistrationInputFromGitHubData = (data: GitHubUserResponse, roleId: string): CreateUserInput => { @@ -64,12 +77,16 @@ export class GithubService { generateUserUpdateInputFromGitHubData = (user: User, data: GitHubUserResponse): UpdateUserInput => { const { avatar_url, name } = data; - return new UpdateUserInput({ + const updateUserInput = new UpdateUserInput({ name, oauthProvider: 'github', pictureUrl: avatar_url, roleId: user.roleId, timezone: user.timezone, }); + + updateUserInput.isEnabled = user.isEnabled; + + return updateUserInput; }; } diff --git a/apps/backend/src/utils/tests/helpers.ts b/apps/backend/src/utils/tests/helpers.ts index 822c5bec..ad825962 100644 --- a/apps/backend/src/utils/tests/helpers.ts +++ b/apps/backend/src/utils/tests/helpers.ts @@ -32,10 +32,14 @@ type CreateSnippetArgs = { }; export class TestHelper { + private graphqlEndpoint: string; + constructor( private readonly app: INestApplication, - private readonly graphqlEndpoint: string, - ) {} + graphqlEndpoint?: string, + ) { + this.graphqlEndpoint = graphqlEndpoint ?? '/graphql'; + } async cleanDatabase(): Promise { const prismaService = this.app.get(PrismaService); @@ -221,4 +225,30 @@ export class TestHelper { return response.body.data.createSnippet.id; } + + async getAuthenticatedUser(authToken: string) { + const authenticatedUserQuery = ` + query AuthenticatedUser { + authenticatedUser { + id + rootFolder { + id + } + } + } + `; + + const authenticatedUserResponse = await request(this.app.getHttpServer()) + .post(this.graphqlEndpoint) + .set('Authorization', authToken) + .send({ query: authenticatedUserQuery }) + .expect(200); + + const { authenticatedUser } = authenticatedUserResponse.body.data; + + return { + id: authenticatedUser.id, + rootFolderId: authenticatedUser.rootFolder.id, + }; + } } diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index 479528f4..23e86132 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -6,6 +6,7 @@ "outDir": "./dist", "incremental": true, "composite": true, + "sourceMap": true, "strictNullChecks": true }, "files": ["env.d.ts"], diff --git a/package.json b/package.json index 5f53da6d..9870326a 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "jest-mock-extended": "3.0.7", + "msw": "2.3.1", "prettier": "3.2.5", "prisma": "5.11.0", "ts-jest": "29.1.2", diff --git a/packages/utils/src/error/error.ts b/packages/utils/src/error/error.ts index d2556df0..de748cf4 100644 --- a/packages/utils/src/error/error.ts +++ b/packages/utils/src/error/error.ts @@ -26,7 +26,7 @@ export class AppError extends Error { public message: string, public code: AppErrorCode = 'INTERNAL_ERROR', ) { - super(); + super(message); } } diff --git a/yarn.lock b/yarn.lock index 8e96ce6b..5969bf17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1267,6 +1267,24 @@ __metadata: languageName: node linkType: hard +"@bundled-es-modules/cookie@npm:^2.0.0": + version: 2.0.0 + resolution: "@bundled-es-modules/cookie@npm:2.0.0" + dependencies: + cookie: "npm:^0.5.0" + checksum: 10/c8ef02aa5d3f6c786cfa407e1c93b4af29c600eb09990973f47a7a49e4771c1bec37c8f8e567638bb9cbc41f4e38d065ff1d8eaf9bf91f0c3613a6d60bc82c8c + languageName: node + linkType: hard + +"@bundled-es-modules/statuses@npm:^1.0.1": + version: 1.0.1 + resolution: "@bundled-es-modules/statuses@npm:1.0.1" + dependencies: + statuses: "npm:^2.0.1" + checksum: 10/9bf6a2bcf040a66fb805da0e1446041fd9def7468bb5da29c5ce02adf121a3f7cec123664308059a62a46fcaee666add83094b76df6dce72e5cafa8e6bebe60d + languageName: node + linkType: hard + "@changesets/apply-release-plan@npm:^7.0.1": version: 7.0.1 resolution: "@changesets/apply-release-plan@npm:7.0.1" @@ -2656,6 +2674,51 @@ __metadata: languageName: node linkType: hard +"@inquirer/confirm@npm:^3.0.0": + version: 3.1.9 + resolution: "@inquirer/confirm@npm:3.1.9" + dependencies: + "@inquirer/core": "npm:^8.2.2" + "@inquirer/type": "npm:^1.3.3" + checksum: 10/aa240ab879cc87c783229185ad34642414fb29a8bbdefdced5defa9e2fbbd030187224274d53b35365bfffa41c509cde08faf73c64af818a9cbb1b972d76986a + languageName: node + linkType: hard + +"@inquirer/core@npm:^8.2.2": + version: 8.2.2 + resolution: "@inquirer/core@npm:8.2.2" + dependencies: + "@inquirer/figures": "npm:^1.0.3" + "@inquirer/type": "npm:^1.3.3" + "@types/mute-stream": "npm:^0.0.4" + "@types/node": "npm:^20.12.13" + "@types/wrap-ansi": "npm:^3.0.0" + ansi-escapes: "npm:^4.3.2" + chalk: "npm:^4.1.2" + cli-spinners: "npm:^2.9.2" + cli-width: "npm:^4.1.0" + mute-stream: "npm:^1.0.0" + signal-exit: "npm:^4.1.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^6.2.0" + checksum: 10/63f7ed154b6a8d8b278d96755379ef46c8bae7a8593e1967d0e051164dd952806f1a9b66347886fe70ddf126eccafc27ff7a81ff9f6ec490e6a9cab4a35c367f + languageName: node + linkType: hard + +"@inquirer/figures@npm:^1.0.3": + version: 1.0.3 + resolution: "@inquirer/figures@npm:1.0.3" + checksum: 10/fa5c46527580c64ba151e1399f91772670f5f59e47045a3d2366188ed4cab1b63b7fb2a6d40d340f622cb174ca6dd3d5e22b962811c00548f9a9b4024b105dce + languageName: node + linkType: hard + +"@inquirer/type@npm:^1.3.3": + version: 1.3.3 + resolution: "@inquirer/type@npm:1.3.3" + checksum: 10/1de6fed6bca013d1d84c6f280c5cb5d1ac7788aed1bbdb3315977abda33dcea234e1e9b7d917fcad573192af9de12b1363c4ea4bf81318f6c45299e3521dbee6 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -3066,6 +3129,27 @@ __metadata: languageName: node linkType: hard +"@mswjs/cookies@npm:^1.1.0": + version: 1.1.0 + resolution: "@mswjs/cookies@npm:1.1.0" + checksum: 10/168ed1966e579a4f454e6d2af5a015150cca570ac4c660f5b656e7bc021afacbf0b3d4ed3d03e9293550f3965c28ce1e293fa7037c6cf46ed7e268e21a1053a4 + languageName: node + linkType: hard + +"@mswjs/interceptors@npm:^0.29.0": + version: 0.29.1 + resolution: "@mswjs/interceptors@npm:0.29.1" + dependencies: + "@open-draft/deferred-promise": "npm:^2.2.0" + "@open-draft/logger": "npm:^0.3.0" + "@open-draft/until": "npm:^2.0.0" + is-node-process: "npm:^1.2.0" + outvariant: "npm:^1.2.1" + strict-event-emitter: "npm:^0.5.1" + checksum: 10/6a6ee6eb3db0fed60bbeb710288f8c1e2cac84f08254756b684dbd553b04449dfe4cce1261fcc83772ee114be2043d9777e2ee6d72bc8d14fd394f961827e528 + languageName: node + linkType: hard + "@nestjs/apollo@npm:12.1.0": version: 12.1.0 resolution: "@nestjs/apollo@npm:12.1.0" @@ -3463,6 +3547,30 @@ __metadata: languageName: node linkType: hard +"@open-draft/deferred-promise@npm:^2.2.0": + version: 2.2.0 + resolution: "@open-draft/deferred-promise@npm:2.2.0" + checksum: 10/bc3bb1668a555bb87b33383cafcf207d9561e17d2ca0d9e61b7ce88e82b66e36a333d3676c1d39eb5848022c03c8145331fcdc828ba297f88cb1de9c5cef6c19 + languageName: node + linkType: hard + +"@open-draft/logger@npm:^0.3.0": + version: 0.3.0 + resolution: "@open-draft/logger@npm:0.3.0" + dependencies: + is-node-process: "npm:^1.2.0" + outvariant: "npm:^1.4.0" + checksum: 10/7a280f170bcd4e91d3eedbefe628efd10c3bd06dd2461d06a7fdbced89ef457a38785847f88cc630fb4fd7dfa176d6f77aed17e5a9b08000baff647433b5ff78 + languageName: node + linkType: hard + +"@open-draft/until@npm:^2.0.0, @open-draft/until@npm:^2.1.0": + version: 2.1.0 + resolution: "@open-draft/until@npm:2.1.0" + checksum: 10/622be42950afc8e89715d0fd6d56cbdcd13e36625e23b174bd3d9f06f80e25f9adf75d6698af93bca1e1bf465b9ce00ec05214a12189b671fb9da0f58215b6f4 + languageName: node + linkType: hard + "@opentelemetry/api-logs@npm:0.51.1": version: 0.51.1 resolution: "@opentelemetry/api-logs@npm:0.51.1" @@ -5178,6 +5286,15 @@ __metadata: languageName: node linkType: hard +"@types/mute-stream@npm:^0.0.4": + version: 0.0.4 + resolution: "@types/mute-stream@npm:0.0.4" + dependencies: + "@types/node": "npm:*" + checksum: 10/af8d83ad7b68ea05d9357985daf81b6c9b73af4feacb2f5c2693c7fd3e13e5135ef1bd083ce8d5bdc8e97acd28563b61bb32dec4e4508a8067fcd31b8a098632 + languageName: node + linkType: hard + "@types/mysql@npm:2.15.22": version: 2.15.22 resolution: "@types/mysql@npm:2.15.22" @@ -5231,6 +5348,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.12.13": + version: 20.14.2 + resolution: "@types/node@npm:20.14.2" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10/c38e47b190fa0a8bdfde24b036dddcf9401551f2fb170a90ff33625c7d6f218907e81c74e0fa6e394804a32623c24c60c50e249badc951007830f0d02c48ee0f + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.4 resolution: "@types/normalize-package-data@npm:2.4.4" @@ -5379,6 +5505,13 @@ __metadata: languageName: node linkType: hard +"@types/statuses@npm:^2.0.4": + version: 2.0.5 + resolution: "@types/statuses@npm:2.0.5" + checksum: 10/3f2609f660b45a878c6782f2fb2cef9f08bbd4e89194bf7512e747b8a73b056839be1ad6f64b1353765528cd8a5e93adeffc471cde24d0d9f7b528264e7154e5 + languageName: node + linkType: hard + "@types/superagent@npm:^8.1.0": version: 8.1.7 resolution: "@types/superagent@npm:8.1.7" @@ -5414,6 +5547,13 @@ __metadata: languageName: node linkType: hard +"@types/wrap-ansi@npm:^3.0.0": + version: 3.0.0 + resolution: "@types/wrap-ansi@npm:3.0.0" + checksum: 10/8aa644946ca4e859668c36b8e2bcf2ac4bdee59dac760414730ea57be8a93ae9166ebd40a088f2ab714843aaea2a2a67f0e6e6ec11cfc9c8701b2466ca1c4089 + languageName: node + linkType: hard + "@types/ws@npm:^8.0.0": version: 8.5.10 resolution: "@types/ws@npm:8.5.10" @@ -7284,7 +7424,7 @@ __metadata: languageName: node linkType: hard -"cli-spinners@npm:^2.5.0": +"cli-spinners@npm:^2.5.0, cli-spinners@npm:^2.9.2": version: 2.9.2 resolution: "cli-spinners@npm:2.9.2" checksum: 10/a0a863f442df35ed7294424f5491fa1756bd8d2e4ff0c8736531d886cec0ece4d85e8663b77a5afaf1d296e3cbbebff92e2e99f52bbea89b667cbe789b994794 @@ -7639,6 +7779,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^0.5.0": + version: 0.5.0 + resolution: "cookie@npm:0.5.0" + checksum: 10/aae7911ddc5f444a9025fbd979ad1b5d60191011339bce48e555cb83343d0f98b865ff5c4d71fecdfb8555a5cafdc65632f6fce172f32aaf6936830a883a0380 + languageName: node + linkType: hard + "cookiejar@npm:^2.1.4": version: 2.1.4 resolution: "cookiejar@npm:2.1.4" @@ -10032,7 +10179,7 @@ __metadata: languageName: node linkType: hard -"graphql@npm:16.8.1": +"graphql@npm:16.8.1, graphql@npm:^16.8.1": version: 16.8.1 resolution: "graphql@npm:16.8.1" checksum: 10/7a09d3ec5f75061afe2bd2421a2d53cf37273d2ecaad8f34febea1f1ac205dfec2834aec3419fa0a10fcc9fb345863b2f893562fb07ea825da2ae82f6392893c @@ -10134,6 +10281,13 @@ __metadata: languageName: node linkType: hard +"headers-polyfill@npm:^4.0.2": + version: 4.0.3 + resolution: "headers-polyfill@npm:4.0.3" + checksum: 10/3a008aa2ef71591e2077706efb48db1b2729b90cf646cc217f9b69744e35cca4ba463f39debb6000904aa7de4fada2e5cc682463025d26bcc469c1d99fa5af27 + languageName: node + linkType: hard + "hexoid@npm:^1.0.0": version: 1.0.0 resolution: "hexoid@npm:1.0.0" @@ -10701,6 +10855,13 @@ __metadata: languageName: node linkType: hard +"is-node-process@npm:^1.2.0": + version: 1.2.0 + resolution: "is-node-process@npm:1.2.0" + checksum: 10/930765cdc6d81ab8f1bbecbea4a8d35c7c6d88a3ff61f3630e0fc7f22d624d7661c1df05c58547d0eb6a639dfa9304682c8e342c4113a6ed51472b704cee2928 + languageName: node + linkType: hard + "is-number-object@npm:^1.0.4": version: 1.0.7 resolution: "is-number-object@npm:1.0.7" @@ -12798,6 +12959,38 @@ __metadata: languageName: node linkType: hard +"msw@npm:2.3.1": + version: 2.3.1 + resolution: "msw@npm:2.3.1" + dependencies: + "@bundled-es-modules/cookie": "npm:^2.0.0" + "@bundled-es-modules/statuses": "npm:^1.0.1" + "@inquirer/confirm": "npm:^3.0.0" + "@mswjs/cookies": "npm:^1.1.0" + "@mswjs/interceptors": "npm:^0.29.0" + "@open-draft/until": "npm:^2.1.0" + "@types/cookie": "npm:^0.6.0" + "@types/statuses": "npm:^2.0.4" + chalk: "npm:^4.1.2" + graphql: "npm:^16.8.1" + headers-polyfill: "npm:^4.0.2" + is-node-process: "npm:^1.2.0" + outvariant: "npm:^1.4.2" + path-to-regexp: "npm:^6.2.0" + strict-event-emitter: "npm:^0.5.1" + type-fest: "npm:^4.9.0" + yargs: "npm:^17.7.2" + peerDependencies: + typescript: ">= 4.7.x" + peerDependenciesMeta: + typescript: + optional: true + bin: + msw: cli/index.js + checksum: 10/449df7c48f82eaa3de4b40ca106be232b09dcf7f736b1bb7410109702f803262016db35247b299c1ec378346678f48d1d50752ee18fc90329c2531326cec7ec4 + languageName: node + linkType: hard + "multer@npm:1.4.4-lts.1": version: 1.4.4-lts.1 resolution: "multer@npm:1.4.4-lts.1" @@ -12820,7 +13013,7 @@ __metadata: languageName: node linkType: hard -"mute-stream@npm:1.0.0": +"mute-stream@npm:1.0.0, mute-stream@npm:^1.0.0": version: 1.0.0 resolution: "mute-stream@npm:1.0.0" checksum: 10/36fc968b0e9c9c63029d4f9dc63911950a3bdf55c9a87f58d3a266289b67180201cade911e7699f8b2fa596b34c9db43dad37649e3f7fdd13c3bb9edb0017ee7 @@ -13404,6 +13597,13 @@ __metadata: languageName: node linkType: hard +"outvariant@npm:^1.2.1, outvariant@npm:^1.4.0, outvariant@npm:^1.4.2": + version: 1.4.2 + resolution: "outvariant@npm:1.4.2" + checksum: 10/f16ba035fb65d1cbe7d2e06693dd42183c46bc8456713d9ddb5182d067defa7d78217edab0a2d3e173d3bacd627b2bd692195c7087c225b82548fbf52c677b38 + languageName: node + linkType: hard + "p-filter@npm:^2.1.0": version: 2.1.0 resolution: "p-filter@npm:2.1.0" @@ -13649,6 +13849,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^6.2.0": + version: 6.2.2 + resolution: "path-to-regexp@npm:6.2.2" + checksum: 10/f7d11c1a9e02576ce0294f4efdc523c11b73894947afdf7b23a0d0f7c6465d7a7772166e770ddf1495a8017cc0ee99e3e8a15ed7302b6b948b89a6dd4eea895e + languageName: node + linkType: hard + "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -15246,7 +15453,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" checksum: 10/c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f @@ -15366,6 +15573,7 @@ __metadata: jest: "npm:29.7.0" jest-environment-jsdom: "npm:29.7.0" jest-mock-extended: "npm:3.0.7" + msw: "npm:2.3.1" prettier: "npm:3.2.5" prisma: "npm:5.11.0" ts-jest: "npm:29.1.2" @@ -15562,7 +15770,7 @@ __metadata: languageName: node linkType: hard -"statuses@npm:2.0.1": +"statuses@npm:2.0.1, statuses@npm:^2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" checksum: 10/18c7623fdb8f646fb213ca4051be4df7efb3484d4ab662937ca6fbef7ced9b9e12842709872eb3020cc3504b93bde88935c9f6417489627a7786f24f8031cbcb @@ -15594,6 +15802,13 @@ __metadata: languageName: node linkType: hard +"strict-event-emitter@npm:^0.5.1": + version: 0.5.1 + resolution: "strict-event-emitter@npm:0.5.1" + checksum: 10/25c84d88be85940d3547db665b871bfecea4ea0bedfeb22aae8db48126820cfb2b0bc2fba695392592a09b1aa36b686d6eede499e1ecd151593c03fe5a50d512 + languageName: node + linkType: hard + "string-env-interpolation@npm:^1.0.1": version: 1.0.1 resolution: "string-env-interpolation@npm:1.0.1" @@ -16594,6 +16809,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^4.9.0": + version: 4.20.0 + resolution: "type-fest@npm:4.20.0" + checksum: 10/df037c11f6393312f27825ea6eb2c8cd62b1ba21c31144bed41854648ba2a18dcb8c68a930607c7227dd531b42006cc7c7a60f7f034668d1c92c205523ae1ea2 + languageName: node + linkType: hard + "type-is@npm:^1.6.4, type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" @@ -17480,7 +17702,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.7.1": +"yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.7.1, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: