106 lines
3.4 KiB
TypeScript
106 lines
3.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import type { NextRequest } from 'next/server';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
generateImage: vi.fn(),
|
|
managed: vi.fn((_section: string, _providerId: string) => false),
|
|
resolveApiKey: vi.fn(),
|
|
resolveBaseUrl: vi.fn(),
|
|
recordUsage: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@/lib/media/image-providers', () => ({
|
|
IMAGE_PROVIDERS: { seedream: { requiresApiKey: true } },
|
|
aspectRatioToDimensions: () => ({ width: 1024, height: 576 }),
|
|
generateImage: (...args: unknown[]) => mocks.generateImage(...args),
|
|
}));
|
|
|
|
vi.mock('@/lib/server/provider-config', () => ({
|
|
isServerConfiguredProvider: (section: string, providerId: string) =>
|
|
mocks.managed(section, providerId),
|
|
resolveImageApiKey: (...args: unknown[]) => mocks.resolveApiKey(...args),
|
|
resolveImageBaseUrl: (...args: unknown[]) => mocks.resolveBaseUrl(...args),
|
|
}));
|
|
|
|
vi.mock('@/lib/server/usage-storage', () => ({
|
|
recordGenerationUsage: (...args: unknown[]) => mocks.recordUsage(...args),
|
|
}));
|
|
|
|
vi.mock('@/lib/server/ssrf-guard', () => ({
|
|
validateUrlForSSRF: vi.fn(async () => null),
|
|
}));
|
|
|
|
vi.mock('@/lib/logger', () => ({
|
|
createLogger: () => ({
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
async function postImage(headers: Record<string, string> = {}) {
|
|
const { POST } = await import('@/app/api/generate/image/route');
|
|
const request = new Request('http://localhost/api/generate/image', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
body: JSON.stringify({ prompt: 'A classroom diagram' }),
|
|
});
|
|
return POST(request as unknown as NextRequest);
|
|
}
|
|
|
|
describe('POST /api/generate/image provider access response', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
vi.unstubAllEnvs();
|
|
mocks.generateImage.mockReset();
|
|
mocks.managed.mockReset().mockReturnValue(false);
|
|
mocks.resolveApiKey.mockReset();
|
|
mocks.resolveBaseUrl.mockReset();
|
|
mocks.recordUsage.mockReset();
|
|
});
|
|
|
|
it('returns 403/FORBIDDEN and never calls the provider for a central denial', async () => {
|
|
const { ServerManagedProviderRequiredError } =
|
|
await import('@/lib/server/provider-access-policy');
|
|
mocks.resolveApiKey.mockImplementation(() => {
|
|
throw new ServerManagedProviderRequiredError();
|
|
});
|
|
|
|
const response = await postImage({
|
|
'x-image-provider': 'seedream',
|
|
'x-api-key': 'caller-key',
|
|
'x-base-url': 'https://caller.invalid/v1',
|
|
});
|
|
|
|
expect(response.status).toBe(403);
|
|
await expect(response.json()).resolves.toMatchObject({
|
|
success: false,
|
|
errorCode: 'FORBIDDEN',
|
|
});
|
|
expect(mocks.generateImage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('keeps the existing successful resolution path unchanged', async () => {
|
|
mocks.resolveApiKey.mockReturnValue('client-key');
|
|
mocks.resolveBaseUrl.mockReturnValue('https://client.example/v1');
|
|
mocks.generateImage.mockResolvedValue({ url: 'https://cdn.example/image.png' });
|
|
|
|
const response = await postImage({
|
|
'x-image-provider': 'seedream',
|
|
'x-api-key': 'client-key',
|
|
'x-base-url': 'https://client.example/v1',
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mocks.generateImage).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
providerId: 'seedream',
|
|
apiKey: 'client-key',
|
|
baseUrl: 'https://client.example/v1',
|
|
}),
|
|
expect.objectContaining({ prompt: 'A classroom diagram' }),
|
|
);
|
|
});
|
|
});
|