Files
openmaic/OpenMAIC/tests/media/auth-probe-adapters.test.ts
2026-08-16 14:58:47 +08:00

582 lines
18 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { testComfyuiImageConnectivity } from '@/lib/media/adapters/comfyui-image-adapter';
import { testGrokImageConnectivity } from '@/lib/media/adapters/grok-image-adapter';
import { testGrokVideoConnectivity } from '@/lib/media/adapters/grok-video-adapter';
import { testHappyHorseConnectivity } from '@/lib/media/adapters/happyhorse-adapter';
import { testKlingConnectivity } from '@/lib/media/adapters/kling-adapter';
import { testLemonadeImageConnectivity } from '@/lib/media/adapters/lemonade-image-adapter';
import { testMiniMaxImageConnectivity } from '@/lib/media/adapters/minimax-image-adapter';
import { testMiniMaxVideoConnectivity } from '@/lib/media/adapters/minimax-video-adapter';
import { testNanoBananaConnectivity } from '@/lib/media/adapters/nano-banana-adapter';
import { testOpenAIImageConnectivity } from '@/lib/media/adapters/openai-image-adapter';
import { testQwenImageConnectivity } from '@/lib/media/adapters/qwen-image-adapter';
import { testSeedanceConnectivity } from '@/lib/media/adapters/seedance-adapter';
import { testSeedreamConnectivity } from '@/lib/media/adapters/seedream-adapter';
import { testVeoConnectivity } from '@/lib/media/adapters/veo-adapter';
const fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);
type ConnectivityResult = { success: boolean; message: string };
interface AuthOnlyCase {
name: string;
providerName: string;
probe: () => Promise<ConnectivityResult>;
assertRequest: () => void;
}
interface StrictProbeCase {
name: string;
probe: () => Promise<ConnectivityResult>;
redirectResponse: () => Response;
expectedResult: ConnectivityResult;
assertRequest: () => void;
}
const PRIVATE_REDIRECT_LOCATION = 'http://127.0.0.1/internal';
const authOnlyCases: AuthOnlyCase[] = [
{
name: 'Seedream',
providerName: 'Seedream',
probe: () =>
testSeedreamConnectivity({
providerId: 'seedream',
apiKey: 'seedream-key',
baseUrl: 'https://seedream.example.com/api/plan/v3/',
model: 'seedream-test',
}),
assertRequest: () => {
expect(fetchMock).toHaveBeenCalledWith(
'https://seedream.example.com/api/plan/v3/images/generations',
{
method: 'POST',
redirect: 'manual',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer seedream-key',
},
body: JSON.stringify({ model: 'seedream-test', prompt: '', size: '1x1' }),
},
);
},
},
{
name: 'Qwen Image',
providerName: 'Qwen Image',
probe: () =>
testQwenImageConnectivity({
providerId: 'qwen-image',
apiKey: 'qwen-key',
baseUrl: 'https://qwen.example.com',
model: 'qwen-test',
}),
assertRequest: () => {
expect(fetchMock).toHaveBeenCalledWith(
'https://qwen.example.com/api/v1/services/aigc/multimodal-generation/generation',
{
method: 'POST',
redirect: 'manual',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer qwen-key',
},
body: JSON.stringify({
model: 'qwen-test',
input: { messages: [{ role: 'user', content: [{ text: '' }] }] },
parameters: { size: '1*1' },
}),
},
);
},
},
{
name: 'Grok Image',
providerName: 'Grok Image',
probe: () =>
testGrokImageConnectivity({
providerId: 'grok-image',
apiKey: 'grok-image-key',
baseUrl: 'https://grok-image.example.com/v1',
model: 'grok-image-test',
}),
assertRequest: () => {
expect(fetchMock).toHaveBeenCalledWith(
'https://grok-image.example.com/v1/images/generations',
{
method: 'POST',
redirect: 'manual',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer grok-image-key',
},
body: JSON.stringify({ model: 'grok-image-test', prompt: '', n: 1 }),
},
);
},
},
{
name: 'Grok Video',
providerName: 'Grok Video',
probe: () =>
testGrokVideoConnectivity({
providerId: 'grok-video',
apiKey: 'grok-video-key',
baseUrl: 'https://grok-video.example.com/v1',
model: 'grok-video-test',
}),
assertRequest: () => {
expect(fetchMock).toHaveBeenCalledWith(
'https://grok-video.example.com/v1/videos/generations',
{
method: 'POST',
redirect: 'manual',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer grok-video-key',
},
body: JSON.stringify({ model: 'grok-video-test', prompt: '' }),
},
);
},
},
{
name: 'Seedance',
providerName: 'Seedance',
probe: () =>
testSeedanceConnectivity({
providerId: 'seedance',
apiKey: 'seedance-key',
baseUrl: 'https://seedance.example.com/api/plan/v3/',
}),
assertRequest: () => {
expect(fetchMock).toHaveBeenCalledWith(
'https://seedance.example.com/api/plan/v3/contents/generations/tasks/connectivity-test-nonexistent',
{
method: 'GET',
redirect: 'manual',
headers: { Authorization: 'Bearer seedance-key' },
},
);
},
},
{
name: 'Kling',
providerName: 'Kling',
probe: () =>
testKlingConnectivity({
providerId: 'kling',
apiKey: 'access-key:secret-key',
baseUrl: 'https://kling.example.com',
}),
assertRequest: () => {
expect(fetchMock).toHaveBeenCalledWith(
'https://kling.example.com/v1/videos/text2video/connectivity-test',
{
method: 'GET',
redirect: 'manual',
headers: {
Authorization: expect.stringMatching(/^Bearer [^.]+\.[^.]+\.[^.]+$/),
},
},
);
},
},
{
name: 'HappyHorse',
providerName: 'HappyHorse',
probe: () =>
testHappyHorseConnectivity({
providerId: 'happyhorse',
apiKey: 'happyhorse-key',
baseUrl: 'https://happyhorse.example.com/',
}),
assertRequest: () => {
expect(fetchMock).toHaveBeenCalledWith(
'https://happyhorse.example.com/api/v1/tasks/connectivity-test-nonexistent',
{
method: 'GET',
redirect: 'manual',
headers: { Authorization: 'Bearer happyhorse-key' },
},
);
},
},
];
const strictProbeCases: StrictProbeCase[] = [
{
name: 'OpenAI Image',
probe: () =>
testOpenAIImageConnectivity({
providerId: 'openai-image',
apiKey: 'openai-key',
baseUrl: 'https://openai.example.com/v1/',
model: 'gpt-image-test',
}),
redirectResponse: () =>
new Response('redirect blocked', {
status: 302,
headers: { Location: PRIVATE_REDIRECT_LOCATION },
}),
expectedResult: {
success: false,
message: 'OpenAI Image API error (302): redirect blocked',
},
assertRequest: () => {
expect(fetchMock).toHaveBeenCalledWith(
'https://openai.example.com/v1/models/gpt-image-test',
{
redirect: 'manual',
headers: { Authorization: 'Bearer openai-key' },
},
);
},
},
{
name: 'Lemonade',
probe: () =>
testLemonadeImageConnectivity({
providerId: 'lemonade',
apiKey: 'lemonade-key',
baseUrl: 'https://lemonade.example.com/v1/',
model: 'lemonade-test',
}),
redirectResponse: () =>
new Response('redirect blocked', {
status: 302,
headers: { Location: PRIVATE_REDIRECT_LOCATION },
}),
expectedResult: {
success: false,
message: 'Lemonade API error (302): redirect blocked',
},
assertRequest: () => {
expect(fetchMock).toHaveBeenCalledWith('https://lemonade.example.com/v1/models', {
redirect: 'manual',
headers: { Authorization: 'Bearer lemonade-key' },
});
},
},
{
name: 'MiniMax Image',
probe: () =>
testMiniMaxImageConnectivity({
providerId: 'minimax-image',
apiKey: 'minimax-image-key',
baseUrl: 'https://minimax-image.example.com/',
model: 'custom-image-model',
}),
redirectResponse: () =>
new Response(JSON.stringify({ base_resp: { status_msg: 'redirect blocked' } }), {
status: 302,
headers: { Location: PRIVATE_REDIRECT_LOCATION },
}),
expectedResult: { success: false, message: 'API error: redirect blocked' },
assertRequest: () => {
expect(fetchMock).toHaveBeenCalledWith(
'https://minimax-image.example.com/v1/image_generation',
{
method: 'POST',
redirect: 'manual',
headers: {
Authorization: 'Bearer minimax-image-key',
'Content-Type': 'application/json; charset=utf-8',
},
body: JSON.stringify({
model: 'image-01',
prompt: 'test',
aspect_ratio: '1:1',
n: 1,
}),
},
);
},
},
{
name: 'MiniMax Video',
probe: () =>
testMiniMaxVideoConnectivity({
providerId: 'minimax-video',
apiKey: 'minimax-video-key',
baseUrl: 'https://minimax-video.example.com/',
model: 'custom-video-model',
}),
redirectResponse: () =>
new Response(JSON.stringify({ base_resp: { status_msg: 'redirect blocked' } }), {
status: 302,
headers: { Location: PRIVATE_REDIRECT_LOCATION },
}),
expectedResult: { success: false, message: 'API error: redirect blocked' },
assertRequest: () => {
expect(fetchMock).toHaveBeenCalledWith(
'https://minimax-video.example.com/v1/video_generation',
{
method: 'POST',
redirect: 'manual',
headers: {
Authorization: 'Bearer minimax-video-key',
'Content-Type': 'application/json; charset=utf-8',
},
body: JSON.stringify({
model: 'MiniMax-Hailuo-2.3',
prompt: 'test connectivity',
duration: 6,
resolution: '768P',
}),
},
);
},
},
{
name: 'ComfyUI',
probe: () =>
testComfyuiImageConnectivity({
providerId: 'comfyui-image',
apiKey: '',
baseUrl: 'https://comfyui.example.com/',
}),
redirectResponse: () =>
new Response(null, {
status: 302,
headers: { Location: PRIVATE_REDIRECT_LOCATION },
}),
expectedResult: {
success: false,
message: 'ComfyUI returned HTTP 302. Is it running at https://comfyui.example.com?',
},
assertRequest: () => {
expect(fetchMock).toHaveBeenCalledWith('https://comfyui.example.com/system_stats', {
redirect: 'manual',
signal: expect.any(AbortSignal),
});
},
},
];
afterEach(() => {
fetchMock.mockReset();
});
describe('auth-only connectivity probe characterization', () => {
it.each(authOnlyCases)(
'$name preserves its request and treats a non-auth HTTP error as connected',
async ({ providerName, probe, assertRequest }) => {
fetchMock.mockResolvedValueOnce(
new Response('server error', { status: 500, statusText: 'Internal Server Error' }),
);
await expect(probe()).resolves.toEqual({
success: true,
message: `Connected to ${providerName}`,
});
expect(fetchMock).toHaveBeenCalledTimes(1);
assertRequest();
},
);
it.each(authOnlyCases)(
'$name preserves its 401 verdict and message',
async ({ providerName, probe }) => {
fetchMock.mockResolvedValueOnce(new Response('invalid key', { status: 401 }));
await expect(probe()).resolves.toEqual({
success: false,
message: `${providerName} auth failed (401): invalid key`,
});
expect(fetchMock).toHaveBeenCalledTimes(1);
},
);
it.each(authOnlyCases)(
'$name preserves its network-error verdict and message',
async ({ providerName, probe }) => {
fetchMock.mockRejectedValueOnce(new Error('offline'));
await expect(probe()).resolves.toEqual({
success: false,
message: `${providerName} connectivity error: Error: offline`,
});
expect(fetchMock).toHaveBeenCalledTimes(1);
},
);
it('keeps Kling credential parsing inside the connectivity error boundary', async () => {
const result = await testKlingConnectivity({
providerId: 'kling',
apiKey: 'malformed-key',
});
expect(result).toEqual({
success: false,
message: 'Kling connectivity error: Error: Kling apiKey must be "accessKey:secretKey" format',
});
expect(fetchMock).not.toHaveBeenCalled();
});
});
describe('strict connectivity probe redirect handling', () => {
it.each(strictProbeCases)(
'$name stops a private redirect after one request and preserves its failure',
async ({ probe, redirectResponse, expectedResult, assertRequest }) => {
fetchMock.mockResolvedValueOnce(redirectResponse());
await expect(probe()).resolves.toEqual(expectedResult);
expect(fetchMock).toHaveBeenCalledTimes(1);
assertRequest();
},
);
});
describe('non-matching connectivity probe sentinels', () => {
it('keeps MiniMax Image strict for non-2xx responses', async () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ base_resp: { status_msg: 'image unavailable' } }), {
status: 500,
statusText: 'Internal Server Error',
}),
);
await expect(
testMiniMaxImageConnectivity({ providerId: 'minimax-image', apiKey: 'minimax-key' }),
).resolves.toEqual({ success: false, message: 'API error: image unavailable' });
});
it('keeps MiniMax Video strict for non-2xx responses', async () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ base_resp: { status_msg: 'video unavailable' } }), {
status: 500,
statusText: 'Internal Server Error',
}),
);
await expect(
testMiniMaxVideoConnectivity({ providerId: 'minimax-video', apiKey: 'minimax-key' }),
).resolves.toEqual({ success: false, message: 'API error: video unavailable' });
});
const googleCases = [
{
name: 'Veo',
probe: () =>
testVeoConnectivity({
providerId: 'veo',
apiKey: 'google-key',
baseUrl: 'https://google.example.com',
model: 'veo-test',
}),
successMessage: 'Connected to Veo (veo-test)',
failureMessage:
'Invalid API key or unauthorized (400). Check your API Key and Base URL match the same provider.',
redirectFailureMessage: 'Veo connectivity failed (302): redirect blocked',
},
{
name: 'Nano Banana',
probe: () =>
testNanoBananaConnectivity({
providerId: 'nano-banana',
apiKey: 'google-key',
baseUrl: 'https://google.example.com',
model: 'nano-test',
}),
successMessage: 'Connected to Nano Banana (nano-test)',
failureMessage:
'Invalid API key or unauthorized (400). Check your API Key and Base URL match the same provider.',
redirectFailureMessage: 'Nano Banana connectivity failed (302): redirect blocked',
},
];
it.each(googleCases)(
'$name keeps its query-to-header auth fallback',
async ({ probe, successMessage }) => {
fetchMock
.mockResolvedValueOnce(new Response('query auth failed', { status: 401 }))
.mockResolvedValueOnce(new Response(null, { status: 200 }));
await expect(probe()).resolves.toEqual({ success: true, message: successMessage });
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'https://google.example.com/v1beta/models?key=google-key',
{ method: 'GET', redirect: 'manual' },
);
expect(fetchMock).toHaveBeenNthCalledWith(2, 'https://google.example.com/v1beta/models', {
method: 'GET',
redirect: 'manual',
headers: { 'x-goog-api-key': 'google-key' },
});
},
);
it.each(googleCases)(
'$name retries a manual 302 with header auth and accepts the 200 fallback',
async ({ probe, successMessage }) => {
fetchMock
.mockResolvedValueOnce(
new Response('redirect blocked', {
status: 302,
headers: { Location: PRIVATE_REDIRECT_LOCATION },
}),
)
.mockResolvedValueOnce(new Response(null, { status: 200 }));
await expect(probe()).resolves.toEqual({ success: true, message: successMessage });
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'https://google.example.com/v1beta/models?key=google-key',
{ method: 'GET', redirect: 'manual' },
);
expect(fetchMock).toHaveBeenNthCalledWith(2, 'https://google.example.com/v1beta/models', {
method: 'GET',
redirect: 'manual',
headers: { 'x-goog-api-key': 'google-key' },
});
},
);
it.each(googleCases)(
'$name keeps treating a final 400 as an invalid-key failure',
async ({ probe, failureMessage }) => {
fetchMock
.mockResolvedValueOnce(new Response('bad query key', { status: 400 }))
.mockResolvedValueOnce(new Response('bad header key', { status: 400 }));
await expect(probe()).resolves.toEqual({ success: false, message: failureMessage });
expect(fetchMock).toHaveBeenCalledTimes(2);
},
);
it.each(googleCases)(
'$name stops after a final manual 302 and preserves its connectivity failure',
async ({ probe, redirectFailureMessage }) => {
fetchMock
.mockResolvedValueOnce(new Response('query auth failed', { status: 401 }))
.mockResolvedValueOnce(
new Response('redirect blocked', {
status: 302,
headers: { Location: PRIVATE_REDIRECT_LOCATION },
}),
);
await expect(probe()).resolves.toEqual({
success: false,
message: redirectFailureMessage,
});
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'https://google.example.com/v1beta/models?key=google-key',
{ method: 'GET', redirect: 'manual' },
);
expect(fetchMock).toHaveBeenNthCalledWith(2, 'https://google.example.com/v1beta/models', {
method: 'GET',
redirect: 'manual',
headers: { 'x-goog-api-key': 'google-key' },
});
},
);
});