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
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,23 @@ import { harborConfigFactory } from '../../config/harbor.config'
import { VaultClientService } from '../vault/vault-client.service'
import { RegistryClientService } from './registry-client.service'
import { RegistryHttpClientService } from './registry-http-client.service'
import { HARBOR_INTERNAL_URL, makeRegistryDb, makeRegistryHandlers, makeRobotPermissions } from './registry-testing.utils'

const harborUrl = 'https://harbor.example'
const harborUrl = HARBOR_INTERNAL_URL
const harborAdminPassword = faker.internet.password()
const basicAuth = `Basic ${Buffer.from(`admin:${harborAdminPassword}`, 'utf8').toString('base64')}`

const server = setupServer()

describe('registryService', () => {
let service: RegistryClientService
let db: ReturnType<typeof makeRegistryDb>

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

beforeEach(async () => {
db = makeRegistryDb()
server.use(...makeRegistryHandlers(db))

const harborConfig = mockDeep<ConfigType<typeof harborConfigFactory>>({
url: harborUrl,
internalUrl: harborUrl,
Expand All @@ -33,7 +37,6 @@ describe('registryService', () => {
ruleCount: 10,
retentionCron: '0 22 2 * * *',
})

const module = await Test.createTestingModule({
providers: [
RegistryClientService,
Expand All @@ -50,7 +53,6 @@ describe('registryService', () => {
}).compile()
service = module.get(RegistryClientService)
})

afterEach(() => server.resetHandlers())
afterAll(() => server.close())

Expand All @@ -59,37 +61,23 @@ describe('registryService', () => {
})

it('should reconcile a project creation conflict (400 CONFLICT) by reloading the existing project', async () => {
server.use(
http.post(`${harborUrl}/api/v2.0/projects`, () =>
HttpResponse.json({
errors: [{ code: 'CONFLICT', message: 'project myproj already exists' }],
}, { status: HttpStatus.BAD_REQUEST })),
http.get(`${harborUrl}/api/v2.0/projects/:projectName`, async ({ request, params }) => {
expect(request.headers.get('x-is-resource-name')).toBe('true')
expect(params.projectName).toBe('myproj')
return HttpResponse.json({ project_id: 123, metadata: {} })
}),
)
await db.projects.create({ name: 'myproj', project_id: 123, metadata: {} })

const result = await service.ensureProject('myproj', -1)

expect(result).toEqual({ project_id: 123, metadata: {} })
expect(result).toMatchObject({ project_id: 123, metadata: {} })

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🟡 Nit] toMatchObject en remplacement de toEqual : si le handler renvoie des champs supplémentaires, l'assertion passe silencieusement. Acceptable le temps de la migration, à surveiller.

})

it('should reconcile a real HTTP 409 on project create by reloading the existing project', async () => {
server.use(
http.post(`${harborUrl}/api/v2.0/projects`, () =>
new HttpResponse(null, { status: HttpStatus.CONFLICT })),
http.get(`${harborUrl}/api/v2.0/projects/:projectName`, async ({ request, params }) => {
expect(request.headers.get('x-is-resource-name')).toBe('true')
expect(params.projectName).toBe('myproj')
return HttpResponse.json({ project_id: 123, metadata: {} })
}),
)
await db.projects.create({ name: 'myproj', project_id: 123, metadata: {} })

const result = await service.ensureProject('myproj', -1)

expect(result).toEqual({ project_id: 123, metadata: {} })
expect(result).toMatchObject({ project_id: 123, metadata: {} })
})

it('should send basic auth and JSON body on ensureProject', async () => {
Expand All @@ -107,26 +95,23 @@ describe('registryService', () => {
})
return HttpResponse.json({}, { status: HttpStatus.CREATED })
}),
http.get(`${harborUrl}/api/v2.0/projects/:projectName`, async ({ request, params }) => {
expect(request.headers.get('authorization')).toBe(basicAuth)
expect(params.projectName).toBe('myproj')
return HttpResponse.json({ project_id: 123, metadata: {} })
}),
)
await db.projects.create({ name: 'myproj', project_id: 123, metadata: {} })

const result = await service.ensureProject('myproj', -1)
expect(result).toEqual({ project_id: 123, metadata: {} })

expect(result).toMatchObject({ project_id: 123, metadata: {} })
})

it('should not rotate an existing robot on creation conflict (400 CONFLICT)', async () => {
await db.robots.create({
id: 33,
name: 'ro-robot',
description: 'robot for ci builds',
level: 'project',
permissions: makeRobotPermissions(),
})
server.use(
http.post(`${harborUrl}/api/v2.0/robots`, () => HttpResponse.json({
errors: [{ code: 'CONFLICT', message: 'robot robot$myproj+ro-robot already exists' }],
}, { status: HttpStatus.BAD_REQUEST })),
http.get(`${harborUrl}/api/v2.0/projects/myproj`, ({ request }) => {
expect(request.headers.get('x-is-resource-name')).toBe('true')
return HttpResponse.json({ project_id: 123, metadata: {} })
}),
http.get(`${harborUrl}/api/v2.0/robots`, () => {
throw new Error('robot listing must not be called on conflict')
}),
Expand All @@ -141,35 +126,22 @@ describe('registryService', () => {
description: 'robot for ci builds',
disable: false,
level: 'project',
permissions: [{ namespace: 'myproj', kind: 'project', access: [{ resource: 'repository', action: 'pull' }] }],
permissions: makeRobotPermissions(),
})

expect(result).toBeUndefined()
})

it('should send X-Is-Resource-Name on getProjectByName', async () => {
server.use(
http.get(`${harborUrl}/api/v2.0/projects/:projectName`, async ({ request, params }) => {
expect(request.method).toBe('GET')
expect(request.headers.get('authorization')).toBe(basicAuth)
expect(request.headers.get('x-is-resource-name')).toBe('true')
expect(params.projectName).toBe('myproj')
return HttpResponse.json({ project_id: 123, metadata: {} })
}),
)
await db.projects.create({ name: 'myproj', project_id: 123, metadata: {} })

const res = await service.getProjectByName('myproj')

expect(res).toMatchObject({ status: HttpStatus.OK, data: { project_id: 123 } })
})

it('should list repositories with page_size', async () => {
server.use(
http.get(`${harborUrl}/api/v2.0/projects/:projectName/repositories`, async ({ request }) => {
expect(request.url).toContain('page_size=100')
return HttpResponse.json([{ name: 'myproj/repo-a' }])
}),
)
await db.repositories.create({ name: 'myproj/repo-a' })

const res: HarborRepository[] = []
for await (const item of service.getRepositories('myproj')) {
Expand All @@ -180,15 +152,6 @@ describe('registryService', () => {
})

it('should delete a repository by name', async () => {
server.use(
http.delete(`${harborUrl}/api/v2.0/projects/:projectName/repositories/:repositoryName`, async ({ request, params }) => {
expect(request.method).toBe('DELETE')
expect(params.projectName).toBe('myproj')
expect(params.repositoryName).toBe('repo-a')
return new HttpResponse(null, { status: HttpStatus.NO_CONTENT })
}),
)

const res = await service.deleteRepository('myproj', 'repo-a')

expect(res).toMatchObject({ status: HttpStatus.NO_CONTENT })
Expand All @@ -201,16 +164,8 @@ describe('registryService', () => {
rules: [],
trigger: { kind: 'Schedule', settings: { cron: '0 22 2 * * *' }, references: [] },
}
await db.projects.create({ name: 'myproj', project_id: 123, metadata: { retention_id: '325' } })
server.use(
http.get(`${harborUrl}/api/v2.0/projects/myproj`, ({ request }) => {
expect(request.headers.get('x-is-resource-name')).toBe('true')
return HttpResponse.json({ project_id: 123, metadata: { retention_id: '325' } })
}),
http.put(`${harborUrl}/api/v2.0/retentions/325`, async ({ request }) => {
expect(request.method).toBe('PUT')
expect(await request.json()).toEqual(policy)
return new HttpResponse(null, { status: HttpStatus.OK })
}),
http.post(`${harborUrl}/api/v2.0/retentions`, () => {
throw new Error('a second retention create must not be issued on re-sync')
}),
Expand All @@ -228,25 +183,8 @@ describe('registryService', () => {
rules: [],
trigger: { kind: 'Schedule', settings: { cron: '0 22 2 * * *' }, references: [] },
}
let reads = 0
server.use(
http.get(`${harborUrl}/api/v2.0/projects/myproj`, ({ request }) => {
expect(request.headers.get('x-is-resource-name')).toBe('true')
reads += 1
return HttpResponse.json({ project_id: 123, metadata: {} })
}),
http.post(`${harborUrl}/api/v2.0/retentions`, async ({ request }) => {
expect(await request.json()).toEqual(policy)
return HttpResponse.json({ id: 500 }, { status: HttpStatus.CREATED })
}),
// After a successful create there is no policy to reconcile in place.
http.put(`${harborUrl}/api/v2.0/retentions/:id`, () => {
throw new Error('a fresh create must not also issue a reconcile PUT')
}),
)
await db.projects.create({ name: 'myproj', project_id: 123, metadata: {} })

await service.ensureRetention('myproj', policy)

expect(reads).toBe(1)
})
})
Loading
Loading