Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,17 @@ export function makeAdminTokenSelect(requirements?: AuthRequirements): Prisma.Ad
return {
id: true,
name: true,
...(includeAdminRoleIds ? { owner: { select: { adminRoleIds: true } } } : {}),
...(includeUserType ? { owner: { select: { type: true } } } : {}),
...(includeAdminRoleIds && includeUserType ? { owner: { select: { adminRoleIds: true, type: true } } } : {}),
status: true,
expirationDate: true,
permissions: true,
owner: {
select: {
id: true,
...(includeAdminRoleIds ? { adminRoleIds: true } : {}),
...(includeUserType ? { type: true } : {}),
...(includeAdminRoleIds && includeUserType ? { adminRoleIds: true, type: true } : {}),
},
},
} satisfies Prisma.AdminTokenSelect
}

Expand Down
126 changes: 126 additions & 0 deletions apps/server-nestjs/test/admin-role.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import type { INestApplication } from '@nestjs/common'
import type { TestingModule } from '@nestjs/testing'
import { createHash } from 'node:crypto'
import { faker } from '@faker-js/faker'
import { ConfigModule } from '@nestjs/config'
import { Test } from '@nestjs/testing'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { baseConfigFactory } from '../src/config/base.config'
import { AdminRoleModule } from '../src/modules/admin-role/admin-role.module'
import { PrismaService } from '../src/modules/infrastructure/database/prisma.service'
import { getDotenvPaths } from '../src/utils/dotenv.utils'

const canRunAdminRoleE2E
= Boolean(process.env.E2E)

const describeWithAdminRole = describe.runIf(canRunAdminRoleE2E)

describeWithAdminRole('AdminRole HTTP (e2e)', () => {
let app: INestApplication
let baseUrl: string
let prisma: PrismaService
let tokenValue: string

let roleId: string

beforeAll(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AdminRoleModule],
}).compile()

await moduleRef.init()
prisma = moduleRef.get(PrismaService)

app = moduleRef.createNestApplication()
await app.listen(0)
const address = app.getHttpServer().address()
if (address === null || typeof address === 'string') throw new Error('HTTP server did not report a port')
baseUrl = `http://localhost:${address.port}`

tokenValue = faker.string.alphanumeric(48)
await prisma.user.create({
data: {
id: faker.string.uuid(),
email: faker.internet.email().toLowerCase(),
firstName: 'E2E',
lastName: 'AdminRoleToken',
type: 'human',
adminTokens: {
create: {
name: 'e2e-admin-role',
permissions: BigInt('0b10010'),
hash: createHash('sha256').update(tokenValue).digest('hex'),
},
},
},
})
})

afterAll(async () => {
if (roleId) {
await prisma.adminRole.deleteMany({ where: { id: roleId } })
}
await prisma.user.deleteMany({ where: { firstName: 'E2E', lastName: 'AdminRoleToken' } })
await app.close()
})

it('rejects a create without a name with 400', async () => {
const response = await fetch(`${baseUrl}/api/v1/admin/roles`, {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-dso-token': tokenValue },
body: JSON.stringify({}),
})

expect(response.status).toBe(400)
})

it('creates a role with 201 and lists it back', async () => {
const name = `e2e-${faker.string.alphanumeric({ length: 8, casing: 'lower' })}`
const response = await fetch(`${baseUrl}/api/v1/admin/roles`, {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-dso-token': tokenValue },
body: JSON.stringify({ name }),
})

expect(response.status).toBe(201)
const created: { id: string, name: string } = await response.json()
roleId = created.id
expect(created.name).toBe(name)

const listResponse = await fetch(`${baseUrl}/api/v1/admin/roles`, {
headers: { 'x-dso-token': tokenValue },
})
expect(listResponse.status).toBe(200)
const list: { id: string }[] = await listResponse.json()
expect(list.some(role => role.id === roleId)).toBe(true)
})

it('rejects a patch with a negative position with 400', async () => {
const response = await fetch(`${baseUrl}/api/v1/admin/roles`, {
method: 'PATCH',
headers: { 'content-type': 'application/json', 'x-dso-token': tokenValue },
body: JSON.stringify([{ id: faker.string.uuid(), position: -1 }]),
})

expect(response.status).toBe(400)
})

it('rejects a delete with a non-uuid roleId with 400', async () => {
const response = await fetch(`${baseUrl}/api/v1/admin/roles/not-an-uuid`, {
method: 'DELETE',
headers: { 'x-dso-token': tokenValue },
})

expect(response.status).toBe(400)
})

