import { NextRequest } from 'next/server'; jest.mock('@/lib/project-auth', () => ({ getPublicProjectDb: jest.fn(), })); jest.mock('@/lib/module-pull-token', () => ({ MODULE_PULL_TOKEN_USERNAME: 'flnxi-module', hashModulePullToken: jest.fn(() => 'token-hash'), verifyModulePullToken: jest.fn(), })); jest.mock('@/lib/module-grants', () => ({ getAuthorizedModuleGrant: jest.fn(), })); import { getPublicProjectDb } from '@/lib/project-auth'; import { verifyModulePullToken } from '@/lib/module-pull-token'; import { getAuthorizedModuleGrant } from '@/lib/module-grants'; const mockGetPublicProjectDb = getPublicProjectDb as jest.MockedFunction; const mockVerifyModulePullToken = verifyModulePullToken as jest.MockedFunction; const mockGetAuthorizedModuleGrant = getAuthorizedModuleGrant as jest.MockedFunction; let projectPrisma: any; function createBasicTokenHeader(token: string): string { return `Basic ${Buffer.from(`flnxi-module:${token}`, 'utf8').toString('base64')}`; } describe('registry auth route', () => { beforeEach(() => { jest.clearAllMocks(); mockVerifyModulePullToken.mockReturnValue({ tokenType: 'flnxi-module-pull', projectCode: 'flnxi', licenseId: 'license-1', activationId: 'activation-1', instanceId: 'instance-1', moduleId: 'intranet-penetration-core', registryHost: 'registry.test', repository: 'flnxi/premium/intranet-penetration-core', tag: '1.0.0', digest: 'sha256:abc', iat: 1780272000, exp: 1780272900, jti: 'jti-1', }); mockGetAuthorizedModuleGrant.mockResolvedValue({ moduleId: 'intranet-penetration-core', artifact: { type: 'registry-image', image: 'registry.test/flnxi/premium/intranet-penetration-core:1.0.0', version: '1.0.0', digest: 'sha256:abc', }, }); projectPrisma = { license: { findUnique: jest.fn().mockResolvedValue({ id: 'license-1', licenseType: 'PRO', status: 'active', expiresAt: null, }), }, activation: { findFirst: jest.fn().mockResolvedValue({ id: 'activation-1', licenseId: 'license-1', instanceId: 'instance-1', isActive: true, license: { id: 'license-1', licenseType: 'PRO', status: 'active', expiresAt: null, }, }), }, licenseModuleGrant: { findUnique: jest.fn(), findMany: jest.fn(), create: jest.fn(), }, modulePullTokenAudit: { findFirst: jest.fn().mockResolvedValue({ id: 'audit-1', licenseId: 'license-1', activationId: 'activation-1', moduleId: 'intranet-penetration-core', repository: 'flnxi/premium/intranet-penetration-core', digest: 'sha256:abc', expiresAt: new Date('2026-06-02T00:15:00.000Z'), lastValidatedAt: null, }), updateMany: jest.fn(), }, }; mockGetPublicProjectDb.mockResolvedValue({ success: true, prisma: projectPrisma, } as any); }); it('应允许 token 声明仓库和 digest 完全匹配的拉取请求', async () => { const { GET } = await import('../../app/api/registry/auth/route'); const request = new NextRequest( 'http://license.test/api/registry/auth?scope=repository:flnxi/premium/intranet-penetration-core:pull&digest=sha256:abc&referenceType=manifests&reference=sha256:abc', { headers: { Authorization: createBasicTokenHeader('pull-token'), 'X-Registry-Host': 'registry.test', }, } ); const response = await GET(request); const data = await response.json(); expect(response.status).toBe(200); expect(data).toEqual( expect.objectContaining({ success: true, moduleId: 'intranet-penetration-core', repository: 'flnxi/premium/intranet-penetration-core', }) ); expect(mockGetPublicProjectDb).toHaveBeenCalledWith('flnxi'); }); it('manifest tag 和 token 不匹配时应拒绝', async () => { const { GET } = await import('../../app/api/registry/auth/route'); const request = new NextRequest( 'http://license.test/api/registry/auth?scope=repository:flnxi/premium/intranet-penetration-core:pull&referenceType=manifests&reference=2.0.0', { headers: { Authorization: createBasicTokenHeader('pull-token'), 'X-Registry-Host': 'registry.test', }, } ); const response = await GET(request); expect(response.status).toBe(403); expect(mockGetPublicProjectDb).not.toHaveBeenCalled(); }); it('manifest tag 和 token 匹配时应允许继续校验项目状态', async () => { const { GET } = await import('../../app/api/registry/auth/route'); const request = new NextRequest( 'http://license.test/api/registry/auth?scope=repository:flnxi/premium/intranet-penetration-core:pull&referenceType=manifests&reference=1.0.0', { headers: { Authorization: createBasicTokenHeader('pull-token'), 'X-Registry-Host': 'registry.test', }, } ); const response = await GET(request); expect(response.status).toBe(200); expect(mockGetPublicProjectDb).toHaveBeenCalledWith('flnxi'); }); it('已登记 token 的 blob 层文件请求不应拿镜像 digest 误判', async () => { const { GET } = await import('../../app/api/registry/auth/route'); const request = new NextRequest( 'http://license.test/api/registry/auth?scope=repository:flnxi/premium/intranet-penetration-core:pull&digest=sha256:layer&referenceType=blobs', { headers: { Authorization: createBasicTokenHeader('pull-token'), 'X-Registry-Host': 'registry.test', }, } ); const response = await GET(request); expect(response.status).toBe(200); }); it('同一 token 已验证主清单后,应允许 Docker 继续请求平台子清单 digest', async () => { projectPrisma.modulePullTokenAudit.findFirst.mockResolvedValue({ id: 'audit-1', licenseId: 'license-1', activationId: 'activation-1', moduleId: 'intranet-penetration-core', repository: 'flnxi/premium/intranet-penetration-core', digest: 'sha256:abc', expiresAt: new Date('2026-06-02T00:15:00.000Z'), lastValidatedAt: new Date(), }); const { GET } = await import('../../app/api/registry/auth/route'); const request = new NextRequest( 'http://license.test/api/registry/auth?scope=repository:flnxi/premium/intranet-penetration-core:pull&digest=sha256:child-manifest&referenceType=manifests&reference=sha256:child-manifest', { headers: { Authorization: createBasicTokenHeader('pull-token'), 'X-Registry-Host': 'registry.test', }, } ); const response = await GET(request); expect(response.status).toBe(200); }); it('未先验证主清单时,陌生 manifest digest 仍应拒绝', async () => { const { GET } = await import('../../app/api/registry/auth/route'); const request = new NextRequest( 'http://license.test/api/registry/auth?scope=repository:flnxi/premium/intranet-penetration-core:pull&digest=sha256:other&referenceType=manifests&reference=sha256:other', { headers: { Authorization: createBasicTokenHeader('pull-token'), 'X-Registry-Host': 'registry.test', }, } ); const response = await GET(request); expect(response.status).toBe(403); }); it('token 未登记或已过期时应拒绝拉取', async () => { projectPrisma.modulePullTokenAudit.findFirst.mockResolvedValue(null); const { GET } = await import('../../app/api/registry/auth/route'); const request = new NextRequest( 'http://license.test/api/registry/auth?scope=repository:flnxi/premium/intranet-penetration-core:pull', { headers: { Authorization: createBasicTokenHeader('pull-token'), 'X-Registry-Host': 'registry.test', }, } ); const response = await GET(request); expect(response.status).toBe(403); }); it('激活记录失效时应拒绝拉取', async () => { projectPrisma.activation.findFirst.mockResolvedValue(null); const { GET } = await import('../../app/api/registry/auth/route'); const request = new NextRequest( 'http://license.test/api/registry/auth?scope=repository:flnxi/premium/intranet-penetration-core:pull', { headers: { Authorization: createBasicTokenHeader('pull-token'), 'X-Registry-Host': 'registry.test', }, } ); const response = await GET(request); expect(response.status).toBe(403); }); it('请求仓库和 token 不匹配时应拒绝,不进入项目数据库', async () => { const { GET } = await import('../../app/api/registry/auth/route'); const request = new NextRequest( 'http://license.test/api/registry/auth?scope=repository:flnxi/premium/custom-templates-core:pull', { headers: { Authorization: createBasicTokenHeader('pull-token'), }, } ); const response = await GET(request); expect(response.status).toBe(403); expect(mockGetPublicProjectDb).not.toHaveBeenCalled(); }); });