/** * Repository-level tests for D1, R2, import normalization, and burn-after-reading coordination. */ import { beforeEach, describe, expect, it } from 'vitest'; import { env } from 'cloudflare:test'; import { HttpError } from '../src/lib/model'; import { buildCommentBlobKey, buildPasteBlobKey, claimBurnAfterReading, consumeBurnAfterReading, coordinatorClaim, coordinatorConsume, createComment, createPaste, deleteObjectKeys, deletePasteByUser, enqueueGcMessages, ensurePasteReadable, findExpiredPasteIds, generateId, generateSecretToken, getCommentRows, getEnvelope, getPasteRow, importPrivateBinBundle, loadPasteResponse, purgePasteStorage, putEnvelope, releaseExpiredClaims, sha256Hex, } from '../src/lib/repository'; import type { PasteRow, PasteStatus } from '../src/lib/types'; import { resetState, sampleEnvelope } from './support'; // Helper rows let the tests target individual repository branches without full request setup. async function insertPasteRow(overrides: Partial & { id: string }): Promise { const row: PasteRow = { id: overrides.id, schema_version: 2, blob_key: overrides.blob_key ?? buildPasteBlobKey(overrides.id), created_at: overrides.created_at ?? 1_700_000_000_000, expire_at: overrides.expire_at ?? null, status: overrides.status ?? 'active', burn_after_reading: overrides.burn_after_reading ?? 0, discussion_open: overrides.discussion_open ?? 0, formatter: overrides.formatter ?? 'plaintext', has_attachment: overrides.has_attachment ?? 0, delete_token_hash: overrides.delete_token_hash ?? 'delete-hash', claim_token_hash: overrides.claim_token_hash ?? null, claim_expires_at: overrides.claim_expires_at ?? null, comment_count: overrides.comment_count ?? 0, size_bytes: overrides.size_bytes ?? 10, import_source: overrides.import_source ?? null, metadata_json: overrides.metadata_json ?? '{}', }; await env.DB.prepare( `INSERT INTO pastes ( id, schema_version, blob_key, created_at, expire_at, status, burn_after_reading, discussion_open, formatter, has_attachment, delete_token_hash, claim_token_hash, claim_expires_at, comment_count, size_bytes, import_source, metadata_json ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)`, ) .bind( row.id, row.schema_version, row.blob_key, row.created_at, row.expire_at, row.status, row.burn_after_reading, row.discussion_open, row.formatter, row.has_attachment, row.delete_token_hash, row.claim_token_hash, row.claim_expires_at, row.comment_count, row.size_bytes, row.import_source, row.metadata_json, ) .run(); return row; } beforeEach(async () => { await resetState(); }); describe('repository helpers', () => { it('stores and removes envelopes in R2', async () => { const pasteId = 'abcdef1234567890'; const commentId = '1234567890abcdef'; const pasteKey = buildPasteBlobKey(pasteId); const commentKey = buildCommentBlobKey(pasteId, commentId); expect(generateId()).toMatch(/^[a-f0-9]{16}$/); expect(generateSecretToken()).toMatch(/^[A-Za-z0-9_-]+$/); expect(await sha256Hex('hello')).toHaveLength(64); await putEnvelope(env.PASTES_BUCKET, pasteKey, sampleEnvelope()); await putEnvelope(env.PASTES_BUCKET, commentKey, sampleEnvelope({ ct: 'comment' })); expect((await getEnvelope(env.PASTES_BUCKET, pasteKey))?.ct).toBe('ciphertext'); expect((await getEnvelope(env.PASTES_BUCKET, commentKey))?.ct).toBe('comment'); await deleteObjectKeys(env.PASTES_BUCKET, [pasteKey, commentKey]); expect(await getEnvelope(env.PASTES_BUCKET, pasteKey)).toBeNull(); expect(await getEnvelope(env.PASTES_BUCKET, commentKey)).toBeNull(); }); it('imports bundles, sanitizes imported fields, and rejects duplicates or expired pastes', async () => { await expect(importPrivateBinBundle(env, null as never, Date.now())).rejects.toThrow(HttpError); const expiredResult = await importPrivateBinBundle( env, { source: 'privatebin-filesystem', pasteId: 'aaaaaaaaaaaaaaaa', paste: sampleEnvelope({ meta: { created: 1_700_000_000, expire_date: 1 }, id: 'discard-me', }), }, Date.now(), ); expect(expiredResult).toEqual({ id: 'aaaaaaaaaaaaaaaa', importedComments: 0, skipped: true, status: 'expired', }); const imported = await importPrivateBinBundle( env, { source: 'filesystem', pasteId: 'bbbbbbbbbbbbbbbb', paste: sampleEnvelope({ meta: { created: '1700000000', expire_date: '4102444800' }, id: 'discard-me', parentId: 'discard-me-too', }), comments: [ { id: 'cccccccccccccccc', v: 2, adata: ['iv', 'salt', 100000, 256, 128, 'aes', 'gcm', 'none'], ct: 'comment-ciphertext', pasteid: 'bbbbbbbbbbbbbbbb', parentid: 'bbbbbbbbbbbbbbbb', parentId: 'bbbbbbbbbbbbbbbb', meta: { created: '1700000001' }, }, ], }, Date.now(), ); expect(imported.skipped).toBe(false); expect(imported.importedComments).toBe(1); expect(imported.deleteToken).toMatch(/^[A-Za-z0-9_-]+$/); const storedPaste = await getEnvelope(env.PASTES_BUCKET, buildPasteBlobKey('bbbbbbbbbbbbbbbb')); const storedComment = await getEnvelope(env.PASTES_BUCKET, buildCommentBlobKey('bbbbbbbbbbbbbbbb', 'cccccccccccccccc')); expect(storedPaste).not.toHaveProperty('id'); expect(storedPaste).not.toHaveProperty('parentId'); expect(storedComment).not.toHaveProperty('id'); expect(storedComment?.pasteid).toBe('bbbbbbbbbbbbbbbb'); expect(storedComment?.parentid).toBe('bbbbbbbbbbbbbbbb'); await expect( importPrivateBinBundle( env, { source: 'filesystem', pasteId: 'bbbbbbbbbbbbbbbb', paste: sampleEnvelope(), }, Date.now(), ), ).rejects.toThrow('Paste already exists.'); }); it('imports modern comments, falls back imported defaults, and rejects malformed bundles', async () => { const modernPaste = sampleEnvelope(); delete modernPaste.meta; const imported = await importPrivateBinBundle( env, { source: ' ', pasteId: 'dededededededede', paste: modernPaste, comments: [ { id: 'efefefefefefefef', v: 2, adata: [['iv', 'salt', 100000, 256, 128, 'aes', 'gcm', 'none'], 'plaintext', 0, 0], ct: 'modern-comment-ciphertext', }, ], }, 1_700_000_000_000, ); expect(imported.status).toBe('active'); expect((await getPasteRow(env, 'dededededededede'))?.import_source).toBe('privatebin-filesystem'); expect((await getCommentRows(env, 'dededededededede'))[0]?.parent_id).toBe('dededededededede'); expect((await getEnvelope(env.PASTES_BUCKET, buildCommentBlobKey('dededededededede', 'efefefefefefefef')))?.pasteid).toBe( 'dededededededede', ); await expect( importPrivateBinBundle( env, { source: 'filesystem', pasteId: '1010101010101010', paste: { v: 2, adata: [null, 'plaintext', 0, 0], ct: 'invalid-imported-paste', } as never, }, Date.now(), ), ).rejects.toThrow('Invalid imported paste.'); await expect( importPrivateBinBundle( env, { source: 'filesystem', pasteId: '2020202020202020', paste: sampleEnvelope(), comments: [ { id: '3030303030303030', v: 2, adata: [null, 'plaintext', 0, 0], ct: 'invalid-comment', } as never, ], }, Date.now(), ), ).rejects.toThrow('Invalid imported comment.'); await expect( importPrivateBinBundle( env, { source: 'filesystem', pasteId: '4040404040404040', paste: sampleEnvelope(), comments: [ { id: '5050505050505050', v: 2, adata: ['iv', 'salt', 100000, 256, 128, 'aes', 'gcm', 'none'], ct: 'mismatch-comment', pasteid: '6060606060606060', parentid: '4040404040404040', }, ], }, Date.now(), ), ).rejects.toThrow('Imported comment paste identifier does not match.'); }); it('loads paste responses, skips missing comment blobs, and deletes pastes by token', async () => { const created = await createPaste( env, { envelope: sampleEnvelope({ meta: { expire: '1day' }, }), expireKey: '1day', turnstileToken: null, }, 1_700_000_000_000, ); await createComment( env, created.id, { envelope: sampleEnvelope({ ct: 'comment-ciphertext', }), parentId: created.id, turnstileToken: null, }, 1_700_000_001_000, ); const comments = await getCommentRows(env, created.id); await deleteObjectKeys(env.PASTES_BUCKET, [comments[0].blob_key]); const row = await getPasteRow(env, created.id); const response = await loadPasteResponse(env, row!, 1_700_000_000_500); expect(response?.comments).toHaveLength(0); expect((response?.meta as Record).time_to_live).toBeGreaterThan(0); expect(await deletePasteByUser(env, created.id, 'wrong-token')).toBe(false); expect(await deletePasteByUser(env, created.id, created.deleteToken)).toBe(true); expect(await deletePasteByUser(env, created.id, created.deleteToken)).toBe(false); const noExpiry = await insertPasteRow({ id: '9090909090909090', expire_at: null, }); await putEnvelope(env.PASTES_BUCKET, buildPasteBlobKey('9090909090909090'), sampleEnvelope()); expect((await loadPasteResponse(env, noExpiry, Date.now()))?.meta.time_to_live).toBe(0); }); it('stores attachment metadata when the envelope meta block is absent', async () => { const envelope = sampleEnvelope({ attachment: { v: 2, adata: [['iv', 'salt', 100000, 256, 128, 'aes', 'gcm', 'none']], ct: 'attachment-ciphertext', }, meta: undefined, }); delete envelope.meta; const created = await createPaste( env, { envelope, expireKey: 'never', turnstileToken: null, }, Date.now(), ); const row = await getPasteRow(env, created.id); expect(row?.has_attachment).toBe(1); expect(row?.expire_at).toBeNull(); const response = await loadPasteResponse(env, row!, Date.now()); expect(response?.meta.time_to_live).toBe(0); expect(response?.meta.comment_count).toBe(0); }); it('releases claims, finds expired ids, and enqueues gc batches', async () => { await insertPasteRow({ id: '1111111111111111', status: 'claimed', burn_after_reading: 1, claim_expires_at: 10, }); await insertPasteRow({ id: '2222222222222222', status: 'active', expire_at: 20, }); await insertPasteRow({ id: '3333333333333333', status: 'claimed', expire_at: 30, }); expect(await releaseExpiredClaims(env, 100)).toBe(1); expect(await findExpiredPasteIds(env, 100, 1)).toEqual(['2222222222222222']); const queued: unknown[] = []; await enqueueGcMessages( { ...env, GC_QUEUE: { sendBatch: async (messages: unknown[]) => { queued.push(...messages); }, } as Queue, }, [ { pasteId: '2222222222222222', finalStatus: 'expired' }, { pasteId: '3333333333333333', finalStatus: 'deleted' }, ], ); expect(queued).toHaveLength(2); await enqueueGcMessages( { ...env, GC_QUEUE: { sendBatch: async () => { throw new Error('should not run'); }, } as Queue, }, [], ); }); it('handles coordinator and readability edge cases', async () => { expect(await coordinatorClaim(env, 'ffffffffffffffff', 1000)).toEqual({ ok: false, message: 'Document is unavailable.', }); await insertPasteRow({ id: '4444444444444444', burn_after_reading: 0, }); expect(await coordinatorClaim(env, '4444444444444444', 1000)).toEqual({ ok: false, message: 'Document is not burn-after-reading.', }); await insertPasteRow({ id: '5555555555555555', burn_after_reading: 1, status: 'claimed', claim_expires_at: Date.now() + 10_000, }); expect(await coordinatorClaim(env, '5555555555555555', 1000)).toEqual({ ok: false, message: 'Document is already being viewed.', }); await putEnvelope(env.PASTES_BUCKET, buildPasteBlobKey('6666666666666666'), sampleEnvelope()); await insertPasteRow({ id: '6666666666666666', burn_after_reading: 1, }); const claim = await coordinatorClaim(env, '6666666666666666', 5_000); expect(claim.ok).toBe(true); expect(claim.claimToken).toBeTruthy(); expect(await coordinatorConsume(env, '6666666666666666', 'wrong-token')).toEqual({ ok: false, message: 'Invalid claim token.', }); expect(await coordinatorConsume(env, '6666666666666666', claim.claimToken!)).toEqual({ ok: true }); expect(await getEnvelope(env.PASTES_BUCKET, buildPasteBlobKey('6666666666666666'))).toBeNull(); expect(() => ensurePasteReadable(null, Date.now())).toThrow('Document does not exist, has expired or has been deleted.'); const deleted = await insertPasteRow({ id: '7777777777777777', status: 'deleted', }); expect(() => ensurePasteReadable(deleted, Date.now())).toThrow('Document does not exist, has expired or has been deleted.'); const readable = await insertPasteRow({ id: '8888888888888888', status: 'active', expire_at: Date.now() + 10_000, }); expect(ensurePasteReadable(readable, Date.now()).id).toBe('8888888888888888'); expect(() => ensurePasteReadable( { ...readable, expire_at: Date.now() - 1, }, Date.now(), ), ).toThrow('Document does not exist, has expired or has been deleted.'); expect(await consumeBurnAfterReading(env, 'ffffffffffffffff', 'token')).toEqual({ ok: false, message: 'Document cannot be consumed.', }); }); it('uses the durable object stub for claim and consume helpers', async () => { await putEnvelope(env.PASTES_BUCKET, buildPasteBlobKey('9999999999999999'), sampleEnvelope()); await insertPasteRow({ id: '9999999999999999', burn_after_reading: 1, }); const claim = await claimBurnAfterReading(env, '9999999999999999'); expect(claim.ok).toBe(true); expect(claim.claimToken).toBeTruthy(); const consume = await consumeBurnAfterReading(env, '9999999999999999', claim.claimToken!); expect(consume).toEqual({ ok: true }); const stub = env.PASTE_COORDINATOR.get(env.PASTE_COORDINATOR.idFromName('aaaaaaaaaaaaaaaa')); const missingId = await stub.fetch('https://coordinator.local/claim', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({}), }); expect(missingId.status).toBe(400); expect((await missingId.json()).ok).toBe(false); const missingClaimToken = await stub.fetch('https://coordinator.local/consume', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ pasteId: 'aaaaaaaaaaaaaaaa' }), }); expect(missingClaimToken.status).toBe(400); expect((await missingClaimToken.json()).ok).toBe(false); const missingRoute = await stub.fetch('https://coordinator.local/unknown', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ pasteId: 'aaaaaaaaaaaaaaaa' }), }); expect(missingRoute.status).toBe(404); expect((await missingRoute.json()).ok).toBe(false); }); it('purges storage and marks final statuses', async () => { await putEnvelope(env.PASTES_BUCKET, buildPasteBlobKey('abababababababab'), sampleEnvelope()); await putEnvelope(env.PASTES_BUCKET, buildCommentBlobKey('abababababababab', 'cdcdcdcdcdcdcdcd'), sampleEnvelope({ ct: 'comment' })); await insertPasteRow({ id: 'abababababababab', comment_count: 1, }); await env.DB.prepare( `INSERT INTO comments (id, paste_id, parent_id, blob_key, created_at, size_bytes) VALUES (?1, ?2, ?3, ?4, ?5, ?6)`, ) .bind( 'cdcdcdcdcdcdcdcd', 'abababababababab', 'abababababababab', buildCommentBlobKey('abababababababab', 'cdcdcdcdcdcdcdcd'), Date.now(), 10, ) .run(); await purgePasteStorage(env, 'abababababababab', 'expired'); expect(await getEnvelope(env.PASTES_BUCKET, buildPasteBlobKey('abababababababab'))).toBeNull(); expect(await getCommentRows(env, 'abababababababab')).toHaveLength(0); expect((await getPasteRow(env, 'abababababababab'))?.status).toBe('expired'); }); });