diff options
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/composer-media-test.js | 21 | ||||
| -rw-r--r-- | tools/media-servers-test.js | 52 |
2 files changed, 71 insertions, 2 deletions
diff --git a/tools/composer-media-test.js b/tools/composer-media-test.js index 512b8bd..94b343a 100644 --- a/tools/composer-media-test.js +++ b/tools/composer-media-test.js @@ -18,7 +18,7 @@ const page = await browser.newPage(); const uploads = []; await page.setRequestInterception(true); // every public media host the upload may try first (nothing leaves the machine) -const HOSTS = /blossom\.ditto\.pub|blossom\.primal\.net|nostr\.download|nostr\.build/; +const HOSTS = /media\.example|blossom\.ditto\.pub|blossom\.primal\.net|nostr\.download|nostr\.build/; page.on('request', (req) => { const u = req.url(); if (HOSTS.test(u) && req.method() === 'PUT') { @@ -46,6 +46,23 @@ try { const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' '); await click('skip verification'); await waitText('receive', 20000); + await page.evaluate(() => document.querySelector('button[aria-label="Settings"]').click()); + await sleep(300); + await page.evaluate(() => [...document.querySelectorAll('.settings-tile')].find(e => e.textContent.includes('Nostr')).click()); + await page.waitForSelector('[data-key="media-servers"] textarea'); + const setServers = async (value) => page.evaluate(value => { + const card = document.querySelector('[data-key="media-servers"]'); + const input = card.querySelector('textarea'); input.value = value; + input.dispatchEvent(new Event('input', {bubbles:true})); + card.querySelector('button').click(); + }, value); + await setServers('https://media.example/'); + check('settings save a normalized custom server', await page.evaluate(() => localStorage.getItem('btc-wallet-media-servers') === '["https://media.example"]')); + await setServers('https://media.example/path'); + check('invalid settings preserve the previous server', await page.evaluate(() => localStorage.getItem('btc-wallet-media-servers') === '["https://media.example"]')); + await page.evaluate(() => document.querySelectorAll('[data-key="media-servers"] button')[1].click()); + check('Use default clears the override', await page.evaluate(() => localStorage.getItem('btc-wallet-media-servers') === null)); + await setServers('https://media.example'); const base = cacheKeyFor(mn + '\n'); const A = 'a'.repeat(63) + '9'; await page.evaluate(([k, a]) => { @@ -91,7 +108,7 @@ try { document.querySelector('.thread-reply-input').dispatchEvent(new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true })); }); const landed = await waitFor(() => /nostr\.download\/f{64}\.gif/.test(document.querySelector('.thread-reply-input')?.value || ''), 15000); - check('a pasted GIF is uploaded and its URL lands in the reply', landed && uploads.length === 1, + check('a pasted GIF is uploaded and its URL lands in the reply', landed && uploads.length === 1 && uploads[0] === 'https://media.example/upload', JSON.stringify(await page.evaluate(() => document.querySelector('.thread-reply-input')?.value)) + ' uploads=' + uploads.length); check('...and the preview shows it', await waitFor(() => !!document.querySelector('.draft-preview img, .draft-preview .note-img, .draft-preview a'), 5000)); // the upload re-rendered the page: the field must still be editable diff --git a/tools/media-servers-test.js b/tools/media-servers-test.js new file mode 100644 index 0000000..c5cad2f --- /dev/null +++ b/tools/media-servers-test.js @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict'; +import { parseMediaServers, customMediaServers, mediaServers, saveMediaServers, resetMediaServers } from '../src/media-servers.js'; +import { uploadPublicMedia, uploadStream, PUBLIC_MEDIA_SERVERS } from '../src/media-upload.js'; +const stored = new Map(); +globalThis.localStorage = { getItem: k => stored.get(k) ?? null, setItem: (k, v) => stored.set(k, v), removeItem: k => stored.delete(k) }; +assert.equal(customMediaServers(), null); +assert.deepEqual(mediaServers(PUBLIC_MEDIA_SERVERS), PUBLIC_MEDIA_SERVERS); +assert.deepEqual(saveMediaServers('https://FIRST.example/\nhttps://first.example https://second.example/'), ['https://first.example', 'https://second.example']); +for (const bad of ['', 'http://host.test', 'https://host.test/path', 'https://user:[email protected]', 'https://host.test?x=1', 'https://host.test#x', 'https://good.test nope', Array.from({length:7}, (_,i)=>`https://host${i}.test`).join('\n')]) { + assert.throws(() => saveMediaServers(bad)); + assert.equal(customMediaServers()[0], 'https://first.example', 'invalid input must not overwrite saved servers'); +} +const calls = []; +const fetcher = async (url, opts) => { + calls.push(url); + if (url.startsWith('https://first.')) return new Response('', {status:503}); + if (opts.method === 'HEAD') return new Response(''); + return Response.json({url: 'https://second.example/' + 'a'.repeat(64)}); +}; +const sign = async e => e; +await uploadPublicMedia(new File(['picture'], 'photo.png', {type:'image/png'}), sign, {fetcher}); +assert(calls.some(u => u.startsWith('https://second.'))); +assert(calls.every(u => /^https:\/\/(first|second)\.example\/upload$/.test(u))); +calls.length = 0; +await uploadStream({master:'master.m3u8', files:new Map([ + ['video.mp4', new File(['video'], 'video.mp4', {type:'video/mp4'})], + ['master.m3u8', new File(['#EXTM3U\nvideo.mp4'], 'master.m3u8', {type:'application/vnd.apple.mpegurl'})], +])}, sign, {fetcher}); +assert(calls.every(u => /^https:\/\/(first|second)\.example\/upload$/.test(u))); +await assert.rejects(uploadPublicMedia(new File(['x'], 'x'), sign, {fetcher: async url => { + assert(calls.includes(url), 'must not fall back to public hosts'); + return new Response('', {status:503}); +}})); +// Exercise the attachment uploader used by DMs, communities and White Noise. +const source = await Bun.file('src/features/messages.js').text(); +const start = source.indexOf(' async function uploadEncrypted('); +const end = source.indexOf(' const imetaTag', start); +const encryptedCalls = []; +const uploadEncrypted = new Function('mediaServers', 'MEDIA_SERVERS', 'fetch', 't', + source.slice(start, end) + '\nreturn uploadEncrypted;')( + mediaServers, ['https://public.example'], async (url, init) => { + encryptedCalls.push({url, init}); + return Response.json({url: url.replace('/upload', '/cipher')}); + }, key => key); +const cipher = new Uint8Array([9, 8, 7]); +const urls = await uploadEncrypted({pubkey:'a'.repeat(64), signer:{signEvent: sign}}, {cipher, x:'b'.repeat(64)}); +assert.deepEqual(urls, ['https://first.example/cipher', 'https://second.example/cipher']); +assert(encryptedCalls.every(c => c.init.body === cipher && c.init.headers['content-type'] === 'application/octet-stream')); +resetMediaServers(); +assert.equal(customMediaServers(), null); +assert.deepEqual(mediaServers(PUBLIC_MEDIA_SERVERS), PUBLIC_MEDIA_SERVERS); +console.log('✓ Media server validation, persistence, custom-only file/HLS/encrypted attachment uploads, failover, and reset'); |
