openreader/tests/unit/data-export.vitest.spec.ts
Richard R 849f4a43e8 test(user): expand cleanup and export coverage for shared docs and TTS segment files
Add tests for user data cleanup with shared document ownership and for TTS segment
variant export scenarios, including cases with duplicate audio keys and storage
disabled. Refactor cleanup logic to use document row types and transactional
ownership removal with last-owner checks. Update TTS segment cache clearing to
report deleted segment count by unique entry. Adjust export logic to always
include all TTS segment variants and conditionally export file buckets based on
storage availability. Update migration to use NOT VALID/VALIDATE for new
cascading constraints.
2026-06-06 17:38:30 -06:00

154 lines
5.4 KiB
TypeScript

import { describe, expect, test, vi } from 'vitest';
import type { Archiver } from 'archiver';
import { appendUserExportArchive } from '../../src/lib/server/user/data-export';
describe('user data export archive', () => {
test('includes user metadata and cached TTS audio without credential secrets', async () => {
const entries = new Map<string, unknown>();
const archive = {
append(value: unknown, options: { name: string }) {
entries.set(options.name, value);
return archive;
},
} as unknown as Archiver;
await appendUserExportArchive({
archive,
userId: 'user-1',
exportedAtMs: 123,
profileData: { user: { id: 'user-1', email: 'person@example.com' }, exportedAtMs: 123 },
preferences: { dataJson: '{}' },
readingHistory: [{ documentId: 'doc-1' }],
ttsUsage: [{ charCount: 10 }],
jobEvents: [{ action: 'pdf-layout' }],
documentSettings: [{ documentId: 'doc-1', dataJson: '{}' }],
authSessions: [{ id: 'session-1', ipAddress: null }],
linkedAccounts: [{ id: 'account-1', providerId: 'credential' }],
documents: [],
audiobooks: [],
audiobookChapters: [],
ttsSegmentEntries: [{ segmentEntryId: 'entry-1', documentId: 'doc-1' }],
ttsSegmentVariants: [{
segmentId: 'segment-1',
segmentEntryId: 'entry-1',
audioKey: 'private/audio/key',
audioFormat: 'mp3',
}],
storageEnabled: true,
getDocumentBlobStream: async () => new Uint8Array(),
listAudiobookObjects: async () => [],
getAudiobookObjectStream: async () => new Uint8Array(),
getTtsSegmentAudioStream: async () => new Uint8Array([1, 2, 3]),
});
expect(entries.has('document_settings.json')).toBe(true);
expect(entries.has('auth_sessions.json')).toBe(true);
expect(entries.has('linked_accounts.json')).toBe(true);
expect(entries.has('tts_segment_entries.json')).toBe(true);
expect(entries.has('tts_segment_variants.json')).toBe(true);
expect(entries.has('files/tts_segments/doc-1/segment-1.mp3')).toBe(true);
const manifest = JSON.parse(String(entries.get('export_manifest.json')));
expect(manifest).toMatchObject({
formatVersion: 3,
counts: {
ttsSegmentEntriesMetadata: 1,
ttsSegmentVariantsMetadata: 1,
ttsSegmentFiles: 1,
},
includes: {
credentialSecrets: false,
},
});
});
test('exports every TTS variant even when variants share an audio key', async () => {
const entries = new Map<string, unknown>();
const archive = {
append(value: unknown, options: { name: string }) {
entries.set(options.name, value);
return archive;
},
} as unknown as Archiver;
const getTtsSegmentAudioStream = vi.fn(async () => new Uint8Array([1]));
await appendUserExportArchive({
archive,
userId: 'user-1',
exportedAtMs: 123,
profileData: {},
preferences: null,
readingHistory: [],
ttsUsage: [],
jobEvents: [],
documentSettings: [],
authSessions: [],
linkedAccounts: [],
documents: [],
audiobooks: [],
audiobookChapters: [],
ttsSegmentEntries: [{ segmentEntryId: 'entry-1', documentId: 'doc-1' }],
ttsSegmentVariants: [
{ segmentId: 'segment-1', segmentEntryId: 'entry-1', audioKey: 'shared-key', audioFormat: 'mp3' },
{ segmentId: 'segment-2', segmentEntryId: 'entry-1', audioKey: 'shared-key', audioFormat: 'mp3' },
],
storageEnabled: true,
getDocumentBlobStream: async () => new Uint8Array(),
listAudiobookObjects: async () => [],
getAudiobookObjectStream: async () => new Uint8Array(),
getTtsSegmentAudioStream,
});
expect(entries.has('files/tts_segments/doc-1/segment-1.mp3')).toBe(true);
expect(entries.has('files/tts_segments/doc-1/segment-2.mp3')).toBe(true);
expect(getTtsSegmentAudioStream).toHaveBeenCalledTimes(2);
});
test('reports file buckets as excluded when storage is disabled', async () => {
const entries = new Map<string, unknown>();
const archive = {
append(value: unknown, options: { name: string }) {
entries.set(options.name, value);
return archive;
},
} as unknown as Archiver;
const getDocumentBlobStream = vi.fn(async () => new Uint8Array());
await appendUserExportArchive({
archive,
userId: 'user-1',
exportedAtMs: 123,
profileData: {},
preferences: null,
readingHistory: [],
ttsUsage: [],
jobEvents: [],
documentSettings: [],
authSessions: [],
linkedAccounts: [],
documents: [{ id: 'doc-1', name: 'doc.pdf' }],
audiobooks: [],
audiobookChapters: [],
ttsSegmentEntries: [],
ttsSegmentVariants: [],
storageEnabled: false,
getDocumentBlobStream,
listAudiobookObjects: async () => [],
getAudiobookObjectStream: async () => new Uint8Array(),
getTtsSegmentAudioStream: async () => new Uint8Array(),
});
const manifest = JSON.parse(String(entries.get('export_manifest.json')));
expect(manifest.includes).toMatchObject({
documentFiles: false,
audiobookFiles: false,
ttsSegmentFiles: false,
});
expect(manifest.counts).toMatchObject({
documentFiles: 0,
audiobookFiles: 0,
ttsSegmentFiles: 0,
});
expect(getDocumentBlobStream).not.toHaveBeenCalled();
});
});