- 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
228 lines
7.9 KiB
TypeScript
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();
|
|
});
|
|
});
|