chore: establish learning module baseline
This commit is contained in:
commit
2d04197f3f
2409 files changed
+533050
No files matched your search
@@ -0,0 +1,217 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
isServerConfiguredProvider: vi.fn(),
|
||||
resolveManagedAliDocMindCredentials: vi.fn(),
|
||||
resolvePDFApiKey: vi.fn(),
|
||||
resolvePDFBaseUrl: vi.fn(),
|
||||
validateUrlForSSRF: vi.fn(),
|
||||
fetchPinnedPublicUrl: vi.fn(),
|
||||
PublicUrlFetchError: class MockPublicUrlFetchError extends Error {
|
||||
constructor(
|
||||
readonly code: 'INVALID_URL' | 'BLOCKED_TARGET' | 'DNS_FAILURE' | 'UPSTREAM_FAILURE',
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/server/provider-config', () => ({
|
||||
isServerConfiguredProvider: mocks.isServerConfiguredProvider,
|
||||
resolveManagedAliDocMindCredentials: mocks.resolveManagedAliDocMindCredentials,
|
||||
resolvePDFApiKey: mocks.resolvePDFApiKey,
|
||||
resolvePDFBaseUrl: mocks.resolvePDFBaseUrl,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/server/ssrf-guard', () => ({
|
||||
validateUrlForSSRF: mocks.validateUrlForSSRF,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/server/public-url-fetch', () => ({
|
||||
fetchPinnedPublicUrl: mocks.fetchPinnedPublicUrl,
|
||||
PublicUrlFetchError: mocks.PublicUrlFetchError,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/logger', () => ({
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
async function postVerifyPdfProvider(body: Record<string, unknown>) {
|
||||
const { POST } = await import('@/app/api/verify-pdf-provider/route');
|
||||
const request = new Request('http://localhost/api/verify-pdf-provider', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return POST(request as unknown as NextRequest);
|
||||
}
|
||||
|
||||
describe('POST /api/verify-pdf-provider', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
mocks.isServerConfiguredProvider.mockReset();
|
||||
mocks.resolveManagedAliDocMindCredentials.mockReset();
|
||||
mocks.resolvePDFApiKey.mockReset();
|
||||
mocks.resolvePDFBaseUrl.mockReset();
|
||||
mocks.validateUrlForSSRF.mockReset();
|
||||
mocks.fetchPinnedPublicUrl.mockReset();
|
||||
|
||||
mocks.isServerConfiguredProvider.mockReturnValue(false);
|
||||
mocks.resolvePDFApiKey.mockImplementation(
|
||||
(_providerId: string, clientApiKey?: string) => clientApiKey,
|
||||
);
|
||||
mocks.resolvePDFBaseUrl.mockImplementation(
|
||||
(_providerId: string, clientBaseUrl?: string) => clientBaseUrl,
|
||||
);
|
||||
mocks.validateUrlForSSRF.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('rejects a MinerU Cloud redirect after one request without reading its body', async () => {
|
||||
const text = vi.fn().mockResolvedValue('redirect response body');
|
||||
const dispose = vi.fn().mockResolvedValue(undefined);
|
||||
mocks.fetchPinnedPublicUrl.mockResolvedValue({
|
||||
response: { status: 302, text },
|
||||
dispose,
|
||||
});
|
||||
|
||||
const res = await postVerifyPdfProvider({
|
||||
providerId: 'mineru-cloud',
|
||||
apiKey: 'test-key',
|
||||
baseUrl: 'https://mineru.example.com',
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(json).toEqual({
|
||||
success: false,
|
||||
errorCode: 'REDIRECT_NOT_ALLOWED',
|
||||
error: 'Redirects are not allowed',
|
||||
});
|
||||
expect(mocks.fetchPinnedPublicUrl).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.fetchPinnedPublicUrl).toHaveBeenCalledWith(
|
||||
'https://mineru.example.com/extract-results/batch/test-connection',
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
Authorization: 'Bearer test-key',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(text).not.toHaveBeenCalled();
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('preserves MinerU Cloud success responses', async () => {
|
||||
const dispose = vi.fn().mockResolvedValue(undefined);
|
||||
mocks.fetchPinnedPublicUrl.mockResolvedValue({ response: { status: 200 }, dispose });
|
||||
|
||||
const res = await postVerifyPdfProvider({
|
||||
providerId: 'mineru-cloud',
|
||||
apiKey: 'test-key',
|
||||
baseUrl: 'https://mineru.example.com/',
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json).toEqual({
|
||||
success: true,
|
||||
message: 'Connection successful',
|
||||
status: 200,
|
||||
});
|
||||
expect(mocks.fetchPinnedPublicUrl).toHaveBeenCalledWith(
|
||||
'https://mineru.example.com/extract-results/batch/test-connection',
|
||||
expect.objectContaining({ maxResponseBytes: 1024 * 1024 }),
|
||||
);
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it.each([401, 403])('preserves MinerU Cloud authentication handling for %i', async (status) => {
|
||||
const text = vi.fn().mockResolvedValue('invalid token');
|
||||
const dispose = vi.fn().mockResolvedValue(undefined);
|
||||
mocks.fetchPinnedPublicUrl.mockResolvedValue({
|
||||
response: { status, statusText: 'Unauthorized', text },
|
||||
dispose,
|
||||
});
|
||||
|
||||
const res = await postVerifyPdfProvider({
|
||||
providerId: 'mineru-cloud',
|
||||
apiKey: 'bad-key',
|
||||
baseUrl: 'https://mineru.example.com',
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(500);
|
||||
expect(json).toEqual({
|
||||
success: false,
|
||||
errorCode: 'INTERNAL_ERROR',
|
||||
error: 'Authentication failed: invalid token',
|
||||
});
|
||||
expect(mocks.fetchPinnedPublicUrl).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.fetchPinnedPublicUrl).toHaveBeenCalledWith(
|
||||
'https://mineru.example.com/extract-results/batch/test-connection',
|
||||
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
||||
);
|
||||
expect(text).toHaveBeenCalledTimes(1);
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('blocks a MinerU Cloud metadata target without leaking the resolved address', async () => {
|
||||
mocks.fetchPinnedPublicUrl.mockRejectedValue(
|
||||
new mocks.PublicUrlFetchError('BLOCKED_TARGET', '100.100.100.200 is blocked'),
|
||||
);
|
||||
|
||||
const res = await postVerifyPdfProvider({
|
||||
providerId: 'mineru-cloud',
|
||||
apiKey: 'test-key',
|
||||
baseUrl: 'http://100.100.100.200',
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(json).toEqual({
|
||||
success: false,
|
||||
errorCode: 'INVALID_URL',
|
||||
error: 'Cloud endpoint is not an allowed public URL',
|
||||
});
|
||||
expect(JSON.stringify(json)).not.toContain('100.100.100.200');
|
||||
});
|
||||
|
||||
it('keeps the existing self-hosted redirect rejection contract', async () => {
|
||||
const text = vi.fn();
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
status: 308,
|
||||
text,
|
||||
} as unknown as Response);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
const res = await postVerifyPdfProvider({
|
||||
providerId: 'mineru',
|
||||
baseUrl: 'https://mineru-self-hosted.example.com',
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(json).toEqual({
|
||||
success: false,
|
||||
errorCode: 'REDIRECT_NOT_ALLOWED',
|
||||
error: 'Redirects are not allowed',
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://mineru-self-hosted.example.com',
|
||||
expect.objectContaining({ redirect: 'manual' }),
|
||||
);
|
||||
expect(text).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user