diff --git a/apps/server-nestjs/test/argocd.e2e-spec.ts b/apps/server-nestjs/test/argocd.e2e-spec.ts index 67f42a78f6..9835165475 100644 --- a/apps/server-nestjs/test/argocd.e2e-spec.ts +++ b/apps/server-nestjs/test/argocd.e2e-spec.ts @@ -3,13 +3,13 @@ import type { ConfigType } from '@nestjs/config' import type { TestingModule } from '@nestjs/testing' import { faker } from '@faker-js/faker' import { ConfigModule } from '@nestjs/config' +import { EventEmitter2 } from '@nestjs/event-emitter' import { Test } from '@nestjs/testing' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import { parse } from 'yaml' import { baseConfigFactory } from '../src/config/base.config' import { projectSelect } from '../src/modules/argocd/argocd-datastore.service' import { ArgoCDModule } from '../src/modules/argocd/argocd.module' -import { ArgoCDService } from '../src/modules/argocd/argocd.service' import { GITLAB_REST_CLIENT, GitlabClientService } from '../src/modules/gitlab/gitlab-client.service' import { AuthModule } from '../src/modules/infrastructure/auth/auth.module' import { DatabaseModule } from '../src/modules/infrastructure/database/database.module' @@ -19,15 +19,16 @@ import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' import { VaultClientService } from '../src/modules/vault/vault-client.service' import { getDotenvPaths } from '../src/utils/dotenv.utils' +import { ARGOCD_RECONCILE_TIMEOUT, EXTERNAL_SYNC_TIMEOUT } from './e2e-timeout' const canRunArgoCDE2E = Boolean(process.env.E2E) const describeWithArgoCD = describe.runIf(canRunArgoCDE2E) -describeWithArgoCD('ArgoCDController (e2e)', {}, () => { +describeWithArgoCD('ArgoCDService (e2e)', () => { let moduleRef: TestingModule - let argocdController: ArgoCDService + let eventEmitter: EventEmitter2 let gitlab: GitlabClientService let gitlabClient: Gitlab let vault: VaultClientService @@ -60,11 +61,11 @@ describeWithArgoCD('ArgoCDController (e2e)', {}, () => { await moduleRef.init() - argocdController = moduleRef.get(ArgoCDService) gitlab = moduleRef.get(GitlabClientService) gitlabClient = moduleRef.get(GITLAB_REST_CLIENT) vault = moduleRef.get(VaultClientService) prisma = moduleRef.get(PrismaService) + eventEmitter = moduleRef.get(EventEmitter2) config = moduleRef.get(baseConfigFactory.KEY) ownerId = faker.string.uuid() @@ -212,7 +213,7 @@ describeWithArgoCD('ArgoCDController (e2e)', {}, () => { vaultProjectValuesPath = `${config.projectsRootDir}/${testProjectId}` await vault.write({ e2e: true }, vaultProjectValuesPath) - }) + }, ARGOCD_RECONCILE_TIMEOUT) afterAll(async () => { if (vaultProjectValuesPath) { @@ -252,7 +253,7 @@ describeWithArgoCD('ArgoCDController (e2e)', {}, () => { const staleAction = await gitlab.generateCreateOrUpdateAction(infraProject, 'main', staleFilePath, 'stale: true\n') await gitlab.maybeCreateCommit(infraProject, 'ci: :robot_face: Seed stale values', staleAction ? [staleAction] : []) - await argocdController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) const expectedFilePath = `${project.name}/${clusterLabel}/${envDevName}/values.yaml` const file = await gitlabClient.RepositoryFiles.show(infraRepoId, expectedFilePath, 'main') @@ -267,7 +268,7 @@ describeWithArgoCD('ArgoCDController (e2e)', {}, () => { const shouldBeDeleted = await gitlab.getFile(infraProject, staleFilePath, 'main') expect(shouldBeDeleted).toBeUndefined() - }, 144000) + }, ARGOCD_RECONCILE_TIMEOUT) it('should update existing values and delete values of a removed environment', async () => { const before = await prisma.project.findUniqueOrThrow({ @@ -294,7 +295,7 @@ describeWithArgoCD('ArgoCDController (e2e)', {}, () => { select: projectSelect, }) - await argocdController.handleUpsert(after) + await eventEmitter.emitAsync('project.upsert', after) const updatedDev = await gitlabClient.RepositoryFiles.show(infraRepoId, devFilePath, 'main') const devRaw = Buffer.from(updatedDev.content, 'base64').toString('utf8') @@ -304,5 +305,5 @@ describeWithArgoCD('ArgoCDController (e2e)', {}, () => { const prodFile = await gitlab.getFile(infraProject, prodFilePath, 'main') expect(prodFile).toBeUndefined() - }, 72000) + }, EXTERNAL_SYNC_TIMEOUT) }) diff --git a/apps/server-nestjs/test/e2e-timeout.ts b/apps/server-nestjs/test/e2e-timeout.ts new file mode 100644 index 0000000000..d4787dac14 --- /dev/null +++ b/apps/server-nestjs/test/e2e-timeout.ts @@ -0,0 +1,7 @@ +// e2e specs hit real external services; the latency lives in the operation, not the test shape. +// Each timeout names the task it bounds so a reader knows which system and operation it covers. +export const SONARQUBE_PROJECT_TIMEOUT = 30_000 // provision + delete a SonarQube project/user +export const KEYCLOAK_GROUP_SYNC_TIMEOUT = 60_000 // reconcile Keycloak groups/roles +export const EXTERNAL_SYNC_TIMEOUT = 72_000 // sync GitLab groups/members, teardown Nexus/registry +export const ARGOCD_RECONCILE_TIMEOUT = 144_000 // ArgoCD commit + sync to git +export const VAULT_PROVISION_TIMEOUT = 180_000 // provision Vault mounts/policies/approles, zone secrets diff --git a/apps/server-nestjs/test/gitlab.e2e-spec.ts b/apps/server-nestjs/test/gitlab.e2e-spec.ts index 32f51abf3d..884dddc781 100644 --- a/apps/server-nestjs/test/gitlab.e2e-spec.ts +++ b/apps/server-nestjs/test/gitlab.e2e-spec.ts @@ -3,6 +3,7 @@ import type { ConfigType } from '@nestjs/config' import type { TestingModule } from '@nestjs/testing' import { faker } from '@faker-js/faker' import { ConfigModule } from '@nestjs/config' +import { EventEmitter2 } from '@nestjs/event-emitter' import { Test } from '@nestjs/testing' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import z from 'zod' @@ -10,7 +11,6 @@ import { baseConfigFactory } from '../src/config/base.config' import { GITLAB_REST_CLIENT, GitlabClientService } from '../src/modules/gitlab/gitlab-client.service' import { projectSelect } from '../src/modules/gitlab/gitlab-datastore.service' import { GitlabModule } from '../src/modules/gitlab/gitlab.module' -import { GitlabService } from '../src/modules/gitlab/gitlab.service' import { AuthModule } from '../src/modules/infrastructure/auth/auth.module' import { DatabaseModule } from '../src/modules/infrastructure/database/database.module' import { PrismaService } from '../src/modules/infrastructure/database/prisma.service' @@ -19,16 +19,17 @@ import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' import { VaultClientService } from '../src/modules/vault/vault-client.service' import { getDotenvPaths } from '../src/utils/dotenv.utils' +import { EXTERNAL_SYNC_TIMEOUT } from './e2e-timeout' const canRunGitlabE2E = Boolean(process.env.E2E) const describeWithGitLab = describe.runIf(canRunGitlabE2E) -describeWithGitLab('GitlabController (e2e)', {}, () => { +describeWithGitLab('GitlabService (e2e)', () => { let moduleRef: TestingModule - let gitlabController: GitlabService - let gitlabService: GitlabClientService + let eventEmitter: EventEmitter2 + let gitlabClientService: GitlabClientService let gitlabClient: Gitlab let vaultService: VaultClientService let prisma: PrismaService @@ -46,11 +47,11 @@ describeWithGitLab('GitlabController (e2e)', {}, () => { await moduleRef.init() - gitlabController = moduleRef.get(GitlabService) - gitlabService = moduleRef.get(GitlabClientService) + gitlabClientService = moduleRef.get(GitlabClientService) gitlabClient = moduleRef.get(GITLAB_REST_CLIENT) vaultService = moduleRef.get(VaultClientService) prisma = moduleRef.get(PrismaService) + eventEmitter = moduleRef.get(EventEmitter2) config = moduleRef.get(baseConfigFactory.KEY) ownerId = faker.string.uuid() @@ -109,9 +110,9 @@ describeWithGitLab('GitlabController (e2e)', {}, () => { // Clean GitLab group if (testProjectSlug && config.projectsRootDir) { const fullPath = `${config.projectsRootDir}/${testProjectSlug}` - const group = await gitlabService.getGroupByPath(fullPath) + const group = await gitlabClientService.getGroupByPath(fullPath) if (group) { - await gitlabService.deleteGroup(group).catch(() => {}) + await gitlabClientService.deleteGroup(group).catch(() => {}) } } @@ -146,7 +147,7 @@ describeWithGitLab('GitlabController (e2e)', {}, () => { }) // Act - await gitlabController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) // Assert const groupPath = `${config.projectsRootDir}/${testProjectSlug}` @@ -155,11 +156,11 @@ describeWithGitLab('GitlabController (e2e)', {}, () => { name: z.string(), full_path: z.string(), web_url: z.string(), - }).parse(await gitlabService.getGroupByPath(groupPath)) + }).parse(await gitlabClientService.getGroupByPath(groupPath)) expect(group.full_path).toBe(groupPath) // Check membership - const members = await gitlabService.getGroupMembers(group) + const members = await gitlabClientService.getGroupMembers(group) const isMember = members.some(m => m.id === ownerUser.id) expect(isMember).toBe(true) @@ -167,7 +168,7 @@ describeWithGitLab('GitlabController (e2e)', {}, () => { const repoSecret = await vaultService.read(repoVaultPath) expect(repoSecret?.data?.GIT_OUTPUT_USER).toBeTruthy() expect(repoSecret?.data?.GIT_OUTPUT_PASSWORD).toBeTruthy() - }, 72000) + }, EXTERNAL_SYNC_TIMEOUT) describe('project members', () => { let newUserId: string | undefined @@ -219,18 +220,33 @@ describeWithGitLab('GitlabController (e2e)', {}, () => { select: projectSelect, }) - await gitlabController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) const groupPath = `${config.projectsRootDir}/${testProjectSlug}` const group = z.object({ id: z.number(), name: z.string(), web_url: z.string(), - }).parse(await gitlabService.getGroupByPath(groupPath)) + }).parse(await gitlabClientService.getGroupByPath(groupPath)) - const members = await gitlabService.getGroupMembers(group) + const members = await gitlabClientService.getGroupMembers(group) const isNewMemberPresent = members.some(m => m.id === newUserGitlabId) expect(isNewMemberPresent).toBe(true) - }, 72000) + }, EXTERNAL_SYNC_TIMEOUT) }) + + it('should remove project group from GitLab on delete', async () => { + const project = await prisma.project.findUniqueOrThrow({ + where: { id: testProjectId }, + select: projectSelect, + }) + + const groupPath = `${config.projectsRootDir}/${testProjectSlug}` + expect(await gitlabClientService.getGroupByPath(groupPath)).toBeTruthy() + + await eventEmitter.emitAsync('project.delete', project) + + const group = await gitlabClientService.getGroupByPath(groupPath) + expect(group).toBeUndefined() + }, EXTERNAL_SYNC_TIMEOUT) }) diff --git a/apps/server-nestjs/test/keycloak.e2e-spec.ts b/apps/server-nestjs/test/keycloak.e2e-spec.ts index b6efcd4e42..cb384a9870 100644 --- a/apps/server-nestjs/test/keycloak.e2e-spec.ts +++ b/apps/server-nestjs/test/keycloak.e2e-spec.ts @@ -3,6 +3,7 @@ import type { TestingModule } from '@nestjs/testing' import { faker } from '@faker-js/faker' import { Logger } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' +import { EventEmitter2 } from '@nestjs/event-emitter' import { Test } from '@nestjs/testing' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import z from 'zod' @@ -16,17 +17,17 @@ import { PermissionModule } from '../src/modules/infrastructure/permission/permi import { KEYCLOAK_ADMIN_CLIENT, KeycloakClientService } from '../src/modules/keycloak/keycloak-client.service' import { projectSelect } from '../src/modules/keycloak/keycloak-datastore.service' import { KeycloakModule } from '../src/modules/keycloak/keycloak.module' -import { KeycloakService } from '../src/modules/keycloak/keycloak.service' import { getDotenvPaths } from '../src/utils/dotenv.utils' +import { KEYCLOAK_GROUP_SYNC_TIMEOUT } from './e2e-timeout' const canRunKeycloakE2E = Boolean(process.env.E2E) const describeWithKeycloak = describe.runIf(canRunKeycloakE2E) -describeWithKeycloak('KeycloakController (e2e)', () => { +describeWithKeycloak('KeycloakService (e2e)', () => { let moduleRef: TestingModule - let keycloakController: KeycloakService + let eventEmitter: EventEmitter2 let keycloak: KeycloakClientService let keycloakAdminClient: KcAdminClient let prisma: PrismaService @@ -44,10 +45,10 @@ describeWithKeycloak('KeycloakController (e2e)', () => { await moduleRef.init() - keycloakController = moduleRef.get(KeycloakService) keycloak = moduleRef.get(KeycloakClientService) keycloakAdminClient = moduleRef.get(KEYCLOAK_ADMIN_CLIENT) prisma = moduleRef.get(PrismaService) + eventEmitter = moduleRef.get(EventEmitter2) ownerId = faker.string.uuid() testProjectId = faker.string.uuid() @@ -147,7 +148,7 @@ describeWithKeycloak('KeycloakController (e2e)', () => { }) // Act - await keycloakController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) // Assert // Check main project group @@ -173,7 +174,7 @@ describeWithKeycloak('KeycloakController (e2e)', () => { const members = await keycloak.getGroupMembers(projectGroup.id) const isMember = members.some(m => m.id === ownerId) expect(isMember).toBe(true) - }, 60000) + }, KEYCLOAK_GROUP_SYNC_TIMEOUT) it('should add member to project group when added in DB', async () => { // Create another user in Keycloak and DB @@ -215,7 +216,7 @@ describeWithKeycloak('KeycloakController (e2e)', () => { }) // Act - await keycloakController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) // Assert const projectGroup = z.object({ @@ -237,7 +238,7 @@ describeWithKeycloak('KeycloakController (e2e)', () => { await keycloakAdminClient.users.del({ id: kcUser.id }) await prisma.projectMembers.deleteMany({ where: { userId: kcUser.id } }) await prisma.user.delete({ where: { id: kcUser.id } }) - }, 60000) + }, KEYCLOAK_GROUP_SYNC_TIMEOUT) it('should remove member from project group when removed in DB', async () => { const newUserId = faker.string.uuid() @@ -278,7 +279,7 @@ describeWithKeycloak('KeycloakController (e2e)', () => { }) // Sync add - await keycloakController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) // Verify added const projectGroup = z.object({ @@ -303,7 +304,7 @@ describeWithKeycloak('KeycloakController (e2e)', () => { }) // Sync remove - await keycloakController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) // Verify removed members = await keycloak.getGroupMembers(projectGroup.id) @@ -313,7 +314,7 @@ describeWithKeycloak('KeycloakController (e2e)', () => { await keycloakAdminClient.users.del({ id: kcUser.id }) await prisma.projectMembers.deleteMany({ where: { userId: kcUser.id } }) await prisma.user.delete({ where: { id: kcUser.id } }) - }, 60000) + }, KEYCLOAK_GROUP_SYNC_TIMEOUT) it('should handle non-existent users gracefully', async () => { // Add a member in DB that does not exist in Keycloak @@ -343,12 +344,12 @@ describeWithKeycloak('KeycloakController (e2e)', () => { }) // Act - should not throw - await expect(keycloakController.handleUpsert(project)).resolves.not.toThrow() + await expect(eventEmitter.emitAsync('project.upsert', project)).resolves.not.toThrow() // Cleanup await prisma.projectMembers.deleteMany({ where: { userId: fakeUserId } }) await prisma.user.delete({ where: { id: fakeUserId } }) - }, 60000) + }, KEYCLOAK_GROUP_SYNC_TIMEOUT) it('should add user back to Keycloak group if missing but present in DB', async () => { // Create user and add to project in DB @@ -387,7 +388,7 @@ describeWithKeycloak('KeycloakController (e2e)', () => { }) // Sync to ensure they are added initially - await keycloakController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) const projectGroup = z.object({ id: z.string(), @@ -401,7 +402,7 @@ describeWithKeycloak('KeycloakController (e2e)', () => { expect(members.some(m => m.id === kcUser.id)).toBe(false) // Sync again - await keycloakController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) // Verify added back members = await keycloak.getGroupMembers(projectGroup.id) @@ -411,7 +412,7 @@ describeWithKeycloak('KeycloakController (e2e)', () => { await keycloakAdminClient.users.del({ id: kcUser.id }) await prisma.projectMembers.deleteMany({ where: { userId: kcUser.id } }) await prisma.user.delete({ where: { id: kcUser.id } }) - }, 60000) + }, KEYCLOAK_GROUP_SYNC_TIMEOUT) it('should remove user from Keycloak group if present but missing in DB', async () => { // Create user @@ -447,7 +448,7 @@ describeWithKeycloak('KeycloakController (e2e)', () => { }) // Sync to create group - await keycloakController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) // Manually add user to Keycloak group const projectGroup = z.object({ @@ -460,7 +461,7 @@ describeWithKeycloak('KeycloakController (e2e)', () => { expect(members.some(m => m.id === kcUser.id)).toBe(true) // Sync again to remove user - await keycloakController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) // Verify removed members = await keycloak.getGroupMembers(projectGroup.id) @@ -470,7 +471,7 @@ describeWithKeycloak('KeycloakController (e2e)', () => { await keycloakAdminClient.users.del({ id: kcUser.id }) await prisma.projectMembers.deleteMany({ where: { userId: kcUser.id } }) await prisma.user.delete({ where: { id: kcUser.id } }) - }, 60000) + }, KEYCLOAK_GROUP_SYNC_TIMEOUT) it('should recreate project group if deleted in Keycloak', async () => { // Ensure project exists and is synced @@ -478,7 +479,7 @@ describeWithKeycloak('KeycloakController (e2e)', () => { where: { id: testProjectId }, select: projectSelect, }) - await keycloakController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) const projectGroup = z.object({ id: z.string(), @@ -492,14 +493,14 @@ describeWithKeycloak('KeycloakController (e2e)', () => { expect(deletedProjectGroup).toBeUndefined() // Sync - await keycloakController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) // Verify recreated const recreatedProjectGroup = z.object({ name: z.string(), }).parse(await keycloak.getGroupByPath(`/${testProjectSlug}`)) expect(recreatedProjectGroup?.name).toBe(testProjectSlug) - }, 60000) + }, KEYCLOAK_GROUP_SYNC_TIMEOUT) it('should recreate role group if deleted in Keycloak', async () => { // Ensure project exists and is synced @@ -507,7 +508,7 @@ describeWithKeycloak('KeycloakController (e2e)', () => { where: { id: testProjectId }, select: projectSelect, }) - await keycloakController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) const roleGroup = z.object({ id: z.string(), @@ -521,12 +522,29 @@ describeWithKeycloak('KeycloakController (e2e)', () => { expect(deletedRoleGroup).toBeUndefined() // Sync - await keycloakController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) // Verify recreated const recreatedRoleGroup = z.object({ name: z.string(), }).parse(await keycloak.getGroupByPath(`/${testProjectSlug}/console/${testRoleName}`)) expect(recreatedRoleGroup?.name).toBe(testRoleName) - }, 60000) + }, KEYCLOAK_GROUP_SYNC_TIMEOUT) + + it('should remove project groups from Keycloak on delete', async () => { + const project = await prisma.project.findUniqueOrThrow({ + where: { id: testProjectId }, + select: projectSelect, + }) + + await eventEmitter.emitAsync('project.delete', project) + + expect(await keycloak.getGroupByPath(`/${testProjectSlug}`)).toBeTruthy() + + const deletedProjectGroup = await keycloak.getGroupByPath(`/${testProjectSlug}`) + expect(deletedProjectGroup).toBeUndefined() + + const deletedConsoleGroup = await keycloak.getGroupByPath(`/${testProjectSlug}/console`) + expect(deletedConsoleGroup).toBeUndefined() + }, KEYCLOAK_GROUP_SYNC_TIMEOUT) }) diff --git a/apps/server-nestjs/test/log.e2e-spec.ts b/apps/server-nestjs/test/log.e2e-spec.ts index 72214c56aa..4d81a4e725 100644 --- a/apps/server-nestjs/test/log.e2e-spec.ts +++ b/apps/server-nestjs/test/log.e2e-spec.ts @@ -1,7 +1,9 @@ import type { TestingModule } from '@nestjs/testing' import { faker } from '@faker-js/faker' +import { ConfigModule } from '@nestjs/config' import { Test } from '@nestjs/testing' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' +import { baseConfigFactory } from '../src/config/base.config' import { AuthModule } from '../src/modules/infrastructure/auth/auth.module' import { DatabaseModule } from '../src/modules/infrastructure/database/database.module' import { PrismaService } from '../src/modules/infrastructure/database/prisma.service' @@ -10,6 +12,7 @@ import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' import { LogModule } from '../src/modules/log/log.module' import { LogService } from '../src/modules/log/log.service' +import { getDotenvPaths } from '../src/utils/dotenv.utils' const canRunLogE2E = Boolean(process.env.E2E) @@ -29,7 +32,7 @@ describeWithLog('LogService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [LogModule, AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), LogModule, AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], }).compile() await moduleRef.init() @@ -156,32 +159,35 @@ describeWithLog('LogService (e2e)', () => { const { total: allTotal, logs: allLogs } = await logService.getLogs({ offset: 0, - limit: 10, + limit: 50, projectId: undefined, clean: true, }) expect(allTotal).toBeGreaterThan(2) - expect(allLogs).toHaveLength(10) - expect(allLogs[0]).toMatchObject({ + + const globalEntry = allLogs.find(log => log.id === globalLog.id) + expect(globalEntry).toMatchObject({ id: globalLog.id, action: 'global-upsert', userId: null, }) - expect(allLogs[0].data).not.toHaveProperty('args') - expect(allLogs[0].data).not.toHaveProperty('results') - expect(allLogs[0].data).not.toHaveProperty('config') - expect(allLogs[1]).toMatchObject({ + expect(globalEntry?.data).not.toHaveProperty('args') + expect(globalEntry?.data).not.toHaveProperty('results') + expect(globalEntry?.data).not.toHaveProperty('config') + + const projectEntry = allLogs.find(log => log.id === projectLog.id) + expect(projectEntry).toMatchObject({ id: projectLog.id, action: 'project-upsert', userId: ownerId, }) - expect(allLogs[1].data).toMatchObject({ + expect(projectEntry?.data).toMatchObject({ warning: ['careful'], totalExecutionTime: 42, messageResume: 'done', }) - expect(allLogs[1].data).not.toHaveProperty('args') - expect(allLogs[1].data).not.toHaveProperty('results') - expect(allLogs[1].data).not.toHaveProperty('config') + expect(projectEntry?.data).not.toHaveProperty('args') + expect(projectEntry?.data).not.toHaveProperty('results') + expect(projectEntry?.data).not.toHaveProperty('config') }) }) diff --git a/apps/server-nestjs/test/nexus.e2e-spec.ts b/apps/server-nestjs/test/nexus.e2e-spec.ts index d74d66ee82..5a680a404c 100644 --- a/apps/server-nestjs/test/nexus.e2e-spec.ts +++ b/apps/server-nestjs/test/nexus.e2e-spec.ts @@ -3,6 +3,7 @@ import type { TestingModule } from '@nestjs/testing' import { ENABLED } from '@cpn-console/shared' import { faker } from '@faker-js/faker' import { ConfigModule } from '@nestjs/config' +import { EventEmitter2 } from '@nestjs/event-emitter' import { Test } from '@nestjs/testing' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import { baseConfigFactory } from '../src/config/base.config' @@ -17,7 +18,6 @@ import { projectSelect } from '../src/modules/nexus/nexus-datastore.service' import { makeProjectWithDetails } from '../src/modules/nexus/nexus-testing.utils' import { NEXUS_CONFIG_KEY_ACTIVATE_MAVEN_REPO, NEXUS_CONFIG_KEY_ACTIVATE_NPM_REPO, PLUGIN_NAME } from '../src/modules/nexus/nexus.constants' import { NexusModule } from '../src/modules/nexus/nexus.module' -import { NexusService } from '../src/modules/nexus/nexus.service' import { generateNexusCredPath } from '../src/modules/nexus/nexus.utils' import { VaultClientService } from '../src/modules/vault/vault-client.service' import { VaultModule } from '../src/modules/vault/vault.module' @@ -28,9 +28,9 @@ const canRunNexusE2E const describeWithNexus = describe.runIf(canRunNexusE2E) -describeWithNexus('NexusController (e2e)', () => { +describeWithNexus('NexusService (e2e)', () => { let moduleRef: TestingModule - let nexusController: NexusService + let eventEmitter: EventEmitter2 let nexusClient: NexusClientService let vaultService: VaultClientService let config: ConfigType @@ -47,11 +47,11 @@ describeWithNexus('NexusController (e2e)', () => { await moduleRef.init() - nexusController = moduleRef.get(NexusService) nexusClient = moduleRef.get(NexusClientService) vaultService = moduleRef.get(VaultClientService) config = moduleRef.get(baseConfigFactory.KEY) prisma = moduleRef.get(PrismaService) + eventEmitter = moduleRef.get(EventEmitter2) ownerId = faker.string.uuid() testProjectId = faker.string.uuid() @@ -70,7 +70,7 @@ describeWithNexus('NexusController (e2e)', () => { afterAll(async () => { if (testProjectSlug) { - await nexusController.handleDelete(makeProjectWithDetails({ slug: testProjectSlug })).catch(() => {}) + await eventEmitter.emitAsync('project.delete', makeProjectWithDetails({ slug: testProjectSlug })).catch(() => {}) } if (prisma) { @@ -112,7 +112,7 @@ describeWithNexus('NexusController (e2e)', () => { select: projectSelect, }) - await nexusController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) const mavenReleaseRepo = `${testProjectSlug}-repository-release` const mavenSnapshotRepo = `${testProjectSlug}-repository-snapshot` @@ -147,4 +147,26 @@ describeWithNexus('NexusController (e2e)', () => { expect(secret.data?.NEXUS_USERNAME).toBe(testProjectSlug) expect(secret.data?.NEXUS_PASSWORD).toBeTruthy() }) + + it('should remove project from Nexus on delete', async () => { + const project = await prisma.project.findUniqueOrThrow({ + where: { id: testProjectId }, + select: projectSelect, + }) + + const mavenReleaseRepo = `${testProjectSlug}-repository-release` + expect(await nexusClient.getRepositoriesMavenHosted(mavenReleaseRepo)).not.toBeNull() + + await eventEmitter.emitAsync('project.delete', project) + + const repo = await nexusClient.getRepositoriesMavenHosted(mavenReleaseRepo) + expect(repo).toBeNull() + + const roleId = `${testProjectSlug}-ID` + const role = await nexusClient.getSecurityRoles(roleId) + expect(role).toBeNull() + + const users = await nexusClient.getSecurityUsers(testProjectSlug) + expect(users.some(u => u.userId === testProjectSlug)).toBe(false) + }) }) diff --git a/apps/server-nestjs/test/project-bulk.e2e-spec.ts b/apps/server-nestjs/test/project-bulk.e2e-spec.ts index 3a1e80f39d..8548971e55 100644 --- a/apps/server-nestjs/test/project-bulk.e2e-spec.ts +++ b/apps/server-nestjs/test/project-bulk.e2e-spec.ts @@ -15,11 +15,11 @@ import { ProjectBulkModule } from '../src/modules/project-bulk/project-bulk.modu import { ProjectBulkService } from '../src/modules/project-bulk/project-bulk.service' import { getDotenvPaths } from '../src/utils/dotenv.utils' -const canRunProjectBulkE2E = Boolean(process.env.E2E) && Boolean(process.env.DB_URL) +const canRunProjectBulkE2E = Boolean(process.env.E2E) const describeWithProjectBulk = describe.runIf(canRunProjectBulkE2E) -describeWithProjectBulk('ProjectBulkService (e2e)', {}, () => { +describeWithProjectBulk('ProjectBulkService (e2e)', () => { let moduleRef: TestingModule let prisma: PrismaService let service: ProjectBulkService diff --git a/apps/server-nestjs/test/project-hooks.e2e-spec.ts b/apps/server-nestjs/test/project-hooks.e2e-spec.ts index ab6e75184e..91d56e504f 100644 --- a/apps/server-nestjs/test/project-hooks.e2e-spec.ts +++ b/apps/server-nestjs/test/project-hooks.e2e-spec.ts @@ -19,11 +19,11 @@ import { VaultClientService } from '../src/modules/vault/vault-client.service' import { VaultService } from '../src/modules/vault/vault.service' import { getDotenvPaths } from '../src/utils/dotenv.utils' -const canRunProjectHooksE2E = Boolean(process.env.E2E) && Boolean(process.env.DB_URL) +const canRunProjectHooksE2E = Boolean(process.env.E2E) const describeWithProjectHooks = describe.runIf(canRunProjectHooksE2E) -describeWithProjectHooks('ProjectHooksService (e2e)', {}, () => { +describeWithProjectHooks('ProjectHooksService (e2e)', () => { let moduleRef: TestingModule let prisma: PrismaService let service: ProjectHooksService diff --git a/apps/server-nestjs/test/project-members.e2e-spec.ts b/apps/server-nestjs/test/project-members.e2e-spec.ts index 10dec97860..315bdba2dd 100644 --- a/apps/server-nestjs/test/project-members.e2e-spec.ts +++ b/apps/server-nestjs/test/project-members.e2e-spec.ts @@ -19,11 +19,11 @@ import { ProjectMembersModule } from '../src/modules/project-members/project-mem import { ProjectMembersService } from '../src/modules/project-members/project-members.service' import { getDotenvPaths } from '../src/utils/dotenv.utils' -const canRunProjectMembersE2E = Boolean(process.env.E2E) && Boolean(process.env.DB_URL) +const canRunProjectMembersE2E = Boolean(process.env.E2E) const describeWithProjectMembers = describe.runIf(canRunProjectMembersE2E) -describeWithProjectMembers('ProjectMembersService (e2e)', {}, () => { +describeWithProjectMembers('ProjectMembersService (e2e)', () => { let moduleRef: TestingModule let prisma: PrismaService let service: ProjectMembersService diff --git a/apps/server-nestjs/test/project-roles.e2e-spec.ts b/apps/server-nestjs/test/project-roles.e2e-spec.ts index 0f530ebf53..3f180330d0 100644 --- a/apps/server-nestjs/test/project-roles.e2e-spec.ts +++ b/apps/server-nestjs/test/project-roles.e2e-spec.ts @@ -16,11 +16,11 @@ import { ProjectRolesModule } from '../src/modules/project-roles/project-roles.m import { ProjectRolesService } from '../src/modules/project-roles/project-roles.service' import { getDotenvPaths } from '../src/utils/dotenv.utils' -const canRunProjectRolesE2E = Boolean(process.env.E2E) && Boolean(process.env.DB_URL) +const canRunProjectRolesE2E = Boolean(process.env.E2E) const describeWithProjectRoles = describe.runIf(canRunProjectRolesE2E) -describeWithProjectRoles('ProjectRolesService (e2e)', {}, () => { +describeWithProjectRoles('ProjectRolesService (e2e)', () => { let moduleRef: TestingModule let prisma: PrismaService let service: ProjectRolesService diff --git a/apps/server-nestjs/test/project-secrets.e2e-spec.ts b/apps/server-nestjs/test/project-secrets.e2e-spec.ts index 9eec3ec83f..52414baf75 100644 --- a/apps/server-nestjs/test/project-secrets.e2e-spec.ts +++ b/apps/server-nestjs/test/project-secrets.e2e-spec.ts @@ -21,7 +21,7 @@ const canRunProjectSecretsE2E const describeWithProjectSecrets = describe.runIf(canRunProjectSecretsE2E) -describeWithProjectSecrets('ProjectSecretsService (e2e)', {}, () => { +describeWithProjectSecrets('ProjectSecretsService (e2e)', () => { let moduleRef: TestingModule let prisma: PrismaService let service: ProjectSecretsService diff --git a/apps/server-nestjs/test/project-services.e2e-spec.ts b/apps/server-nestjs/test/project-services.e2e-spec.ts index 21a284ea91..4a8b41f222 100644 --- a/apps/server-nestjs/test/project-services.e2e-spec.ts +++ b/apps/server-nestjs/test/project-services.e2e-spec.ts @@ -11,18 +11,15 @@ import { PrismaService } from '../src/modules/infrastructure/database/prisma.ser import { EventsModule } from '../src/modules/infrastructure/events/events.module' import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module' import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' +import { NEXUS_CONFIG_KEY_ACTIVATE_NPM_REPO, PLUGIN_NAME } from '../src/modules/nexus/nexus.constants' import { ProjectServicesModule } from '../src/modules/project-services/project-services.module' import { ProjectServicesService } from '../src/modules/project-services/project-services.service' import { getDotenvPaths } from '../src/utils/dotenv.utils' -const canRunServicesE2E - = Boolean(process.env.E2E) - +const canRunServicesE2E = Boolean(process.env.E2E) const describeWithServices = describe.runIf(canRunServicesE2E) -const PLUGIN_NAME = 'gitlab' - -describeWithServices('ProjectServicesService (e2e)', {}, () => { +describeWithServices('ProjectServicesService (e2e)', () => { let moduleRef: TestingModule let prisma: PrismaService let service: ProjectServicesService @@ -103,8 +100,8 @@ describeWithServices('ProjectServicesService (e2e)', {}, () => { it('update stores project configuration', async () => { await service.update(projectId, { - gitlab: { - enabled: 'enabled', + nexus: { + activateNpmRepo: 'enabled', }, }, ['user']) @@ -113,7 +110,7 @@ describeWithServices('ProjectServicesService (e2e)', {}, () => { projectId_pluginName_key: { projectId, pluginName: PLUGIN_NAME, - key: 'user.enabled', + key: NEXUS_CONFIG_KEY_ACTIVATE_NPM_REPO, }, }, select: { diff --git a/apps/server-nestjs/test/project.e2e-spec.ts b/apps/server-nestjs/test/project.e2e-spec.ts index 0aa6b9011d..0a670aa45a 100644 --- a/apps/server-nestjs/test/project.e2e-spec.ts +++ b/apps/server-nestjs/test/project.e2e-spec.ts @@ -13,16 +13,16 @@ import { PrismaService } from '../src/modules/infrastructure/database/prisma.ser import { EventsModule } from '../src/modules/infrastructure/events/events.module' import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module' import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' -import { ProjectPermissionModule } from '../src/modules/infrastructure/permission/project/project.module' import { makeCreateProjectBody } from '../src/modules/project/project-testing.utils' +import { ProjectModule } from '../src/modules/project/project.module' import { ProjectService } from '../src/modules/project/project.service' import { getDotenvPaths } from '../src/utils/dotenv.utils' -const canRunProjectE2E = Boolean(process.env.E2E) && Boolean(process.env.DB_URL) +const canRunProjectE2E = Boolean(process.env.E2E) const describeWithProject = describe.runIf(canRunProjectE2E) -describeWithProject('ProjectService (e2e)', {}, () => { +describeWithProject('ProjectService (e2e)', () => { let moduleRef: TestingModule let prisma: PrismaService let service: ProjectService @@ -32,7 +32,7 @@ describeWithProject('ProjectService (e2e)', {}, () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectPermissionModule], + imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectModule], }).compile() await moduleRef.init() diff --git a/apps/server-nestjs/test/registry.e2e-spec.ts b/apps/server-nestjs/test/registry.e2e-spec.ts index d86e89d6c7..13200f8500 100644 --- a/apps/server-nestjs/test/registry.e2e-spec.ts +++ b/apps/server-nestjs/test/registry.e2e-spec.ts @@ -2,10 +2,12 @@ import type { ConfigType } from '@nestjs/config' import type { TestingModule } from '@nestjs/testing' import { faker } from '@faker-js/faker' import { ConfigModule } from '@nestjs/config' +import { EventEmitter2 } from '@nestjs/event-emitter' import { Test } from '@nestjs/testing' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { baseConfigFactory } from '../src/config/base.config' import { harborConfigFactory } from '../src/config/harbor.config' +import { EventsModule } from '../src/modules/infrastructure/events/events.module' import { RegistryClientService } from '../src/modules/registry/registry-client.service' import { makeProjectWithDetails } from '../src/modules/registry/registry-testing.utils' import { ROBOT_NAME_PROJECT, ROBOT_NAME_RO, ROBOT_NAME_RW } from '../src/modules/registry/registry.constants' @@ -23,6 +25,7 @@ const describeWithRegistry = describe.runIf(canRunRegistryE2E) describeWithRegistry('RegistryService (e2e)', () => { let moduleRef: TestingModule + let eventEmitter: EventEmitter2 let registry: RegistryService let client: RegistryClientService let vault: VaultClientService @@ -32,13 +35,14 @@ describeWithRegistry('RegistryService (e2e)', () => { beforeAll(async () => { moduleRef = await Test.createTestingModule({ - imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory, harborConfigFactory] }), RegistryModule], + imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory, harborConfigFactory] }), RegistryModule, EventsModule], }) .compile() await moduleRef.init() registry = moduleRef.get(RegistryService) + eventEmitter = moduleRef.get(EventEmitter2) client = moduleRef.get(RegistryClientService) vault = moduleRef.get(VaultClientService) config = moduleRef.get(baseConfigFactory.KEY) @@ -82,4 +86,11 @@ describeWithRegistry('RegistryService (e2e)', () => { expect(rwSecret.data?.USERNAME).toBe(`robot$${projectSlug}+${ROBOT_NAME_RW}`) expect(projectSecret.data?.USERNAME).toBe(`robot$${projectSlug}+${ROBOT_NAME_PROJECT}`) }) + + it('should remove project from Harbor on delete', async () => { + await eventEmitter.emitAsync('project.delete', makeProjectWithDetails({ slug: projectSlug })) + + const project = await client.getProjectByName(projectSlug) + expect(project.status).toBe(404) + }) }) diff --git a/apps/server-nestjs/test/sonarqube.e2e-spec.ts b/apps/server-nestjs/test/sonarqube.e2e-spec.ts index 21a287aa8f..af31d85b58 100644 --- a/apps/server-nestjs/test/sonarqube.e2e-spec.ts +++ b/apps/server-nestjs/test/sonarqube.e2e-spec.ts @@ -2,6 +2,7 @@ import type { TestingModule } from '@nestjs/testing' import { generateProjectKey } from '@cpn-console/hooks' import { faker } from '@faker-js/faker' import { ConfigModule } from '@nestjs/config' +import { EventEmitter2 } from '@nestjs/event-emitter' import { Test } from '@nestjs/testing' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import { baseConfigFactory } from '../src/config/base.config' @@ -20,6 +21,7 @@ import { VaultClientService } from '../src/modules/vault/vault-client.service' import { VaultModule } from '../src/modules/vault/vault.module' import { getDotenvPaths } from '../src/utils/dotenv.utils' import { getAll } from '../src/utils/iterable.utils' +import { SONARQUBE_PROJECT_TIMEOUT } from './e2e-timeout' const canRunSonarqubeE2E = Boolean(process.env.E2E) @@ -28,6 +30,7 @@ const describeWithSonarqube = describe.runIf(canRunSonarqubeE2E) describeWithSonarqube('SonarqubeService (e2e)', () => { let moduleRef: TestingModule + let eventEmitter: EventEmitter2 let sonarqubeService: SonarqubeService let sonarqubeClient: SonarqubeClientService let vaultService: VaultClientService @@ -49,6 +52,7 @@ describeWithSonarqube('SonarqubeService (e2e)', () => { sonarqubeClient = moduleRef.get(SonarqubeClientService) vaultService = moduleRef.get(VaultClientService) prisma = moduleRef.get(PrismaService) + eventEmitter = moduleRef.get(EventEmitter2) ownerId = faker.string.uuid() testProjectId = faker.string.uuid() @@ -68,9 +72,7 @@ describeWithSonarqube('SonarqubeService (e2e)', () => { afterAll(async () => { if (sonarqubeService && testProjectSlug) { - await sonarqubeService.handleDelete( - makeProjectWithDetails({ slug: testProjectSlug, repositories: [] }), - ).catch(() => {}) + await eventEmitter.emitAsync('project.delete', makeProjectWithDetails({ slug: testProjectSlug, repositories: [] })).catch(() => {}) } if (prisma) { @@ -128,7 +130,7 @@ describeWithSonarqube('SonarqubeService (e2e)', () => { select: projectSelect, }) - await sonarqubeService.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) // All 5 project role groups should exist in SonarQube const projectGroupNames = [ @@ -157,7 +159,7 @@ describeWithSonarqube('SonarqubeService (e2e)', () => { expect(vaultSecret?.data?.SONAR_USERNAME).toBe(testProjectSlug) expect(vaultSecret?.data?.SONAR_TOKEN).toBeTruthy() expect(vaultSecret?.data?.SONAR_PASSWORD).toBeTruthy() - }, 30000) + }, SONARQUBE_PROJECT_TIMEOUT) it('should delete the project from SonarQube and remove vault credentials', async () => { const project = await prisma.project.findUniqueOrThrow({ @@ -165,15 +167,18 @@ describeWithSonarqube('SonarqubeService (e2e)', () => { select: projectSelect, }) - await sonarqubeService.handleDelete(project) + const projectKey = generateProjectKey(testProjectSlug, testRepoName) + const projectsBefore = await getAll(sonarqubeClient.searchProject({ q: testProjectSlug })) + expect(projectsBefore.some(p => p.key === projectKey)).toBe(true) + + await eventEmitter.emitAsync('project.delete', project) // SonarQube analysis project should be removed - const projectKey = generateProjectKey(testProjectSlug, testRepoName) const projectsResult = await getAll(sonarqubeClient.searchProject({ q: testProjectSlug })) expect(projectsResult.some(p => p.key === projectKey)).toBe(false) // Vault credentials should be removed const vaultSecret = await vaultService.readSonarqubeUser(testProjectSlug) expect(vaultSecret).toBeNull() - }, 30000) + }, SONARQUBE_PROJECT_TIMEOUT) }) diff --git a/apps/server-nestjs/test/vault.e2e-spec.ts b/apps/server-nestjs/test/vault.e2e-spec.ts index 819c62dc92..73c484cafb 100644 --- a/apps/server-nestjs/test/vault.e2e-spec.ts +++ b/apps/server-nestjs/test/vault.e2e-spec.ts @@ -1,6 +1,7 @@ import type { TestingModule } from '@nestjs/testing' import { faker } from '@faker-js/faker' import { ConfigModule } from '@nestjs/config' +import { EventEmitter2 } from '@nestjs/event-emitter' import { Test } from '@nestjs/testing' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import { baseConfigFactory } from '../src/config/base.config' @@ -14,17 +15,17 @@ import { VaultClientService } from '../src/modules/vault/vault-client.service' import { projectSelect } from '../src/modules/vault/vault-datastore.service' import { makeProjectWithDetails } from '../src/modules/vault/vault-testing.utils' import { VaultModule } from '../src/modules/vault/vault.module' -import { VaultService } from '../src/modules/vault/vault.service' import { getDotenvPaths } from '../src/utils/dotenv.utils' +import { VAULT_PROVISION_TIMEOUT } from './e2e-timeout' const canRunVaultE2E = Boolean(process.env.E2E) const describeWithVault = describe.runIf(canRunVaultE2E) -describeWithVault('VaultController (e2e)', () => { +describeWithVault('VaultService (e2e)', () => { let moduleRef: TestingModule - let vaultController: VaultService + let eventEmitter: EventEmitter2 let vaultClient: VaultClientService let prisma: PrismaService @@ -39,9 +40,9 @@ describeWithVault('VaultController (e2e)', () => { await moduleRef.init() - vaultController = moduleRef.get(VaultService) vaultClient = moduleRef.get(VaultClientService) prisma = moduleRef.get(PrismaService) + eventEmitter = moduleRef.get(EventEmitter2) ownerId = faker.string.uuid() testProjectId = faker.string.uuid() @@ -60,7 +61,7 @@ describeWithVault('VaultController (e2e)', () => { afterAll(async () => { if (testProjectSlug) { - await vaultController.handleDelete(makeProjectWithDetails({ slug: testProjectSlug })).catch(() => {}) + await eventEmitter.emitAsync('project.delete', makeProjectWithDetails({ slug: testProjectSlug })).catch(() => {}) } if (prisma) { @@ -95,11 +96,25 @@ describeWithVault('VaultController (e2e)', () => { select: projectSelect, }) - await vaultController.handleUpsert(project) + await eventEmitter.emitAsync('project.upsert', project) - const group = await vaultClient.getIdentityGroupName(testProjectSlug) + const adminGroupName = `project-${testProjectSlug}-admin` + const group = await vaultClient.getIdentityGroupName(adminGroupName) expect(group.data?.id).toBeTruthy() - expect(group.data?.name).toBe(testProjectSlug) - expect(group.data?.alias?.name).toBe(`/${testProjectSlug}`) - }, 180000) + expect(group.data?.name).toBe(adminGroupName) + }, VAULT_PROVISION_TIMEOUT) + + it('should remove project from Vault on delete', async () => { + const project = await prisma.project.findUniqueOrThrow({ + where: { id: testProjectId }, + select: projectSelect, + }) + + const adminGroupName = `project-${testProjectSlug}-admin` + expect(await vaultClient.getIdentityGroupName(adminGroupName)).toBeTruthy() + + await eventEmitter.emitAsync('project.delete', project) + + await expect(vaultClient.getIdentityGroupName(adminGroupName)).rejects.toThrow('Not Found') + }, VAULT_PROVISION_TIMEOUT) }) diff --git a/apps/server-nestjs/test/zone.e2e-spec.ts b/apps/server-nestjs/test/zone.e2e-spec.ts new file mode 100644 index 0000000000..462359a575 --- /dev/null +++ b/apps/server-nestjs/test/zone.e2e-spec.ts @@ -0,0 +1,85 @@ +import type { TestingModule } from '@nestjs/testing' +import { faker } from '@faker-js/faker' +import { ConfigModule } from '@nestjs/config' +import { EventEmitter2 } from '@nestjs/event-emitter' +import { Test } from '@nestjs/testing' +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' +import { baseConfigFactory } from '../src/config/base.config' +import { AuthModule } from '../src/modules/infrastructure/auth/auth.module' +import { DatabaseModule } from '../src/modules/infrastructure/database/database.module' +import { PrismaService } from '../src/modules/infrastructure/database/prisma.service' +import { EventsModule } from '../src/modules/infrastructure/events/events.module' +import { LoggerModule } from '../src/modules/infrastructure/logger/logger.module' +import { PermissionModule } from '../src/modules/infrastructure/permission/permission.module' +import { VaultClientService } from '../src/modules/vault/vault-client.service' +import { makeZoneWithDetails } from '../src/modules/vault/vault-testing.utils' +import { VaultModule } from '../src/modules/vault/vault.module' +import { VaultService } from '../src/modules/vault/vault.service' +import { getDotenvPaths } from '../src/utils/dotenv.utils' +import { VAULT_PROVISION_TIMEOUT } from './e2e-timeout' + +const canRunZoneE2E = Boolean(process.env.E2E) + +const describeWithZone = describe.runIf(canRunZoneE2E) + +describeWithZone('Zone lifecycle (e2e)', () => { + let moduleRef: TestingModule + let eventEmitter: EventEmitter2 + let vaultService: VaultService + let vaultClient: VaultClientService + let prisma: PrismaService + + let zoneId: string + let zoneSlug: string + + beforeAll(async () => { + moduleRef = await Test.createTestingModule({ + imports: [VaultModule, ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule], + }).compile() + + await moduleRef.init() + + vaultService = moduleRef.get(VaultService) + vaultClient = moduleRef.get(VaultClientService) + prisma = moduleRef.get(PrismaService) + eventEmitter = moduleRef.get(EventEmitter2) + + zoneId = faker.string.uuid() + zoneSlug = faker.helpers.slugify(`test-zone-${faker.string.alphanumeric({ length: 10 }).toLowerCase()}`) + }) + + afterAll(async () => { + if (zoneSlug) { + await vaultService.deleteZone(zoneSlug).catch(() => {}) + } + + if (prisma) { + await prisma.zone.deleteMany({ where: { id: zoneId } }).catch(() => {}) + } + + await moduleRef?.close() + + vi.unstubAllEnvs() + }) + + it('should provision zone secrets space in Vault (mount, policy, approle)', async () => { + const zone = makeZoneWithDetails({ id: zoneId, slug: zoneSlug }) + + await eventEmitter.emitAsync('zone.upsert', zone) + + const kvName = `zone-${zoneSlug}` + const roleId = await vaultClient.getAuthApproleRoleRoleId(kvName) + expect(roleId).toBeTruthy() + }, VAULT_PROVISION_TIMEOUT) + + it('should remove zone from Vault on delete', async () => { + const zone = makeZoneWithDetails({ id: zoneId, slug: zoneSlug }) + + const kvName = `zone-${zoneSlug}` + expect(await vaultClient.getAuthApproleRoleRoleId(kvName)).toBeTruthy() + + await eventEmitter.emitAsync('zone.delete', zone) + + await expect(vaultClient.getAuthApproleRoleRoleId(kvName)).rejects.toThrow() + }, VAULT_PROVISION_TIMEOUT) +})