284 lines
9.4 KiB
TypeScript
284 lines
9.4 KiB
TypeScript
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<typeof getPublicProjectDb>;
|
|
const mockVerifyModulePullToken = verifyModulePullToken as jest.MockedFunction<typeof verifyModulePullToken>;
|
|
const mockGetAuthorizedModuleGrant = getAuthorizedModuleGrant as jest.MockedFunction<typeof getAuthorizedModuleGrant>;
|
|
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();
|
|
});
|
|
});
|