Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaC9HmtHMdRP8dAbN4wAKj
44 lines
5.0 KiB
JavaScript
44 lines
5.0 KiB
JavaScript
// E-book library: uploads are checked by content, each person has their own shelf, and position and notes merge so the newest edit wins.
|
|
const http = require('http'), assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path'), cp = require('child_process');
|
|
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-books-'));
|
|
const books = require('../books'), users = require('../users');
|
|
const fx = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-bookfx-'));
|
|
cp.execFileSync('python3', [path.join(__dirname, 'mkbooks.py'), fx]);
|
|
const epub = fs.readFileSync(path.join(fx, 'test.epub')), pdf = fs.readFileSync(path.join(fx, 'test.pdf')), docx = fs.readFileSync(path.join(__dirname, '..', 'sample.docx'));
|
|
const people = { a: users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), b: users.create({ name: 'Bob', role: 'user', tabs: ['read'], handle: 'Yg' }) };
|
|
const hub = http.createServer((req, res) => users.runAs(req.headers['x-user'] === 'b' ? people.b : people.a, () => books.handle(req, res))).listen(0, async () => {
|
|
const base = 'http://127.0.0.1:' + hub.address().port + '/api/read', call = (p, o = {}) => fetch(base + p, o), up = (name, buf, h) => call('/upload?name=' + encodeURIComponent(name), { method: 'POST', body: buf, headers: h });
|
|
try {
|
|
let r = await up('Rabbit_Tales.epub', epub); assert.equal(r.status, 200); const e = await r.json(); assert.equal(e.ext, 'epub'); assert.equal(e.title, 'Rabbit Tales');
|
|
r = await up('again.epub', epub); assert.equal((await r.json()).duplicate, true, 'the same file is not stored twice');
|
|
r = await up('m.pdf', pdf); const p = await r.json(); assert.equal(p.ext, 'pdf');
|
|
r = await up('d.docx', docx); assert.equal(r.status, 200);
|
|
assert.equal((await up('fake.pdf', Buffer.from('<html>not a pdf</html>'))).status, 400, 'a file must look like what its name says');
|
|
assert.equal((await up('fake.epub', pdf)).status, 400); assert.equal((await up('x.exe', epub)).status, 400); assert.equal((await up('plain.docx', epub)).status, 400, 'an EPUB is not a DOCX');
|
|
assert.equal((await call('/list')).status, 200); assert.equal((await (await call('/list')).json()).length, 3);
|
|
assert.equal((await (await call('/list', { headers: { 'x-user': 'b' } })).json()).length, 0, 'each person has their own shelf');
|
|
assert.equal((await call('/book/' + e.id + '/file', { headers: { 'x-user': 'b' } })).status, 404, "nobody can open another person's book");
|
|
r = await call('/book/' + p.id + '/file', { headers: { range: 'bytes=0-4' } }); assert.equal(r.status, 206); assert.equal(await r.text(), '%PDF-'); assert.equal(r.headers.get('content-range'), `bytes 0-4/${pdf.length}`);
|
|
r = await call('/book/' + p.id + '/file', { headers: { range: 'bytes=-6' } }); assert.equal((await r.text()), '%%EOF\n');
|
|
assert.equal((await call('/book/' + p.id + '/file', { headers: { range: 'bytes=99999-' } })).status, 416);
|
|
r = await call('/book/' + e.id, { method: 'PATCH', body: JSON.stringify({ title: 'Better', author: 'Me' }) }); assert.equal((await r.json()).author, 'Me');
|
|
assert.equal((await call('/book/' + e.id + '/cover', { method: 'PUT', body: 'x', headers: { 'content-type': 'text/html' } })).status, 400, 'covers must be images');
|
|
r = await call('/book/' + e.id + '/cover', { method: 'PUT', body: Buffer.from('GIF89a'), headers: { 'content-type': 'image/gif' } }); assert.equal((await r.json()).cover, true);
|
|
r = await call('/book/' + e.id + '/cover'); assert.equal(r.headers.get('content-type'), 'image/gif');
|
|
const pos = (loc, t) => call('/book/' + e.id + '/pos', { method: 'POST', body: JSON.stringify({ loc, pct: 0.5, t }) }).then(x => x.json());
|
|
await pos('new', 100); assert.equal((await pos('old', 50)).loc, 'new', 'the newest position wins');
|
|
let l = await (await call('/list')).json(); assert.equal(l.find(b => b.id === e.id).loc, 'new'); assert.equal(l.find(b => b.id === e.id).pct, 0.5);
|
|
const note = (arr) => call('/book/' + e.id + '/notes', { method: 'POST', body: JSON.stringify(arr) }).then(x => x.json());
|
|
await note([{ id: 'n1', t: 10, type: 'hl', loc: 'epubcfi(/6/2)', text: 'hello', color: 'green' }]);
|
|
let ns = await note([{ id: 'n2', t: 11, type: 'note', note: 'think', loc: 'x' }, { id: 'n1', t: 5, type: 'hl', text: 'stale edit', loc: 'y' }, { id: '../bad', t: 1 }]);
|
|
assert.deepEqual(ns.map(n => n.id).sort(), ['n1', 'n2'], 'two devices add notes without losing each other'); assert.equal(ns.find(n => n.id === 'n1').text, 'hello', 'an older edit does not win');
|
|
ns = await note([{ id: 'n1', t: 20, del: true, loc: 'y' }]); assert.deepEqual(ns.map(n => n.id), ['n2'], 'deleted notes stay deleted');
|
|
assert.equal((await (await call('/book/' + e.id + '/notes')).json()).length, 1);
|
|
assert.equal((await call('/book/' + e.id, { method: 'DELETE' })).status, 200); assert.equal((await (await call('/list')).json()).length, 2);
|
|
assert.equal((await call('/book/' + e.id + '/file')).status, 404);
|
|
assert.equal((await call('/nope')).status, 404);
|
|
console.log('books test ok');
|
|
} catch (err) { console.error(err); process.exitCode = 1; }
|
|
hub.close();
|
|
});
|