import { describe, it } from './runner.mjs'; import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; const schema = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../src/viewer/schema.json'), 'utf8')); const ajv = new Ajv({ strict: false }); addFormats(ajv); const validate = ajv.compile(schema); describe('Data flow: popup -> storage -> viewer', () => { it('should pass a scraped HLS entry through schema validation', () => { // Simulate popup scraping an HLS stream const scrapedEntry = { url: 'https://stream.example.com/watch', src: 'https://cdn.example.com/live/playlist.m3u8', poster: 'https://cdn.example.com/live/thumb.jpg', tab: 'Live Broadcast', hlsManifest: 'https://cdn.example.com/live/playlist.m3u8', mediaType: 'hls', }; // Validate against schema (as storage layer would) const valid = validate(scrapedEntry); if (!valid) { throw new Error(`Scraped HLS entry fails schema: ${JSON.stringify(validate.errors)}`); } // Verify the viewer would detect it as HLS const isHls = (entry) => typeof entry === 'object' && entry.mediaType === 'hls'; if (!isHls(scrapedEntry)) { throw new Error('Scraped HLS entry not detected as HLS by viewer'); } // Verify the viewer would resolve the manifest const resolveHlsManifest = (entry) => { if (typeof entry === 'object' && entry.hlsManifest) return entry.hlsManifest; return null; }; const manifest = resolveHlsManifest(scrapedEntry); if (manifest !== 'https://cdn.example.com/live/playlist.m3u8') { throw new Error(`Manifest URL wrong: ${manifest}`); } }); it('should pass a scraped direct entry through schema validation', () => { const scrapedEntry = { url: 'https://video.example.com/watch', src: 'https://cdn.example.com/video.mp4', poster: 'https://cdn.example.com/thumb.jpg', tab: 'Video Page', }; const valid = validate(scrapedEntry); if (!valid) { throw new Error(`Scraped direct entry fails schema: ${JSON.stringify(validate.errors)}`); } const isHls = (entry) => typeof entry === 'object' && entry.mediaType === 'hls'; if (isHls(scrapedEntry)) { throw new Error('Direct entry falsely detected as HLS'); } const resolveSrc = (entry) => { if (typeof entry === 'string') return entry; return entry.src || entry.url || ''; }; const src = resolveSrc(scrapedEntry); if (src !== 'https://cdn.example.com/video.mp4') { throw new Error(`Src URL wrong: ${src}`); } }); it('should survive a round-trip: storage serialization -> deserialization', () => { const originalEntry = { url: 'https://example.com/stream', src: 'https://cdn.example.com/stream.m3u8', poster: 'https://example.com/thumb.jpg', tab: 'HLS Stream', hlsManifest: 'https://cdn.example.com/stream.m3u8', mediaType: 'hls', }; // Simulate storage round-trip (JSON serialization) const serialized = JSON.stringify(originalEntry); const deserialized = JSON.parse(serialized); // Validate after deserialization const valid = validate(deserialized); if (!valid) { throw new Error(`Round-tripped entry fails schema: ${JSON.stringify(validate.errors)}`); } // Verify fields are intact if (deserialized.mediaType !== 'hls') { throw new Error('mediaType lost during round-trip'); } if (deserialized.hlsManifest !== 'https://cdn.example.com/stream.m3u8') { throw new Error('hlsManifest lost during round-trip'); } // Verify viewer still detects it correctly const isHls = (entry) => typeof entry === 'object' && entry.mediaType === 'hls'; if (!isHls(deserialized)) { throw new Error('Round-tripped entry not detected as HLS'); } }); it('should handle batch entries from popup -> storage -> viewer pipeline', () => { // Simulate popup storing multiple entries const popupEntries = [ { url: 'https://example.com/1', src: 'https://cdn.example.com/1.mp4', tab: 'Tab 1' }, { url: 'https://example.com/2', src: 'https://cdn.example.com/2.m3u8', hlsManifest: 'https://cdn.example.com/2.m3u8', mediaType: 'hls', tab: 'Tab 2', }, { url: 'https://example.com/3', src: 'https://cdn.example.com/3.webm', mediaType: 'direct', tab: 'Tab 3' }, ]; // Validate all entries for (const [i, entry] of popupEntries.entries()) { const valid = validate(entry); if (!valid) { throw new Error(`Entry ${i} fails schema: ${JSON.stringify(validate.errors)}`); } } // Simulate storage round-trip const serialized = JSON.stringify(popupEntries); const deserialized = JSON.parse(serialized); // Verify viewer can process all entries const isHls = (entry) => typeof entry === 'object' && entry.mediaType === 'hls'; const resolveSrc = (entry) => { if (typeof entry === 'string') return entry; return entry.src || entry.url || ''; }; const resolveHlsManifest = (entry) => { if (typeof entry === 'object' && entry.hlsManifest) return entry.hlsManifest; return null; }; // Entry 0: legacy (no mediaType) if (isHls(deserialized[0])) throw new Error('Entry 0 should not be HLS'); if (resolveSrc(deserialized[0]) !== 'https://cdn.example.com/1.mp4') throw new Error('Entry 0 src wrong'); // Entry 1: HLS if (!isHls(deserialized[1])) throw new Error('Entry 1 should be HLS'); if (resolveHlsManifest(deserialized[1]) !== 'https://cdn.example.com/2.m3u8') throw new Error('Entry 1 manifest wrong'); // Entry 2: explicit direct if (isHls(deserialized[2])) throw new Error('Entry 2 should not be HLS'); if (resolveSrc(deserialized[2]) !== 'https://cdn.example.com/3.webm') throw new Error('Entry 2 src wrong'); }); it('should reject malformed entries that could crash the viewer', () => { const malformedEntries = [ { hlsManifest: 'https://example.com/stream.m3u8' }, // missing url { url: 'https://example.com/video.mp4', mediaType: 'invalid' }, // invalid mediaType { url: 'not-a-url' }, // technically valid per schema (format: uri not enforced by Ajv without custom formats) ]; for (const [i, entry] of malformedEntries.entries()) { const valid = validate(entry); if (!valid && i < 2) { // Expected failures for entries 0 and 1 continue; } } // Entry 0 (missing url) should be rejected const entry0Valid = validate(malformedEntries[0]); if (entry0Valid) { throw new Error('Entry without url should be rejected'); } // Entry 1 (invalid mediaType) should be rejected const entry1Valid = validate(malformedEntries[1]); if (entry1Valid) { throw new Error('Entry with invalid mediaType should be rejected'); } }); it('an HLS entry carrying a scan-time referer passes schema + round-trips', () => { // The referer is bound to the entry at scan time so it survives the // watch-later flow (the source tab may be long closed). Because the schema // sets additionalProperties:false, a `referer` the store carries must be an // explicit property or storage-layer validation would reject the entry. const scrapedEntry = { url: 'https://pageA.example.com/watch', src: 'https://hv-h.phncdn.com/hls/live/master.m3u8', hlsManifest: 'https://hv-h.phncdn.com/hls/live/master.m3u8', mediaType: 'hls', poster: 'https://pageA.example.com/thumb.jpg', tab: 'Referer-protected stream', referer: 'https://pageA.example.com/watch', }; const valid = validate(scrapedEntry); if (!valid) { throw new Error(`Entry with referer fails schema: ${JSON.stringify(validate.errors)}`); } // The referer must survive the storage round-trip intact so the viewer can // send it to the proxy when the user opens a previously-scanned entry. const roundTripped = JSON.parse(JSON.stringify(scrapedEntry)); if (roundTripped.referer !== 'https://pageA.example.com/watch') { throw new Error('referer lost during round-trip'); } if (!validate(roundTripped)) { throw new Error('Round-tripped entry with referer fails schema'); } }); });