- Extract database schema and migrations into @openreader/database - Extract startup and orchestration scripts into @openreader/bootstrap - Move compute-worker into packages/compute-worker - Remove runtime dependency on drizzle-kit - Update Dockerfile to deploy isolated packages without merging node_modules
146 lines
4.7 KiB
TypeScript
146 lines
4.7 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
import { NextRequest } from 'next/server';
|
|
|
|
const hoisted = vi.hoisted(() => ({
|
|
db: null as {
|
|
select: ReturnType<typeof vi.fn>;
|
|
} | null,
|
|
row: {
|
|
id: 'doc-1',
|
|
type: 'pdf',
|
|
},
|
|
requireAuthContext: vi.fn(),
|
|
fetchPdfParseOperation: vi.fn(),
|
|
isPdfParseOperationForDocument: vi.fn(),
|
|
openOperationEvents: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@openreader/database', () => ({
|
|
get db() {
|
|
return hoisted.db;
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/lib/server/auth/auth', () => ({
|
|
requireAuthContext: hoisted.requireAuthContext,
|
|
}));
|
|
|
|
vi.mock('@/lib/server/pdf-parse/operation', () => ({
|
|
fetchPdfParseOperation: hoisted.fetchPdfParseOperation,
|
|
isPdfParseOperationForDocument: hoisted.isPdfParseOperationForDocument,
|
|
}));
|
|
|
|
vi.mock('@/lib/server/compute-worker/client', () => ({
|
|
getComputeWorkerClient: () => ({
|
|
openOperationEvents: hoisted.openOperationEvents,
|
|
}),
|
|
}));
|
|
|
|
vi.mock('@/lib/server/documents/blobstore', () => ({
|
|
isValidDocumentId: vi.fn(() => true),
|
|
}));
|
|
|
|
vi.mock('@/lib/server/storage/s3', () => ({
|
|
isS3Configured: vi.fn(() => true),
|
|
}));
|
|
|
|
vi.mock('@/lib/server/testing/test-namespace', () => ({
|
|
getOpenReaderTestNamespace: vi.fn(() => null),
|
|
}));
|
|
|
|
vi.mock('@/lib/server/logger', () => ({
|
|
createRequestLogger: vi.fn(() => ({
|
|
logger: {
|
|
warn: vi.fn(),
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
},
|
|
requestId: 'req-test',
|
|
})),
|
|
}));
|
|
|
|
describe('GET /api/documents/[id]/parsed/events worker event proxy', () => {
|
|
const originalFetch = global.fetch;
|
|
|
|
beforeEach(() => {
|
|
hoisted.db = {
|
|
select: vi.fn(() => ({
|
|
from: vi.fn(() => ({
|
|
where: vi.fn(() => ({
|
|
limit: vi.fn(async () => [{ ...hoisted.row }]),
|
|
})),
|
|
})),
|
|
})),
|
|
};
|
|
hoisted.requireAuthContext.mockReset();
|
|
hoisted.requireAuthContext.mockResolvedValue({ userId: 'user-1' });
|
|
hoisted.fetchPdfParseOperation.mockReset();
|
|
hoisted.fetchPdfParseOperation.mockResolvedValue({
|
|
opId: 'op-1',
|
|
opKey: 'pdf_layout|v1|parser|doc-1||doc-key|',
|
|
kind: 'pdf_layout',
|
|
jobId: 'job-1',
|
|
status: 'running',
|
|
queuedAt: Date.now() - 1000,
|
|
updatedAt: Date.now(),
|
|
});
|
|
hoisted.isPdfParseOperationForDocument.mockReset();
|
|
hoisted.isPdfParseOperationForDocument.mockReturnValue(true);
|
|
hoisted.openOperationEvents.mockReset();
|
|
hoisted.openOperationEvents.mockResolvedValue(new Response(
|
|
'event: snapshot\ndata: {"eventId":1,"snapshot":{"opId":"op-1","status":"running"}}\n\n',
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
},
|
|
},
|
|
));
|
|
});
|
|
|
|
test('requires an opId', async () => {
|
|
const { GET } = await import('../../src/app/api/documents/[id]/parsed/events/route');
|
|
const response = await GET(new NextRequest('http://localhost/api/documents/doc-1/parsed/events'), {
|
|
params: Promise.resolve({ id: 'doc-1' }),
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
await expect(response.json()).resolves.toMatchObject({ error: 'opId is required' });
|
|
});
|
|
|
|
test('proxies the worker SSE stream after validating document ownership', async () => {
|
|
const { GET } = await import('../../src/app/api/documents/[id]/parsed/events/route');
|
|
const response = await GET(new NextRequest('http://localhost/api/documents/doc-1/parsed/events?opId=op-1'), {
|
|
params: Promise.resolve({ id: 'doc-1' }),
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers.get('content-type')).toContain('text/event-stream');
|
|
const text = await response.text();
|
|
expect(text).toContain('event: snapshot');
|
|
expect(text).toContain('"opId":"op-1"');
|
|
expect(hoisted.fetchPdfParseOperation).toHaveBeenCalledWith('op-1');
|
|
expect(hoisted.isPdfParseOperationForDocument).toHaveBeenCalled();
|
|
expect(hoisted.openOperationEvents).toHaveBeenCalledWith('op-1', expect.objectContaining({
|
|
signal: expect.any(AbortSignal),
|
|
}));
|
|
});
|
|
|
|
test('denies proxying when the op does not belong to the document', async () => {
|
|
hoisted.isPdfParseOperationForDocument.mockReturnValue(false);
|
|
|
|
const { GET } = await import('../../src/app/api/documents/[id]/parsed/events/route');
|
|
const response = await GET(new NextRequest('http://localhost/api/documents/doc-1/parsed/events?opId=op-1'), {
|
|
params: Promise.resolve({ id: 'doc-1' }),
|
|
});
|
|
|
|
expect(response.status).toBe(404);
|
|
await expect(response.json()).resolves.toMatchObject({ error: 'Operation not found' });
|
|
expect(hoisted.fetchPdfParseOperation).toHaveBeenCalledWith('op-1');
|
|
expect(hoisted.openOperationEvents).not.toHaveBeenCalled();
|
|
});
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch;
|
|
});
|
|
});
|