openreader/tests/unit/pdf-parsed-route-worker-flow.vitest.spec.ts
Richard R 358a56cc9a refactor: implement modular monorepo packages
- 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
2026-06-13 13:51:18 -06:00

228 lines
7.9 KiB
TypeScript

import { 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(),
readCurrentParsedPdfArtifact: vi.fn(),
readParsedPdfArtifactByKey: vi.fn(),
lookupCurrentPdfParseOperation: vi.fn(),
createOrReuseCurrentPdfParseOperation: vi.fn(),
checkJobRate: vi.fn(),
getPdfLayoutRateConfig: vi.fn(),
getResolvedRuntimeConfig: vi.fn(),
buildComputeRateLimitedResponse: 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/artifact', () => ({
readCurrentParsedPdfArtifact: hoisted.readCurrentParsedPdfArtifact,
readParsedPdfArtifactByKey: hoisted.readParsedPdfArtifactByKey,
}));
vi.mock('@/lib/server/pdf-parse/operation', () => ({
lookupCurrentPdfParseOperation: hoisted.lookupCurrentPdfParseOperation,
createOrReuseCurrentPdfParseOperation: hoisted.createOrReuseCurrentPdfParseOperation,
}));
vi.mock('@/lib/server/rate-limit/job-rate-limiter', () => ({
checkJobRate: hoisted.checkJobRate,
getPdfLayoutRateConfig: hoisted.getPdfLayoutRateConfig,
}));
vi.mock('@/lib/server/rate-limit/problem-response', () => ({
buildComputeRateLimitedResponse: hoisted.buildComputeRateLimitedResponse,
}));
vi.mock('@/lib/server/runtime-config', () => ({
getResolvedRuntimeConfig: hoisted.getResolvedRuntimeConfig,
}));
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/POST /api/documents/[id]/parsed worker flow', () => {
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.readCurrentParsedPdfArtifact.mockReset();
hoisted.readCurrentParsedPdfArtifact.mockResolvedValue(null);
hoisted.readParsedPdfArtifactByKey.mockReset();
hoisted.readParsedPdfArtifactByKey.mockResolvedValue(null);
hoisted.lookupCurrentPdfParseOperation.mockReset();
hoisted.lookupCurrentPdfParseOperation.mockResolvedValue(null);
hoisted.createOrReuseCurrentPdfParseOperation.mockReset();
hoisted.checkJobRate.mockReset();
hoisted.checkJobRate.mockResolvedValue({ allowed: true });
hoisted.getPdfLayoutRateConfig.mockReset();
hoisted.getPdfLayoutRateConfig.mockReturnValue({});
hoisted.getResolvedRuntimeConfig.mockReset();
hoisted.getResolvedRuntimeConfig.mockResolvedValue({});
hoisted.buildComputeRateLimitedResponse.mockReset();
});
test('GET returns pending when no current artifact or worker op exists', async () => {
const { GET } = await import('../../src/app/api/documents/[id]/parsed/route');
const response = await GET(new NextRequest('http://localhost/api/documents/doc-1/parsed'), {
params: Promise.resolve({ id: 'doc-1' }),
});
expect(response.status).toBe(409);
await expect(response.json()).resolves.toMatchObject({
parseStatus: 'pending',
parseProgress: null,
opId: null,
});
});
test('GET reads the artifact referenced by a succeeded worker op', async () => {
hoisted.lookupCurrentPdfParseOperation.mockResolvedValue({
opId: 'op-1',
opKey: 'pdf_layout|v1|parser|doc-1||doc-key|',
kind: 'pdf_layout',
jobId: 'job-1',
status: 'succeeded',
queuedAt: Date.now() - 1000,
updatedAt: Date.now(),
result: { parsedObjectKey: 'parsed-key.json' },
});
hoisted.readParsedPdfArtifactByKey.mockResolvedValue({
key: 'parsed-key.json',
bytes: Buffer.from(JSON.stringify({ documentId: 'doc-1', pages: [] })),
parsed: { documentId: 'doc-1', pages: [] },
});
const { GET } = await import('../../src/app/api/documents/[id]/parsed/route');
const response = await GET(new NextRequest('http://localhost/api/documents/doc-1/parsed'), {
params: Promise.resolve({ id: 'doc-1' }),
});
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({ documentId: 'doc-1' });
expect(hoisted.readParsedPdfArtifactByKey).toHaveBeenCalledWith('parsed-key.json');
});
test('POST creates a worker op when replace is requested', async () => {
hoisted.createOrReuseCurrentPdfParseOperation.mockResolvedValue({
opId: 'op-force-1',
opKey: 'pdf_layout|v1|parser|doc-1||doc-key|force',
kind: 'pdf_layout',
jobId: 'job-force-1',
status: 'queued',
queuedAt: Date.now(),
updatedAt: Date.now(),
});
const { POST } = await import('../../src/app/api/documents/[id]/parsed/route');
const request = new NextRequest('http://localhost/api/documents/doc-1/parsed', {
method: 'POST',
body: JSON.stringify({ replace: true }),
headers: { 'Content-Type': 'application/json' },
});
const response = await POST(request, {
params: Promise.resolve({ id: 'doc-1' }),
});
expect(response.status).toBe(202);
await expect(response.json()).resolves.toMatchObject({
parseStatus: 'pending',
opId: 'op-force-1',
});
expect(hoisted.createOrReuseCurrentPdfParseOperation).toHaveBeenCalled();
});
test('POST does not report ready for a succeeded op until its artifact is readable', async () => {
hoisted.lookupCurrentPdfParseOperation.mockResolvedValue({
opId: 'op-ready-1',
opKey: 'pdf_layout|v1|parser|doc-1||doc-key|',
kind: 'pdf_layout',
jobId: 'job-ready-1',
status: 'succeeded',
queuedAt: Date.now() - 1000,
updatedAt: Date.now(),
result: { parsedObjectKey: 'missing-parsed-key.json' },
});
hoisted.readParsedPdfArtifactByKey.mockResolvedValue(null);
const { POST } = await import('../../src/app/api/documents/[id]/parsed/route');
const request = new NextRequest('http://localhost/api/documents/doc-1/parsed', {
method: 'POST',
body: JSON.stringify({ replace: false }),
headers: { 'Content-Type': 'application/json' },
});
const response = await POST(request, {
params: Promise.resolve({ id: 'doc-1' }),
});
expect(response.status).toBe(202);
await expect(response.json()).resolves.toMatchObject({
parseStatus: 'running',
opId: 'op-ready-1',
});
expect(hoisted.readParsedPdfArtifactByKey).toHaveBeenCalledWith('missing-parsed-key.json');
expect(hoisted.createOrReuseCurrentPdfParseOperation).not.toHaveBeenCalled();
});
test('POST returns the rate-limited response without creating a worker op', async () => {
const sentinel = new Response('rate limited', { status: 429 });
hoisted.checkJobRate.mockResolvedValue({ allowed: false });
hoisted.buildComputeRateLimitedResponse.mockReturnValue(sentinel);
const { POST } = await import('../../src/app/api/documents/[id]/parsed/route');
const request = new NextRequest('http://localhost/api/documents/doc-1/parsed', {
method: 'POST',
body: JSON.stringify({ replace: true }),
headers: { 'Content-Type': 'application/json' },
});
const response = await POST(request, {
params: Promise.resolve({ id: 'doc-1' }),
});
expect(response).toBe(sentinel);
expect(hoisted.createOrReuseCurrentPdfParseOperation).not.toHaveBeenCalled();
});
});