// Audiobooks through the hub: the hub signs in to the player as the person, streams audio with ranges, keeps progress per person, and blocks everything else. const http = require('http'), assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path'); const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-ab-')); process.env.DATA_DIR = dir; process.env.COOKIE_SECRET = 'ab-'.repeat(12); const seen = []; let progress = {}; const player = http.createServer((req, res) => { let b = ''; req.on('data', c => b += c); req.on('end', () => { seen.push({ m: req.method, u: req.url, cookie: req.headers.cookie, range: req.headers.range }); const j = (c, o, h = {}) => { res.writeHead(c, { 'content-type': 'application/json', ...h }); res.end(JSON.stringify(o)); }; if (req.url === '/login') { const o = JSON.parse(b); return o.username === 'nate' && o.password === 'pw' ? j(200, { user: 'nate' }, { 'set-cookie': 'oa_session=tok1; Path=/; HttpOnly' }) : j(401, { error: 'wrong' }); } if (!/oa_session=tok1/.test(req.headers.cookie || '')) return j(401, { error: 'not signed in' }); if (req.url === '/api/books') return j(200, [{ id: 'b1', title: 'Book One', author: 'A. Writer' }]); if (req.url === '/admin/api/books/b1/files') return j(200, ['b1.m4b', 'b1.mp3']); if (req.url === '/api/books/b1/files/b1.mp3') { res.writeHead(206, { 'content-type': 'audio/mpeg', 'content-range': 'bytes 0-3/10', 'content-length': '4' }); return res.end('abcd'); } if (req.url === '/sync/progress' && req.method === 'POST') { progress = { ...progress, ...JSON.parse(b) }; return j(200, progress); } if (req.url === '/sync/progress') return j(200, progress); j(404, { error: 'no' }); }); }).listen(0, async () => { process.env.AUDIOBOOKS_URL = 'http://127.0.0.1:' + player.address().port; const users = require('../users'), abapi = require('../abapi'); const me = { id: 'ab-user', name: 'Nate' }; fs.mkdirSync(path.join(dir, 'users', me.id), { recursive: true }); fs.writeFileSync(path.join(dir, 'users', me.id, 'settings.json'), JSON.stringify({ ab_creds: { u: 'nate', p: 'pw' } })); const hub = http.createServer((req, res) => users.runAs(req.headers['x-nobody'] ? { id: 'nobody', name: 'N' } : me, () => abapi.handle(req, res))).listen(0, async () => { const base = 'http://127.0.0.1:' + hub.address().port, get = (p, o = {}) => fetch(base + '/api/ab' + p, { redirect: 'manual', ...o }); try { let r = await get('/books'); assert.equal(r.status, 200); assert.equal((await r.json())[0].title, 'Book One'); assert.ok(/oa_session=tok1/.test(seen.at(-1).cookie), 'signed in as the person'); r = await get('/books/b1/filelist'); assert.deepEqual(await r.json(), ['b1.m4b', 'b1.mp3']); r = await get('/books/b1/files/b1.mp3', { headers: { range: 'bytes=0-3' } }); assert.equal(r.status, 206); assert.equal(await r.text(), 'abcd'); assert.equal(seen.at(-1).range, 'bytes=0-3'); r = await get('/progress', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ b1: { pos: 12, dur: 100, t: 5 } }) }); assert.equal(r.status, 200); r = await get('/progress'); assert.equal((await r.json()).b1.pos, 12); await new Promise(ok => setTimeout(ok, 100)); assert.equal(progress.b1.pos, 12, 'a personal sign-in on the player is kept in step'); progress.b7 = { pos: 3, dur: 9, t: 99 }; r = await get('/progress'); assert.equal((await r.json()).b7.pos, 3, 'what the player knows is merged in'); r = await get('/progress', { headers: { 'x-nobody': '1' } }); assert.deepEqual(await r.json(), {}, 'another person starts with their own empty positions'); const logins = seen.filter(s => s.u === '/login').length; assert.equal(logins, 1, 'one sign-in is reused'); const n = seen.length; for (const [m, p] of [['POST', '/books'], ['GET', '/admin/users'], ['GET', '/books/b1'], ['GET', '/books/b1/files'], ['DELETE', '/progress'], ['GET', '/books/../admin']]) { r = await get(p, { method: m }); assert.ok([404, 405].includes(r.status), m + ' ' + p + ' -> ' + r.status); } assert.equal(seen.length, n, 'blocked calls never reach the player'); r = await get('/books', { headers: { 'x-nobody': '1' } }); assert.equal(r.status, 503); assert.ok(/sign-in in Settings/.test((await r.json()).error), 'someone with no saved sign-in is told what to do'); console.log('audiobooks ok'); } catch (e) { console.error(e); process.exitCode = 1; } hub.close(); player.close(); }); });