it('deletes a role with 204', async () => {
const response = await fetch(`${baseUrl}/api/v1/admin/roles/${roleId}`, {
method: 'DELETE',
headers: { 'x-dso-token': tokenValue },
})

expect(response.status).toBe(204)
roleId = ''
})
})
69 changes: 52 additions & 17 deletions apps/server-nestjs/test/admin-token.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,85 @@
import type { INestApplication } from '@nestjs/common'
import type { TestingModule } from '@nestjs/testing'
import { createHash } from 'node:crypto'
import { faker } from '@faker-js/faker'
import { ConfigModule } from '@nestjs/config'
import { Test } from '@nestjs/testing'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { mock } from 'vitest-mock-extended'
import { AdminTokenController } from '../src/modules/admin-token/admin-token.controller'
import { AdminTokenService } from '../src/modules/admin-token/admin-token.service'
import { UserGuard } from '../src/modules/infrastructure/permission/user/user.guard'
import { baseConfigFactory } from '../src/config/base.config'
import { AdminTokenModule } from '../src/modules/admin-token/admin-token.module'
import { PrismaService } from '../src/modules/infrastructure/database/prisma.service'
import { getDotenvPaths } from '../src/utils/dotenv.utils'

describe('AdminToken HTTP validation', () => {
const canRunAdminTokenE2E
= Boolean(process.env.E2E)

const describeWithAdminToken = describe.runIf(canRunAdminTokenE2E)

describeWithAdminToken('AdminToken HTTP (e2e)', () => {
let app: INestApplication
let baseUrl: string

let service: ReturnType<typeof mock<AdminTokenService>>
let prisma: PrismaService
let tokenValue: string

beforeAll(async () => {
service = mock<AdminTokenService>()
service.list.mockResolvedValue([])
const moduleRef: TestingModule = await Test.createTestingModule({
controllers: [AdminTokenController],
providers: [{ provide: AdminTokenService, useValue: service }],
})
.overrideGuard(UserGuard)
.useValue({ canActivate: () => true })
.compile()
imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AdminTokenModule],
}).compile()

await moduleRef.init()
prisma = moduleRef.get(PrismaService)

app = moduleRef.createNestApplication()
await app.listen(0)
const address = app.getHttpServer().address()
if (address === null || typeof address === 'string') throw new Error('HTTP server did not report a port')
baseUrl = `http://localhost:${address.port}`

tokenValue = faker.string.alphanumeric(48)
await prisma.user.create({
data: {
id: faker.string.uuid(),
email: faker.internet.email().toLowerCase(),
firstName: 'E2E',
lastName: 'AdminTokenSpec',
type: 'human',
adminTokens: {
create: {
name: 'e2e-admin-token',
permissions: BigInt('0b110000000000000000'),
hash: createHash('sha256').update(tokenValue).digest('hex'),
},
},
},
})
})

afterAll(async () => {
await prisma.user.deleteMany({ where: { firstName: 'E2E', lastName: 'AdminTokenSpec' } })
await app.close()
})

it('accepts GET without withRevoked', async () => {
const response = await fetch(`${baseUrl}/api/v1/admin/tokens`)
const response = await fetch(`${baseUrl}/api/v1/admin/tokens`, {
headers: { 'x-dso-token': tokenValue },
})

expect(response.status).toBe(200)
})

it('rejects a delete with a non-uuid tokenId with 400', async () => {
const response = await fetch(`${baseUrl}/api/v1/admin/tokens/not-an-uuid`, {
method: 'DELETE',
headers: { 'x-dso-token': tokenValue },
})

expect(response.status).toBe(400)
})

it('maps a too-close expiration date to a legacy-compatible 400', async () => {
const response = await fetch(`${baseUrl}/api/v1/admin/tokens`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
headers: { 'content-type': 'application/json', 'x-dso-token': tokenValue },
body: JSON.stringify({ name: 'legacy-parity', permissions: '4', expirationDate: new Date().toISOString() }),
})

Expand Down
35 changes: 26 additions & 9 deletions apps/server-nestjs/test/project-secrets.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { TestingModule } from '@nestjs/testing'
import { ENABLED } from '@cpn-console/shared'
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 { harborConfigFactory } from '../src/config/harbor.config'
import { nexusConfigFactory } from '../src/config/nexus.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'
Expand All @@ -29,6 +31,7 @@ describeWithProjectSecrets('ProjectSecretsService (e2e)', () => {
let vaultClient: VaultClientService
let projectsRootDir: string
let harborUrl: string
let nexusUrl: string

let ownerId: string
let projectId: string
Expand All @@ -54,6 +57,7 @@ describeWithProjectSecrets('ProjectSecretsService (e2e)', () => {
vaultClient = moduleRef.get(VaultClientService)
projectsRootDir = moduleRef.get(baseConfigFactory.KEY).projectsRootDir
harborUrl = moduleRef.get(harborConfigFactory.KEY).url
nexusUrl = moduleRef.get(nexusConfigFactory.KEY).url

ownerId = faker.string.uuid()
projectId = faker.string.uuid()
Expand Down Expand Up @@ -90,6 +94,12 @@ describeWithProjectSecrets('ProjectSecretsService (e2e)', () => {
prodMemory: 0,
everyonePerms: 0n,
lastSuccessProvisionningVersion: null,
plugins: {
create: [
{ pluginName: 'nexus', key: 'activateNpmRepo', value: ENABLED },
{ pluginName: 'nexus', key: 'activateMavenRepo', value: ENABLED },
],
},
},
})
})
Expand All @@ -107,9 +117,19 @@ describeWithProjectSecrets('ProjectSecretsService (e2e)', () => {
vi.unstubAllEnvs()
})

it('returns an empty secret map when no Vault secrets exist', async () => {
it('returns the synthesized Nexus URLs and the synthetic Vault group when no Vault secrets exist', async () => {
const secrets = await service.get(projectId)
expect(secrets).toEqual({})
expect(secrets).toEqual({
NEXUS: {
MAVEN_REPO_RELEASE: new URL(`${projectSlug}-repository-release`, nexusUrl).toString(),
MAVEN_REPO_SNAPSHOT: new URL(`${projectSlug}-repository-snapshot`, nexusUrl).toString(),
NPM_REPO: new URL(`${projectSlug}-npm`, nexusUrl).toString(),
},
VAULT: {
'.spec.mount': projectSlug,
'.spec.vaultAuthRef': 'vault-auth',
},
})
})

describe('when Vault secrets exist', () => {
Expand All @@ -119,10 +139,6 @@ describeWithProjectSecrets('ProjectSecretsService (e2e)', () => {
key1: 'value1',
key2: 42,
}, `${projectPath}/GITLAB`),
vaultClient.write({
key3: false,
key4: null,
}, `${projectPath}/NEXUS`),
vaultClient.write({
REGISTRY_ROBOT_SECRET: 'robot',
}, `${projectPath}/REGISTRY`),
Expand All @@ -144,17 +160,18 @@ describeWithProjectSecrets('ProjectSecretsService (e2e)', () => {
key1: 'value1',
key2: '42',
})
// NEXUS synthesizes repo URLs from config and per-project repo activation
expect(secrets.NEXUS).toEqual({
key3: 'false',
key4: '',
MAVEN_REPO_RELEASE: new URL(`${projectSlug}-repository-release`, nexusUrl).toString(),
MAVEN_REPO_SNAPSHOT: new URL(`${projectSlug}-repository-snapshot`, nexusUrl).toString(),
NPM_REPO: new URL(`${projectSlug}-npm`, nexusUrl).toString(),
})
const harborUrlObj = new URL(`${projectSlug}/`, harborUrl)
expect(secrets.REGISTRY).toEqual({
REGISTRY_ROBOT_SECRET: 'robot',
'Registry base path': `${harborUrlObj.host}${harborUrlObj.pathname}`,
})
expect(secrets.VAULT).toEqual({
VAULT_ROLE: 'role',
'.spec.mount': projectSlug,
'.spec.vaultAuthRef': 'vault-auth',
})
Expand Down
9 changes: 6 additions & 3 deletions apps/server-nestjs/test/project-services.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import { ProjectServicesModule } from '../src/modules/project-services/project-s
import { ProjectServicesService } from '../src/modules/project-services/project-services.service'
import { getDotenvPaths } from '../src/utils/dotenv.utils'

const canRunServicesE2E = Boolean(process.env.E2E)
const describeWithServices = describe.runIf(canRunServicesE2E)
const canRunProjectServicesE2E = Boolean(process.env.E2E)
const describeWithProjectServices = describe.runIf(canRunProjectServicesE2E)

describeWithServices('ProjectServicesService (e2e)', () => {
describeWithProjectServices('ProjectServicesService (e2e)', () => {
let moduleRef: TestingModule
let prisma: PrismaService
let service: ProjectServicesService
Expand All @@ -28,6 +28,9 @@ describeWithServices('ProjectServicesService (e2e)', () => {
let projectSlug: string

beforeAll(async () => {
vi.stubEnv('USE_GITLAB', 'true')
vi.stubEnv('USE_NEXUS', 'true')

moduleRef = await Test.createTestingModule({
imports: [ConfigModule.forRoot({ envFilePath: getDotenvPaths(), isGlobal: true, load: [baseConfigFactory] }), AuthModule, DatabaseModule, EventsModule, LoggerModule, PermissionModule, ProjectServicesModule],
}).compile()
Expand Down
Loading