Skip to content
Draft
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
59 changes: 30 additions & 29 deletions apps/server-nestjs/src/modules/nexus/nexus-client.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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<ConfigType<typeof nexusConfigFactory>>
let db: ReturnType<typeof makeNexusDb>

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))

beforeEach(async () => {
config = mockDeep<ConfigType<typeof nexusConfigFactory>>({
internalUrl: nexusUrl,
db = makeNexusDb()
server.use(...makeNexusHandlers(db))

const config = mockDeep<ConfigType<typeof nexusConfigFactory>>({
internalUrl: NEXUS_INTERNAL_URL,
admin: 'admin',
adminPassword: nexusAdminPassword,
})
Expand All @@ -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')
})
})
190 changes: 190 additions & 0 deletions apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts
Original file line number Diff line number Diff line change
@@ -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> = {}): ProjectWithDetails {
return {
Expand All @@ -13,3 +19,187 @@
...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<typeof makeNexusDb>): HttpHandler[] {
const url = `${NEXUS_INTERNAL_URL}/service/rest/v1`

const getOr404 = async (collection: Collection<any>, 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 }))

Check failure on line 156 in apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts

View check run for this annotation

cloud-pi-native-sonarqube / SonarQube Code Analysis

apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts#L156

Unexpected `await` of a non-Promise (non-"Thenable") value.
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 }))

Check failure on line 176 in apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts

View check run for this annotation

cloud-pi-native-sonarqube / SonarQube Code Analysis

apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts#L176

Unexpected `await` of a non-Promise (non-"Thenable") value.
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 }))

Check failure on line 182 in apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts

View check run for this annotation

cloud-pi-native-sonarqube / SonarQube Code Analysis

apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts#L182

Unexpected `await` of a non-Promise (non-"Thenable") value.
: await db.users.findMany()

Check failure on line 183 in apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts

View check run for this annotation

cloud-pi-native-sonarqube / SonarQube Code Analysis

apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts#L183

Unexpected `await` of a non-Promise (non-"Thenable") value.
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 }))

Check failure on line 192 in apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts

View check run for this annotation

cloud-pi-native-sonarqube / SonarQube Code Analysis

apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts#L192

Unexpected `await` of a non-Promise (non-"Thenable") value.
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 })),

Check failure on line 197 in apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts

View check run for this annotation

cloud-pi-native-sonarqube / SonarQube Code Analysis

apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts#L197

Unexpected iterable of non-Promise (non-"Thenable") values passed to promise aggregator.
db.mavenGroup.deleteMany((q: any) => q.where({ name: params.name })),

Check failure on line 198 in apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts

View check run for this annotation

cloud-pi-native-sonarqube / SonarQube Code Analysis

apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts#L198

Unexpected iterable of non-Promise (non-"Thenable") values passed to promise aggregator.
db.npmHosted.deleteMany((q: any) => q.where({ name: params.name })),

Check failure on line 199 in apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts

View check run for this annotation

cloud-pi-native-sonarqube / SonarQube Code Analysis

apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts#L199

Unexpected iterable of non-Promise (non-"Thenable") values passed to promise aggregator.
db.npmGroup.deleteMany((q: any) => q.where({ name: params.name })),

Check failure on line 200 in apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts

View check run for this annotation

cloud-pi-native-sonarqube / SonarQube Code Analysis

apps/server-nestjs/src/modules/nexus/nexus-testing.utils.ts#L200

Unexpected iterable of non-Promise (non-"Thenable") values passed to promise aggregator.
])
return new HttpResponse(null, { status: 204 })
}),
]
}