diff --git a/apps/server-nestjs/src/modules/nexus/nexus-client.service.spec.ts b/apps/server-nestjs/src/modules/nexus/nexus-client.service.spec.ts index a47f2bc7a5..9b343008c2 100644 --- a/apps/server-nestjs/src/modules/nexus/nexus-client.service.spec.ts +++ b/apps/server-nestjs/src/modules/nexus/nexus-client.service.spec.ts @@ -1,31 +1,29 @@ import type { ConfigType } from '@nestjs/config' -import type { DeepMockProxy } from 'vitest-mock-extended' import { faker } from '@faker-js/faker' -import { HttpStatus } from '@nestjs/common' import { Test } from '@nestjs/testing' -import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' import { mockDeep } from 'vitest-mock-extended' import { nexusConfigFactory } from '../../config/nexus.config' import { NexusClientService } from './nexus-client.service' import { NexusHttpClientService } from './nexus-http-client.service' - -const nexusUrl = 'https://nexus.internal' +import { makeNexusDb, makeNexusHandlers, NEXUS_INTERNAL_URL } from './nexus-testing.utils' const server = setupServer() const nexusAdminPassword = faker.internet.password() -const basicAuth = `Basic ${Buffer.from(`admin:${nexusAdminPassword}`, 'utf8').toString('base64')}` describe('nexusClientService', () => { let service: NexusClientService - let config: DeepMockProxy> + let db: ReturnType beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) beforeEach(async () => { - config = mockDeep>({ - internalUrl: nexusUrl, + db = makeNexusDb() + server.use(...makeNexusHandlers(db)) + + const config = mockDeep>({ + internalUrl: NEXUS_INTERNAL_URL, admin: 'admin', adminPassword: nexusAdminPassword, }) @@ -51,30 +49,33 @@ describe('nexusClientService', () => { expect(service).toBeDefined() }) - it('should return null on 404 (getRepositoriesMavenHosted)', async () => { - server.use( - http.get(`${nexusUrl}/service/rest/v1/repositories/maven/hosted/:name`, ({ request }) => { - expect(request.headers.get('authorization')).toBe(basicAuth) - return HttpResponse.json({}, { status: HttpStatus.NOT_FOUND }) - }), - ) - + it('should return null on missing repository (getRepositoriesMavenHosted)', async () => { await expect(service.getRepositoriesMavenHosted('missing')).resolves.toBeNull() }) - it('should send basic auth and plain text body on change-password', async () => { - server.use( - http.put(`${nexusUrl}/service/rest/v1/security/users/:userId/change-password`, async ({ request, params }) => { - expect(request.method).toBe('PUT') - expect(request.url).toBe(`${nexusUrl}/service/rest/v1/security/users/u1/change-password`) - expect(params.userId).toBe('u1') - expect(request.headers.get('authorization')).toBe(basicAuth) - expect(request.headers.get('content-type')).toContain('text/plain') - expect(await request.text()).toBe('pw123') - return new HttpResponse(null, { status: HttpStatus.NO_CONTENT }) - }), - ) + it('should read and create repositories through the fake nexus', async () => { + await db.mavenHosted.create({ + name: 'maven-existing', + online: true, + storage: { blobStoreName: 'default', strictContentTypeValidation: true, writePolicy: 'ALLOW' }, + component: { proprietaryComponents: false }, + maven: { versionPolicy: 'RELEASE', layoutPolicy: 'PERMISSIVE', contentDisposition: 'ATTACHMENT' }, + }) + + await expect(service.getRepositoriesMavenHosted('maven-existing')).resolves.toMatchObject({ name: 'maven-existing' }) + + await service.createRepositoriesNpmHosted({ + name: 'npm-hosted-new', + online: true, + storage: { blobStoreName: 'default', strictContentTypeValidation: true, writePolicy: 'ALLOW' }, + cleanup: { policyNames: [] }, + component: { proprietaryComponents: false }, + }) + + await expect(service.getRepositoriesNpmHosted('npm-hosted-new')).resolves.toMatchObject({ name: 'npm-hosted-new' }) + }) + it('should accept a change-password call', async () => { await service.updateSecurityUsersChangePassword('u1', 'pw123') }) }) diff --git a/apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts b/apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts index 4cdc734919..14e9fc0e13 100644 --- a/apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts +++ b/apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts @@ -1,5 +1,11 @@ +import type { HttpHandler } from 'msw' import type { ProjectWithDetails } from './nexus-datastore.service' import { faker } from '@faker-js/faker' +import { Collection } from '@msw/data' +import { http, HttpResponse } from 'msw' +import { z } from 'zod' + +export const NEXUS_INTERNAL_URL = 'https://nexus.internal' export function makeProjectWithDetails(overrides: Partial = {}): ProjectWithDetails { return { @@ -13,3 +19,187 @@ export function makeProjectWithDetails(overrides: Partial = ...overrides, } satisfies ProjectWithDetails } + +const repositorySchema = z.object({ + name: z.string(), + online: z.boolean(), + storage: z.object({ + blobStoreName: z.string(), + strictContentTypeValidation: z.boolean(), + writePolicy: z.string(), + }), + cleanup: z.object({ policyNames: z.array(z.string()) }).optional(), + component: z.object({ proprietaryComponents: z.boolean() }).optional(), + maven: z.object({ + versionPolicy: z.string(), + layoutPolicy: z.string(), + contentDisposition: z.string(), + }).optional(), + group: z.object({ memberNames: z.array(z.string()) }).optional(), +}) + +const privilegeSchema = z.object({ + name: z.string(), + description: z.string(), + actions: z.array(z.string()), + format: z.string(), + repository: z.string(), + type: z.string().optional(), +}) + +const roleSchema = z.object({ + id: z.string(), + name: z.string(), + privileges: z.array(z.string()), + source: z.string().optional(), + roles: z.array(z.string()).optional(), + description: z.string().optional(), +}) + +const userSchema = z.object({ + userId: z.string(), + firstName: z.string().optional(), + lastName: z.string().optional(), + emailAddress: z.string().optional(), + status: z.string().optional(), + roles: z.array(z.string()).optional(), +}) + +/** + * In-memory fake of the Nexus API surface used by NexusClientService. + * Seed the collections, pass them to makeNexusHandlers, then assert + * against the same collections in the spec. + */ +export function makeNexusDb() { + return { + mavenHosted: new Collection({ schema: repositorySchema }), + mavenGroup: new Collection({ schema: repositorySchema }), + npmHosted: new Collection({ schema: repositorySchema }), + npmGroup: new Collection({ schema: repositorySchema }), + privileges: new Collection({ schema: privilegeSchema }), + roles: new Collection({ schema: roleSchema }), + users: new Collection({ schema: userSchema }), + } +} + +export function makeNexusHandlers(db: ReturnType): HttpHandler[] { + const url = `${NEXUS_INTERNAL_URL}/service/rest/v1` + + const getOr404 = async (collection: Collection, name: string) => { + const data = await collection.findFirst((q: any) => q.where({ name })) + if (!data) return HttpResponse.json({}, { status: 404 }) + return HttpResponse.json(data) + } + + return [ + http.get(`${url}/repositories/maven/hosted/:name`, async ({ params }) => getOr404(db.mavenHosted, String(params.name))), + http.post(`${url}/repositories/maven/hosted`, async ({ request }) => { + await db.mavenHosted.create(await request.json() as any) + return new HttpResponse(null, { status: 204 }) + }), + http.put(`${url}/repositories/maven/hosted/:name`, async ({ request, params }) => { + const data = await request.json() as any + const record = await db.mavenHosted.findFirst((q: any) => q.where({ name: params.name })) + if (record) await db.mavenHosted.update(record, { data: () => data }) + else await db.mavenHosted.create(data) + return new HttpResponse(null, { status: 204 }) + }), + http.get(`${url}/repositories/maven/group/:name`, async ({ params }) => getOr404(db.mavenGroup, String(params.name))), + http.post(`${url}/repositories/maven/group`, async ({ request }) => { + await db.mavenGroup.create(await request.json() as any) + return new HttpResponse(null, { status: 204 }) + }), + http.put(`${url}/repositories/maven/group/:name`, async ({ request, params }) => { + const data = await request.json() as any + const record = await db.mavenGroup.findFirst((q: any) => q.where({ name: params.name })) + if (record) await db.mavenGroup.update(record, { data: () => data }) + else await db.mavenGroup.create(data) + return new HttpResponse(null, { status: 204 }) + }), + http.get(`${url}/repositories/npm/hosted/:name`, async ({ params }) => getOr404(db.npmHosted, String(params.name))), + http.post(`${url}/repositories/npm/hosted`, async ({ request }) => { + await db.npmHosted.create(await request.json() as any) + return new HttpResponse(null, { status: 204 }) + }), + http.put(`${url}/repositories/npm/hosted/:name`, async ({ request, params }) => { + const data = await request.json() as any + const record = await db.npmHosted.findFirst((q: any) => q.where({ name: params.name })) + if (record) await db.npmHosted.update(record, { data: () => data }) + else await db.npmHosted.create(data) + return new HttpResponse(null, { status: 204 }) + }), + http.get(`${url}/repositories/npm/group/:name`, async ({ params }) => getOr404(db.npmGroup, String(params.name))), + http.post(`${url}/repositories/npm/group`, async ({ request }) => { + await db.npmGroup.create(await request.json() as any) + return new HttpResponse(null, { status: 204 }) + }), + http.put(`${url}/repositories/npm/group/:name`, async ({ request, params }) => { + const data = await request.json() as any + const record = await db.npmGroup.findFirst((q: any) => q.where({ name: params.name })) + if (record) await db.npmGroup.update(record, { data: () => data }) + else await db.npmGroup.create(data) + return new HttpResponse(null, { status: 204 }) + }), + http.get(`${url}/security/privileges/:name`, async ({ params }) => getOr404(db.privileges, String(params.name))), + http.post(`${url}/security/privileges/repository-view`, async ({ request }) => { + await db.privileges.create(await request.json() as any) + return new HttpResponse(null, { status: 204 }) + }), + http.put(`${url}/security/privileges/repository-view/:name`, async ({ request, params }) => { + const data = await request.json() as any + const record = await db.privileges.findFirst((q: any) => q.where({ name: params.name })) + if (record) await db.privileges.update(record, { data: () => data }) + else await db.privileges.create(data) + return new HttpResponse(null, { status: 204 }) + }), + http.delete(`${url}/security/privileges/:name`, async ({ params }) => { + await db.privileges.deleteMany((q: any) => q.where({ name: params.name })) + return new HttpResponse(null, { status: 204 }) + }), + http.get(`${url}/security/roles/:id`, async ({ params }) => { + const data = await db.roles.findFirst((q: any) => q.where({ id: params.id })) + if (!data) return HttpResponse.json({}, { status: 404 }) + return HttpResponse.json(data) + }), + http.post(`${url}/security/roles`, async ({ request }) => { + await db.roles.create(await request.json() as any) + return new HttpResponse(null, { status: 204 }) + }), + http.put(`${url}/security/roles/:id`, async ({ request, params }) => { + const data = await request.json() as any + const record = await db.roles.findFirst((q: any) => q.where({ id: params.id })) + if (record) await db.roles.update(record, { data: () => data }) + else await db.roles.create(data) + return new HttpResponse(null, { status: 204 }) + }), + http.delete(`${url}/security/roles/:id`, async ({ params }) => { + await db.roles.deleteMany((q: any) => q.where({ id: params.id })) + return new HttpResponse(null, { status: 204 }) + }), + http.get(`${url}/security/users`, async ({ request }) => { + const userId = new URL(request.url).searchParams.get('userId') + const users = userId + ? await db.users.findMany((q: any) => q.where({ userId })) + : await db.users.findMany() + return HttpResponse.json(users) + }), + http.put(`${url}/security/users/:userId/change-password`, () => new HttpResponse(null, { status: 204 })), + http.post(`${url}/security/users`, async ({ request }) => { + await db.users.create(await request.json() as any) + return new HttpResponse(null, { status: 204 }) + }), + http.delete(`${url}/security/users/:userId`, async ({ params }) => { + await db.users.deleteMany((q: any) => q.where({ userId: params.userId })) + return new HttpResponse(null, { status: 204 }) + }), + http.delete(`${url}/repositories/:name`, async ({ params }) => { + await Promise.all([ + db.mavenHosted.deleteMany((q: any) => q.where({ name: params.name })), + db.mavenGroup.deleteMany((q: any) => q.where({ name: params.name })), + db.npmHosted.deleteMany((q: any) => q.where({ name: params.name })), + db.npmGroup.deleteMany((q: any) => q.where({ name: params.name })), + ]) + return new HttpResponse(null, { status: 204 }) + }), + ] +}