Remove the separate docx-to-pdf upload API and integrate DOCX conversion directly into the blob upload finalize route. Update client and uploader logic to treat DOCX as a supported upload type and streamline batch state handling. Adjust document caching and test helpers to align with the new unified flow. Add server-side DOCX-to-PDF conversion utility and corresponding unit tests. This change simplifies the upload pipeline, consolidates conversion logic, and improves maintainability.
150 lines
5.3 KiB
TypeScript
150 lines
5.3 KiB
TypeScript
import { createHash } from 'node:crypto';
|
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
import { NextRequest } from 'next/server';
|
|
|
|
const hoisted = vi.hoisted(() => ({
|
|
requireAuthContext: vi.fn(),
|
|
registerUploadedDocument: vi.fn(),
|
|
convertDocxBufferToPdfBuffer: vi.fn(),
|
|
headTempDocumentBlob: vi.fn(),
|
|
getTempDocumentBlob: vi.fn(),
|
|
getTempDocumentFinalizeReceipt: vi.fn(),
|
|
putTempDocumentFinalizeReceipt: vi.fn(),
|
|
deleteTempDocumentUpload: vi.fn(),
|
|
headDocumentBlob: vi.fn(),
|
|
copyTempDocumentBlobToDocument: vi.fn(),
|
|
putDocumentBlob: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@/lib/server/auth/auth', () => ({
|
|
requireAuthContext: hoisted.requireAuthContext,
|
|
}));
|
|
|
|
vi.mock('@/lib/server/documents/register-upload', () => ({
|
|
registerUploadedDocument: hoisted.registerUploadedDocument,
|
|
}));
|
|
|
|
vi.mock('@/lib/server/documents/docx-convert', () => ({
|
|
convertDocxBufferToPdfBuffer: hoisted.convertDocxBufferToPdfBuffer,
|
|
}));
|
|
|
|
vi.mock('@/lib/server/documents/blobstore', () => ({
|
|
TEMP_DOCUMENT_UPLOAD_TTL_MS: 24 * 60 * 60 * 1000,
|
|
copyTempDocumentBlobToDocument: hoisted.copyTempDocumentBlobToDocument,
|
|
deleteTempDocumentUpload: hoisted.deleteTempDocumentUpload,
|
|
getTempDocumentBlob: hoisted.getTempDocumentBlob,
|
|
getTempDocumentFinalizeReceipt: hoisted.getTempDocumentFinalizeReceipt,
|
|
headDocumentBlob: hoisted.headDocumentBlob,
|
|
headTempDocumentBlob: hoisted.headTempDocumentBlob,
|
|
isMissingBlobError: vi.fn((error: unknown) => {
|
|
const maybe = error as { code?: string } | undefined;
|
|
return maybe?.code === 'NoSuchKey';
|
|
}),
|
|
isPreconditionFailed: vi.fn(() => false),
|
|
isValidTempUploadToken: vi.fn(() => true),
|
|
putDocumentBlob: hoisted.putDocumentBlob,
|
|
putTempDocumentFinalizeReceipt: hoisted.putTempDocumentFinalizeReceipt,
|
|
}));
|
|
|
|
vi.mock('@/lib/server/testing/test-namespace', () => ({
|
|
getOpenReaderTestNamespace: vi.fn(() => null),
|
|
}));
|
|
|
|
vi.mock('@/lib/server/storage/s3', () => ({
|
|
isS3Configured: vi.fn(() => true),
|
|
}));
|
|
|
|
vi.mock('@/lib/server/logger', () => ({
|
|
errorToLog: vi.fn((error: unknown) => error),
|
|
serverLogger: {
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
describe('POST /api/documents/blob/upload/finalize DOCX flow', () => {
|
|
beforeEach(() => {
|
|
hoisted.requireAuthContext.mockReset();
|
|
hoisted.registerUploadedDocument.mockReset();
|
|
hoisted.convertDocxBufferToPdfBuffer.mockReset();
|
|
hoisted.headTempDocumentBlob.mockReset();
|
|
hoisted.getTempDocumentBlob.mockReset();
|
|
hoisted.getTempDocumentFinalizeReceipt.mockReset();
|
|
hoisted.putTempDocumentFinalizeReceipt.mockReset();
|
|
hoisted.deleteTempDocumentUpload.mockReset();
|
|
hoisted.headDocumentBlob.mockReset();
|
|
hoisted.copyTempDocumentBlobToDocument.mockReset();
|
|
hoisted.putDocumentBlob.mockReset();
|
|
|
|
hoisted.requireAuthContext.mockResolvedValue({ userId: 'user-1' });
|
|
hoisted.getTempDocumentFinalizeReceipt.mockResolvedValue(null);
|
|
hoisted.headTempDocumentBlob.mockResolvedValue({
|
|
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
contentLength: 12,
|
|
lastModified: Date.now(),
|
|
});
|
|
hoisted.getTempDocumentBlob.mockResolvedValue(Buffer.from('docx-bytes'));
|
|
hoisted.convertDocxBufferToPdfBuffer.mockResolvedValue(Buffer.from('pdf-bytes'));
|
|
hoisted.headDocumentBlob
|
|
.mockRejectedValueOnce({ code: 'NoSuchKey' })
|
|
.mockResolvedValue({
|
|
contentLength: Buffer.byteLength('pdf-bytes'),
|
|
contentType: 'application/pdf',
|
|
eTag: 'etag-1',
|
|
});
|
|
hoisted.registerUploadedDocument.mockImplementation(async (input: { documentId: string; name: string; type: string; size: number; lastModified: number }) => ({
|
|
id: input.documentId,
|
|
name: input.name,
|
|
type: input.type,
|
|
size: input.size,
|
|
lastModified: input.lastModified,
|
|
scope: 'user',
|
|
}));
|
|
hoisted.putTempDocumentFinalizeReceipt.mockResolvedValue(undefined);
|
|
hoisted.deleteTempDocumentUpload.mockResolvedValue(undefined);
|
|
});
|
|
|
|
test('converts raw DOCX during finalize and registers a PDF', async () => {
|
|
const { POST } = await import('../../src/app/api/documents/blob/upload/finalize/route');
|
|
const request = new NextRequest('http://localhost/api/documents/blob/upload/finalize', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
uploads: [{
|
|
token: '123e4567-e89b-12d3-a456-426614174000',
|
|
name: 'Report.docx',
|
|
type: 'docx',
|
|
lastModified: 1700000000000,
|
|
}],
|
|
}),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
const response = await POST(request);
|
|
const expectedId = createHash('sha256').update(Buffer.from('pdf-bytes')).digest('hex');
|
|
|
|
expect(response.status).toBe(200);
|
|
await expect(response.json()).resolves.toMatchObject({
|
|
stored: [{
|
|
id: expectedId,
|
|
name: 'Report.pdf',
|
|
type: 'pdf',
|
|
}],
|
|
});
|
|
expect(hoisted.convertDocxBufferToPdfBuffer).toHaveBeenCalledTimes(1);
|
|
expect(hoisted.putDocumentBlob).toHaveBeenCalledWith(
|
|
expectedId,
|
|
Buffer.from('pdf-bytes'),
|
|
'application/pdf',
|
|
null,
|
|
{ ifNoneMatch: true },
|
|
);
|
|
expect(hoisted.copyTempDocumentBlobToDocument).not.toHaveBeenCalled();
|
|
expect(hoisted.registerUploadedDocument).toHaveBeenCalledWith(expect.objectContaining({
|
|
documentId: expectedId,
|
|
name: 'Report.pdf',
|
|
type: 'pdf',
|
|
}));
|
|
});
|
|
});
|