import { describe, expect, it } from 'vitest'; import { base64ToBytes } from '@/utils/encoding/base64ToBytes'; import { buildZkViewerPath } from '@/utils/share/buildZkViewerPath'; import { decryptPasswordProtectedZkUpload } from '@/utils/upload/decryptPasswordProtectedZkUpload'; import { decryptZkUpload } from '@/utils/upload/decryptZkUpload'; import { prepareZkUploadRequest } from '@/utils/upload/prepareZkUploadRequest'; describe('prepareZkUploadRequest', () => { const options = { expiresAt: null, maxViews: null, passwordProtected: false as const }; it('returns fragmentKey separately from the POST request', async () => { const { request, fragmentKey } = await prepareZkUploadRequest(new Uint8Array([9]), options); expect(fragmentKey).toMatch(/^[A-Za-z0-9_-]+$/); expect(request).toEqual({ ...options, iv: request.iv, ciphertext: request.ciphertext, passwordProtected: false }); expect(request).not.toHaveProperty('fragmentKey'); expect(Object.keys(request).sort()).toEqual([ 'ciphertext', 'expiresAt', 'iv', 'maxViews', 'passwordProtected' ]); }); it('roundtrips plaintext using request fields and URL fragment key', async () => { const plaintext = new TextEncoder().encode('zk-payload'); const { request, fragmentKey } = await prepareZkUploadRequest(plaintext, options); const recovered = await decryptZkUpload( base64ToBytes(request.ciphertext), base64ToBytes(request.iv), fragmentKey ); expect(recovered).toEqual(plaintext); }); it('wires fragmentKey into the viewer URL hash for decryption', async () => { const plaintext = new TextEncoder().encode('share-link'); const uploadId = '7f3e2a1b-0000-4000-8000-000000000001'; const { request, fragmentKey } = await prepareZkUploadRequest(plaintext, options); const path = buildZkViewerPath(uploadId, fragmentKey); const [pathWithoutHash, hashKey] = path.split('#'); expect(pathWithoutHash).toBe(`/v/${uploadId}`); expect(hashKey).toBe(fragmentKey); const recovered = await decryptZkUpload( base64ToBytes(request.ciphertext), base64ToBytes(request.iv), hashKey ); expect(recovered).toEqual(plaintext); }); describe('password protected', () => { const passwordOptions = { expiresAt: null, maxViews: null, passwordProtected: true as const, password: 'batch-secret' }; it('includes wrap fields and omits password from the POST body', async () => { const { request, fragmentKey } = await prepareZkUploadRequest(new Uint8Array([1]), passwordOptions); expect(request.passwordProtected).toBe(true); expect(request.wrappedDek).toMatch(/^[A-Za-z0-9+/]+=*$/); expect(request.wrapIv).toMatch(/^[A-Za-z0-9+/]+=*$/); expect(request.salt).toMatch(/^[A-Za-z0-9+/]+=*$/); expect(request).not.toHaveProperty('password'); expect(fragmentKey).toMatch(/^[A-Za-z0-9_-]+$/); }); it('does not decrypt with URL fragment alone', async () => { const plaintext = new TextEncoder().encode('protected'); const { request, fragmentKey } = await prepareZkUploadRequest(plaintext, passwordOptions); await expect( decryptZkUpload( base64ToBytes(request.ciphertext), base64ToBytes(request.iv), fragmentKey ) ).rejects.toThrow(); }); it('roundtrips with fragment key and password', async () => { const plaintext = new TextEncoder().encode('protected-payload'); const { request, fragmentKey } = await prepareZkUploadRequest(plaintext, passwordOptions); const recovered = await decryptPasswordProtectedZkUpload( base64ToBytes(request.ciphertext), base64ToBytes(request.iv), fragmentKey, passwordOptions.password, base64ToBytes(request.wrappedDek!), base64ToBytes(request.wrapIv!), base64ToBytes(request.salt!) ); expect(recovered).toEqual(plaintext); }); }); });