// Free audiobooks: LibriVox search and book details, chapter audio is only ever sent to the recording hosts, each person's shelf and listening position. const http = require('http'), assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path'); process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-lv-')); const gif = Buffer.from('R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', 'base64'), seen = []; let port; const fake = http.createServer((req, res) => { const u = new URL(req.url, 'http://x'), p = u.pathname; seen.push(p + u.search); const J = (o, code = 200) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(o)); }; if (p === '/api/feed/audiobooks/') { const book = { id: '55', title: 'Pride & Prejudice', description: '

A novel

', url_librivox: 'https://librivox.org/pp', language: 'English', copyright_year: '1813', totaltimesecs: 600, authors: [{ first_name: 'Jane', last_name: 'Austen' }], coverart_jpg: `http://127.0.0.1:${port}/cover.gif` }; const full = { ...book, sections: [{ section_number: '1', title: 'Chapter 1', listen_url: `http://127.0.0.1:${port}/dl/1.mp3`, playtime: '300', readers: [{ display_name: 'Ann' }] }, { section_number: '2', title: 'Chapter 2', listen_url: 'https://evil.example.com/2.mp3', playtime: '300' }, { section_number: '3', title: 'Chapter 3', listen_url: `http://127.0.0.1:${port}/dl/3.mp3`, playtime: '300' }] }; if (u.searchParams.get('id') === '55') return J({ books: [u.searchParams.get('extended') ? full : book] }); if (u.searchParams.get('title') === 'prejudice' || u.searchParams.get('author') === 'austen') return J({ books: [book] }); return J({ error: 'Audiobooks could not be found' }, 404); } if (p === '/cover.gif') { res.writeHead(200, { 'content-type': 'image/gif' }); return res.end(gif); } res.writeHead(404); res.end(); }).listen(0, () => { port = fake.address().port; process.env.LIBRIVOX_URL = 'http://127.0.0.1:' + port; const lv = require('../librivox'), users = require('../users'); const people = { a: users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), b: users.create({ name: 'Bob', role: 'user', tabs: ['audio'], handle: 'Yg' }) }; const hub = http.createServer((req, res) => users.runAs(req.headers['x-user'] === 'b' ? people.b : people.a, () => lv.handle(req, res))).listen(0, async () => { const base = 'http://127.0.0.1:' + hub.address().port + '/api/ab/free', call = (p, o = {}) => fetch(base + p, { redirect: 'manual', ...o }), j = r => r.json(); try { let r = await call('/search?q=prejudice'); let s = await j(r); assert.equal(s.items.length, 1, 'found by title and by author, listed once'); assert.equal(s.items[0].title, 'Pride & Prejudice'); assert.equal(s.items[0].author, 'Jane Austen'); assert.match(s.items[0].cover, /^\/api\/ab\/free\/cover\/55/); assert.deepEqual((await j(await call('/search?q=zzz'))).items, [], 'nothing found is an empty list, not an error'); assert.ok((await j(await call('/search'))).items.length >= 0, 'no search words lists the catalogue'); r = await call('/book/55'); const b = await j(r); assert.equal(b.chapters.length, 2, 'chapters on other sites are dropped'); assert.ok(!JSON.stringify(b).includes('127.0.0.1:' + port + '/dl'), 'audio addresses are not sent to the browser'); assert.equal(b.chapters[1].title, 'Chapter 3'); assert.equal(b.description, 'A novel'); r = await call('/audio/55/0'); assert.equal(r.status, 302); assert.equal(r.headers.get('location'), `http://127.0.0.1:${port}/dl/1.mp3`); assert.equal((await call('/audio/55/9')).status, 404); assert.equal((await call('/book/abc')).status, 404); r = await call('/cover/55'); assert.equal(r.headers.get('content-type'), 'image/gif'); assert.equal((await call('/cover/55?u=' + encodeURIComponent('http://169.254.169.254/x'))).status, 200, 'an address off the allowed sites is ignored (the book\'s own cover is used)'); assert.ok(!seen.some(x => x.includes('169.254'))); assert.deepEqual(await j(await call('/shelf')), []); await call('/shelf', { method: 'POST', body: JSON.stringify({ id: '55' }) }); s = await j(await call('/shelf')); assert.equal(s[0].title, 'Pride & Prejudice'); assert.deepEqual(await j(await call('/shelf', { headers: { 'x-user': 'b' } })), [], "each person's shelf is their own"); const post = o => call('/progress', { method: 'POST', body: JSON.stringify(o) }).then(j); await post({ 'lv:55': { pos: 400, dur: 600, t: 10 } }); assert.equal((await post({ 'lv:55': { pos: 5, dur: 600, t: 3 } }))['lv:55'].pos, 400, 'the newest position wins'); assert.deepEqual(await j(await call('/progress', { headers: { 'x-user': 'b' } })), {}); await call('/shelf/55', { method: 'DELETE' }); assert.deepEqual(await j(await call('/shelf')), []); console.log('librivox test ok'); } catch (err) { console.error(err); process.exitCode = 1; } hub.close(); fake.close(); }); });