From f0033bde92b548a034a621babafdcb9aa3ab0d97 Mon Sep 17 00:00:00 2001 From: shikanime Date: Wed, 26 Aug 2026 01:19:17 +0200 Subject: [PATCH 1/3] test(server-nestjs): adversarial coverage registry client pagination/error edges Co-authored-by: Automata --- .../registry/registry-client.service.spec.ts | 110 +++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/apps/server-nestjs/src/modules/registry/registry-client.service.spec.ts b/apps/server-nestjs/src/modules/registry/registry-client.service.spec.ts index 980c5335d5..300d0a2697 100644 --- a/apps/server-nestjs/src/modules/registry/registry-client.service.spec.ts +++ b/apps/server-nestjs/src/modules/registry/registry-client.service.spec.ts @@ -8,7 +8,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from import { mockDeep } from 'vitest-mock-extended' import { harborConfigFactory } from '../../config/harbor.config' import { VaultClientService } from '../vault/vault-client.service' -import { RegistryClientService } from './registry-client.service' +import { RegistryClientService, type HarborRobot } from './registry-client.service' import { RegistryHttpClientService } from './registry-http-client.service' const harborUrl = 'https://harbor.example' @@ -92,4 +92,112 @@ describe('registryService', () => { expect(res).toMatchObject({ status: HttpStatus.OK, data: { project_id: 123 } }) }) + + describe('error and edge contracts (adversarial)', () => { + it('returns non-2xx statuses as data without throwing (caller-decides contract)', async () => { + server.use( + http.post(`${harborUrl}/api/v2.0/projects`, () => + HttpResponse.json({ message: 'conflict: project name already exists' }, { status: HttpStatus.CONFLICT })), + ) + + const res = await service.createProject('myproj', -1) + expect(res.status).toBe(HttpStatus.CONFLICT) + expect(res.data).toMatchObject({ message: expect.stringContaining('already exists') }) + }) + + it('propagates a 500 body through the caller-decides contract', async () => { + server.use( + http.get(`${harborUrl}/api/v2.0/projects/:projectName`, () => + HttpResponse.json({ errors: [{ code: 'INTERNAL' }] }, { status: HttpStatus.INTERNAL_SERVER_ERROR })), + ) + + const res = await service.getProjectByName('myproj') + expect(res.status).toBe(HttpStatus.INTERNAL_SERVER_ERROR) + }) + + it('treats 204 No Content as null data', async () => { + server.use( + http.delete(`${harborUrl}/api/v2.0/projects/:projectName`, () => new Response(null, { status: HttpStatus.NO_CONTENT })), + ) + + const res = await service.deleteProjectByName('myproj') + expect(res).toEqual({ status: HttpStatus.NO_CONTENT, data: null }) + }) + + it('falls back to text parsing when content-type is not JSON', async () => { + server.use( + http.get(`${harborUrl}/api/v2.0/quotas`, () => + new Response('plain text error', { status: HttpStatus.OK, headers: { 'content-type': 'text/plain' } })), + ) + + const res = await service.listQuotas(123) + expect(res.data).toBe('plain text error') + }) + }) + + describe('paginate', () => { + it('yields robots across pages until a short page ends iteration', async () => { + const seenPages: string[] = [] + server.use( + http.get(`${harborUrl}/api/v2.0/robots`, ({ request }) => { + const page = Number(new URL(request.url).searchParams.get('page')) + seenPages.push(String(page)) + // page size is ROBOT_LIST_PAGE_SIZE=100: full page → continue, short page → stop + const rows = page === 1 + ? Array.from({ length: 100 }, (_, i) => ({ id: i + 1, name: `robot${i}` })) + : [{ id: 99, name: 'robot-last' }] + return HttpResponse.json(rows, { status: HttpStatus.OK }) + }), + ) + + const robots: HarborRobot[] = [] + for await (const robot of service.getProjectRobots(42)) robots.push(robot) + + expect(seenPages).toEqual(['1', '2']) + expect(robots).toHaveLength(101) + expect(robots.at(-1)).toMatchObject({ id: 99 }) + }) + + it('stops immediately on an empty first page', async () => { + let callCount = 0 + server.use( + http.get(`${harborUrl}/api/v2.0/robots`, () => { + callCount++ + return HttpResponse.json([], { status: HttpStatus.OK }) + }), + ) + + const robots: HarborRobot[] = [] + for await (const robot of service.getProjectRobots(42)) robots.push(robot) + + expect(callCount).toBe(1) + expect(robots).toEqual([]) + }) + }) + + describe('getRetentionId', () => { + it('returns null when project lookup fails', async () => { + server.use( + http.get(`${harborUrl}/api/v2.0/projects/:projectName`, () => + HttpResponse.json({}, { status: HttpStatus.NOT_FOUND })), + ) + await expect(service.getRetentionId('ghost')).resolves.toBeNull() + }) + + it('returns null when retention_id is absent or NaN', async () => { + server.use( + http.get(`${harborUrl}/api/v2.0/projects/:projectName`, () => + HttpResponse.json({ project_id: 1, metadata: {} }, { status: HttpStatus.OK })), + ) + await expect(service.getRetentionId('myproj')).resolves.toBeNull() + }) + + it('returns the numeric retention id from metadata', async () => { + server.use( + http.get(`${harborUrl}/api/v2.0/projects/:projectName`, () => + HttpResponse.json({ project_id: 1, metadata: { retention_id: '77' } }, { status: HttpStatus.OK })), + ) + await expect(service.getRetentionId('myproj')).resolves.toBe(77) + }) + }) }) From fee7f3821d62dcd61256cb2d944c37f95b5f3a79 Mon Sep 17 00:00:00 2001 From: shikanime Date: Wed, 26 Aug 2026 11:37:47 +0200 Subject: [PATCH 2/3] test(server-nestjs): neutral wording in registry client spec Co-authored-by: Automata --- .../src/modules/registry/registry-client.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server-nestjs/src/modules/registry/registry-client.service.spec.ts b/apps/server-nestjs/src/modules/registry/registry-client.service.spec.ts index 300d0a2697..8a24f2d7cd 100644 --- a/apps/server-nestjs/src/modules/registry/registry-client.service.spec.ts +++ b/apps/server-nestjs/src/modules/registry/registry-client.service.spec.ts @@ -93,7 +93,7 @@ describe('registryService', () => { expect(res).toMatchObject({ status: HttpStatus.OK, data: { project_id: 123 } }) }) - describe('error and edge contracts (adversarial)', () => { + describe('error and edge contracts', () => { it('returns non-2xx statuses as data without throwing (caller-decides contract)', async () => { server.use( http.post(`${harborUrl}/api/v2.0/projects`, () => From c26b90b43c92e0e84681d7eeef7167188cb06a14 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 28 Aug 2026 17:03:20 +0200 Subject: [PATCH 3/3] fix(server-nestjs): split type import in registry client spec Signed-off-by: William Phetsinorath Change-Id: I256fbe0de561639b7acd46621bb2a84f6a6a6964 --- .../src/modules/registry/registry-client.service.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server-nestjs/src/modules/registry/registry-client.service.spec.ts b/apps/server-nestjs/src/modules/registry/registry-client.service.spec.ts index 8a24f2d7cd..c3958e5f40 100644 --- a/apps/server-nestjs/src/modules/registry/registry-client.service.spec.ts +++ b/apps/server-nestjs/src/modules/registry/registry-client.service.spec.ts @@ -1,4 +1,5 @@ import type { ConfigType } from '@nestjs/config' +import type { HarborRobot } from './registry-client.service' import { faker } from '@faker-js/faker' import { HttpStatus } from '@nestjs/common' import { Test } from '@nestjs/testing' @@ -8,7 +9,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from import { mockDeep } from 'vitest-mock-extended' import { harborConfigFactory } from '../../config/harbor.config' import { VaultClientService } from '../vault/vault-client.service' -import { RegistryClientService, type HarborRobot } from './registry-client.service' +import { RegistryClientService } from './registry-client.service' import { RegistryHttpClientService } from './registry-http-client.service' const harborUrl = 'https://harbor.example'