Transition document upload flow to use presigned token-based temporary uploads. Add finalize endpoint to register uploaded documents after blob transfer, decoupling document ID assignment from upload initiation. Update client and server logic to operate on tokens, improve security, and enable canonical ID reuse for identical uploads. Update tests to verify deduplication behavior.
209 lines
8.4 KiB
TypeScript
209 lines
8.4 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import { uploadFile, uploadAndDisplay, setupTest, expectDocumentListed, uploadFiles, ensureDocumentsListed, clickDocumentLink, expectViewerForFile } from './helpers';
|
|
|
|
interface HtmlDocumentRow {
|
|
id?: string;
|
|
data?: string;
|
|
}
|
|
|
|
type HashCheckResult =
|
|
| { ok: true; storedId: string; computedId: string }
|
|
| { ok: false; reason: 'Missing stored html document' | 'Hash mismatch'; storedId?: string; computedId?: string };
|
|
|
|
test.describe('Document Upload Tests', () => {
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
await setupTest(page, testInfo);
|
|
});
|
|
|
|
test('uploads a PDF document', async ({ page }) => {
|
|
await uploadFile(page, 'sample.pdf');
|
|
await expectDocumentListed(page, 'sample.pdf');
|
|
});
|
|
|
|
test('uploads an EPUB document', async ({ page }) => {
|
|
await uploadFile(page, 'sample.epub');
|
|
await expectDocumentListed(page, 'sample.epub');
|
|
});
|
|
|
|
test('uploads a TXT document', async ({ page }) => {
|
|
await uploadFile(page, 'sample.txt');
|
|
await expectDocumentListed(page, 'sample.txt');
|
|
});
|
|
|
|
test('reuses the same canonical id for identical uploads', async ({ page }) => {
|
|
await uploadFile(page, 'sample.pdf');
|
|
await uploadFile(page, 'sample.pdf');
|
|
|
|
const result = await page.evaluate(async () => {
|
|
const res = await fetch('/api/documents', { cache: 'no-store' });
|
|
if (!res.ok) {
|
|
return { ok: false as const, reason: `status:${res.status}` };
|
|
}
|
|
const data = await res.json() as { documents?: Array<{ id: string; name: string }> };
|
|
const matching = (data.documents || []).filter((doc) => doc.name === 'sample.pdf');
|
|
const uniqueIds = Array.from(new Set(matching.map((doc) => doc.id)));
|
|
return { ok: true as const, matchingCount: matching.length, uniqueIds };
|
|
});
|
|
|
|
expect(result.ok).toBeTruthy();
|
|
if (!result.ok) {
|
|
throw new Error(`Failed to inspect uploaded documents: ${result.reason}`);
|
|
}
|
|
expect(result.matchingCount).toBe(1);
|
|
expect(result.uniqueIds).toHaveLength(1);
|
|
});
|
|
|
|
test('hashes text/HTML docs using UTF-8 encoded stored string', async ({ page }) => {
|
|
await uploadFile(page, 'sample.txt');
|
|
await expectDocumentListed(page, 'sample.txt');
|
|
|
|
const result = await page.evaluate<HashCheckResult>(async () => {
|
|
const idb = await new Promise<IDBDatabase>((resolve, reject) => {
|
|
const request = indexedDB.open('openreader-db');
|
|
request.onerror = () => reject(request.error);
|
|
request.onsuccess = () => resolve(request.result);
|
|
});
|
|
|
|
try {
|
|
const docs = await new Promise<HtmlDocumentRow[]>((resolve, reject) => {
|
|
const tx = idb.transaction('html-documents', 'readonly');
|
|
const store = tx.objectStore('html-documents');
|
|
const request = store.getAll();
|
|
request.onerror = () => reject(request.error);
|
|
request.onsuccess = () => resolve(request.result as HtmlDocumentRow[]);
|
|
});
|
|
|
|
if (!docs[0]?.data || !docs[0]?.id) {
|
|
return { ok: false, reason: 'Missing stored html document' as const };
|
|
}
|
|
|
|
const bytes = new TextEncoder().encode(String(docs[0].data));
|
|
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
const computedId = Array.from(new Uint8Array(digest))
|
|
.map((b) => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
|
|
if (computedId === docs[0].id) {
|
|
return { ok: true as const, storedId: docs[0].id as string, computedId };
|
|
}
|
|
return { ok: false as const, reason: 'Hash mismatch', storedId: docs[0].id as string, computedId };
|
|
} finally {
|
|
idb.close();
|
|
}
|
|
});
|
|
|
|
const detail = result.ok
|
|
? `Expected storedId=${result.storedId} computedId=${result.computedId}`
|
|
: `Expected valid stored html document but got reason=${result.reason}`;
|
|
expect(result.ok, detail).toBeTruthy();
|
|
});
|
|
|
|
test('uploads and converts a DOCX document', async ({ page }) => {
|
|
await uploadFile(page, 'sample.docx');
|
|
// Should see the converting message (best-effort; conversion may complete extremely fast)
|
|
try {
|
|
await expect(page.getByText('Converting DOCX to PDF...')).toBeVisible({ timeout: 5000 });
|
|
} catch {
|
|
// ignore
|
|
}
|
|
// After conversion, should see the PDF with the same name
|
|
await expectDocumentListed(page, 'sample.pdf');
|
|
});
|
|
|
|
test('displays a PDF document', async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
await uploadAndDisplay(page, 'sample.pdf');
|
|
await expectViewerForFile(page, 'sample.pdf');
|
|
// Additional content checks specific to the sample PDF
|
|
await expect(page.getByRole('heading', { level: 1, name: 'sample.pdf' })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /1\s*\/\s*2/ })).toBeVisible();
|
|
});
|
|
|
|
test('displays an EPUB document', async ({ page }) => {
|
|
await uploadAndDisplay(page, 'sample.epub');
|
|
await expectViewerForFile(page, 'sample.epub');
|
|
// Navigation controls should be exposed via accessible labels
|
|
await expect(page.getByRole('button', { name: 'Previous section' })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: 'Next section' })).toBeVisible();
|
|
});
|
|
|
|
test('displays a DOCX document as PDF after conversion', async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
await uploadAndDisplay(page, 'sample.docx');
|
|
await expectViewerForFile(page, 'sample.docx'); // DOCX converts to PDF
|
|
// Keep specific content checks
|
|
await expect(page.getByText('Demonstration of DOCX')).toBeVisible();
|
|
});
|
|
|
|
test('displays a TXT document', async ({ page }) => {
|
|
await uploadAndDisplay(page, 'sample.txt');
|
|
await expectViewerForFile(page, 'sample.txt');
|
|
await expect(page.getByText('Lorem ipsum dolor sit amet')).toBeVisible();
|
|
});
|
|
|
|
test('uploads PDF/EPUB/TXT and opens correct viewer for each', async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
// Upload multiple files
|
|
await uploadFiles(page, 'sample.pdf', 'sample.epub', 'sample.txt');
|
|
|
|
// Verify all uploaded files appear in the list
|
|
await ensureDocumentsListed(page, ['sample.pdf', 'sample.epub', 'sample.txt']);
|
|
|
|
// PDF navigation and viewer
|
|
await clickDocumentLink(page, 'sample.pdf');
|
|
await expectViewerForFile(page, 'sample.pdf');
|
|
await page.goBack();
|
|
await ensureDocumentsListed(page, ['sample.pdf', 'sample.epub', 'sample.txt']);
|
|
|
|
// EPUB navigation and viewer
|
|
await clickDocumentLink(page, 'sample.epub');
|
|
await expectViewerForFile(page, 'sample.epub');
|
|
await page.goBack();
|
|
await ensureDocumentsListed(page, ['sample.pdf', 'sample.epub', 'sample.txt']);
|
|
|
|
// TXT navigation and viewer (HTML viewer)
|
|
await clickDocumentLink(page, 'sample.txt');
|
|
await expectViewerForFile(page, 'sample.txt');
|
|
});
|
|
|
|
test('renders Markdown via ReactMarkdown and keeps TXT preformatted', async ({ page }) => {
|
|
// Upload MD and TXT
|
|
await uploadFiles(page, 'sample.md', 'sample.txt');
|
|
await ensureDocumentsListed(page, ['sample.md', 'sample.txt']);
|
|
|
|
// Open MD and verify rendered markdown
|
|
await clickDocumentLink(page, 'sample.md');
|
|
await expectViewerForFile(page, 'sample.md');
|
|
const mdContainer = page.locator('.html-container');
|
|
await expect(mdContainer).toBeVisible();
|
|
// Should have prose classes (not monospace)
|
|
await expect(mdContainer).toHaveClass(/prose/);
|
|
await expect(mdContainer).not.toHaveClass(/font-mono/);
|
|
// Heading and link rendered
|
|
await expect(page.getByRole('heading', { name: 'Sample Markdown' })).toBeVisible();
|
|
await expect(page.getByRole('link', { name: 'OpenAI' })).toBeVisible();
|
|
|
|
// Go back and open TXT, verify monospace preformatted
|
|
await page.goBack();
|
|
await clickDocumentLink(page, 'sample.txt');
|
|
await expectViewerForFile(page, 'sample.txt');
|
|
const txtContainer = page.locator('.html-container');
|
|
await expect(txtContainer).toHaveClass(/font-mono/);
|
|
});
|
|
|
|
test('unsupported file type is ignored and no new document is created', async ({ page }) => {
|
|
// Capture initial list of names
|
|
const before = await page.locator('.document-link').count();
|
|
|
|
// Try to upload unsupported file
|
|
await uploadFile(page, 'unsupported.xyz');
|
|
// Give the UI a moment just in case
|
|
await page.waitForTimeout(500);
|
|
|
|
// Assert no new document entries created
|
|
const after = await page.locator('.document-link').count();
|
|
expect(after).toBe(before);
|
|
// Also ensure no link with that filename exists
|
|
await expect(page.getByRole('link', { name: /unsupported\.xyz/i })).toHaveCount(0);
|
|
});
|
|
});
|