build-container / build (push) Successful in 9m2s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
49 lines
4.5 KiB
JavaScript
49 lines
4.5 KiB
JavaScript
// 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();
|
|
});
|
|
});
|