mirror of
https://github.com/ancsemi/Haven.git
synced 2026-10-27 17:15:24 +00:00
205 lines
9.5 KiB
JavaScript
205 lines
9.5 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Protected messages survive every delete: their author, the admin, a bulk
|
|
* delete, a ban scrub, the clear and delete timers, self-destruct, deleting
|
|
* the channel and deleting the account. Unprotected messages beside them go
|
|
* as before.
|
|
*
|
|
* node --test test/protectedMessages.integration.test.js
|
|
*
|
|
* Takes a little over a minute: the channel timers are swept once a minute.
|
|
*/
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert');
|
|
const path = require('node:path');
|
|
const Database = require('better-sqlite3');
|
|
const { createHarness, wait } = require('./socketServerHarness');
|
|
|
|
const h = createHarness(3428, 'protected');
|
|
const REFUSAL = 'This message is protected. Unprotect it first.';
|
|
|
|
let db;
|
|
test.before(async () => {
|
|
await h.start();
|
|
db = new Database(path.join(h.dataDir, 'haven.db'));
|
|
db.pragma('busy_timeout = 5000');
|
|
});
|
|
test.after(() => {
|
|
if (db) db.close();
|
|
return h.stop();
|
|
});
|
|
|
|
const row = (id) => db.prepare('SELECT id, user_id, content, is_archived, destruct_at FROM messages WHERE id = ?').get(id);
|
|
|
|
async function idOf(content) {
|
|
for (let i = 0; i < 40; i++) {
|
|
const r = db.prepare('SELECT id FROM messages WHERE content = ?').get(content);
|
|
if (r) return r.id;
|
|
await wait(100);
|
|
}
|
|
throw new Error(`message never arrived: ${content}`);
|
|
}
|
|
|
|
async function send(sock, code, content, extra = {}) {
|
|
sock.emit('send-message', { code, content, ...extra });
|
|
return idOf(content);
|
|
}
|
|
|
|
async function protect(sock, code, messageId) {
|
|
sock.emit('enter-channel', { code });
|
|
await wait(100);
|
|
const done = h.next(sock, 'message-archived', (d) => d && d.messageId === messageId);
|
|
sock.emit('archive-message', { messageId });
|
|
assert.ok(await done, `message ${messageId} was protected`);
|
|
}
|
|
|
|
async function makeChannel(sock, name) {
|
|
const created = h.next(sock, 'channels-list', (chs) => Array.isArray(chs) && chs.some((c) => c.name === name));
|
|
sock.emit('create-channel', { name });
|
|
return (await created).find((c) => c.name === name);
|
|
}
|
|
|
|
async function until(check, ms) {
|
|
const end = Date.now() + ms;
|
|
while (Date.now() < end) {
|
|
if (check()) return true;
|
|
await wait(1000);
|
|
}
|
|
return check();
|
|
}
|
|
|
|
test('protected messages survive every delete path', async () => {
|
|
const admin = await h.register('admin');
|
|
const bob = await h.register('bob');
|
|
const carol = await h.register('carol');
|
|
const dave = await h.register('dave');
|
|
const A = await h.connect(admin.token);
|
|
const B = await h.connect(bob.token);
|
|
const C = await h.connect(carol.token);
|
|
const D = await h.connect(dave.token);
|
|
|
|
const ev = await makeChannel(A, 'evidence');
|
|
for (const s of [B, C, D]) s.emit('join-channel', { code: ev.code });
|
|
await wait(400);
|
|
B.emit('enter-channel', { code: ev.code });
|
|
A.emit('enter-channel', { code: ev.code });
|
|
await wait(200);
|
|
|
|
// Author and admin deletes are refused.
|
|
const bobKept = await send(B, ev.code, 'bob: kept');
|
|
const bobGone = await send(B, ev.code, 'bob: goes');
|
|
await protect(A, ev.code, bobKept);
|
|
let refused = h.next(B, 'error-msg', (m) => m === REFUSAL);
|
|
B.emit('delete-message', { messageId: bobKept, channelCode: ev.code });
|
|
assert.ok(await refused, 'the author is told it is protected');
|
|
refused = h.next(A, 'error-msg', (m) => m === REFUSAL);
|
|
A.emit('delete-message', { messageId: bobKept, channelCode: ev.code });
|
|
assert.ok(await refused, 'the admin is told it is protected');
|
|
assert.ok(row(bobKept), 'still there after the author and admin tried');
|
|
// Nor can the author rewrite or blank it.
|
|
refused = h.next(B, 'error-msg', (m) => m === REFUSAL);
|
|
B.emit('edit-message', { messageId: bobKept, channelCode: ev.code, content: 'rewritten' });
|
|
assert.ok(await refused, 'an edit is refused too');
|
|
assert.strictEqual(row(bobKept).content, 'bob: kept', 'the text is unchanged');
|
|
|
|
// A single delete of an unprotected message still works.
|
|
const deleted = h.next(A, 'message-deleted', (d) => d && d.messageId === bobGone);
|
|
B.emit('delete-message', { messageId: bobGone, channelCode: ev.code });
|
|
assert.ok(await deleted);
|
|
assert.strictEqual(row(bobGone), undefined);
|
|
|
|
// Bulk delete keeps the protected one, and a thread head holding a protected reply.
|
|
const bulkGone = await send(A, ev.code, 'admin: bulk goes');
|
|
const head = await send(A, ev.code, 'admin: thread head');
|
|
A.emit('send-thread-message', { parentId: head, content: 'admin: protected reply' });
|
|
const reply = await idOf('admin: protected reply');
|
|
A.emit('send-thread-message', { parentId: head, content: 'admin: plain reply goes' });
|
|
const replyGone = await idOf('admin: plain reply goes');
|
|
await protect(A, ev.code, reply);
|
|
refused = h.next(A, 'error-msg', (m) => /protected reply/.test(m));
|
|
A.emit('delete-message', { messageId: head, channelCode: ev.code });
|
|
assert.ok(await refused, 'a thread head holding a protected reply is refused');
|
|
const bulk = await h.ask(A, 'delete-messages', { code: ev.code, messageIds: [bobKept, bulkGone, head, reply, replyGone] });
|
|
assert.ok(bulk && bulk.success, JSON.stringify(bulk));
|
|
assert.strictEqual(bulk.deleted, 2);
|
|
assert.ok(row(bobKept) && row(head) && row(reply), 'protected, and the head holding one, stay');
|
|
assert.strictEqual(row(bulkGone), undefined);
|
|
assert.strictEqual(row(replyGone), undefined, 'an unprotected reply under the kept head goes');
|
|
|
|
// Ban with scrub.
|
|
C.emit('enter-channel', { code: ev.code });
|
|
await wait(100);
|
|
const carolKept = await send(C, ev.code, 'carol: kept');
|
|
const carolGone = await send(C, ev.code, 'carol: goes');
|
|
await protect(A, ev.code, carolKept);
|
|
A.emit('ban-user', { userId: carol.id, scrubMessages: true });
|
|
assert.ok(await until(() => !row(carolGone), 5000), 'the ban scrub removed the unprotected message');
|
|
assert.ok(row(carolKept), 'the ban scrub left the protected message');
|
|
|
|
// Self-destruct: a protected message's timer stops, and so does a thread
|
|
// head's when its reply is protected.
|
|
const sdKept = await send(A, ev.code, 'admin: self-destruct kept', { destructSeconds: 4 });
|
|
const sdGone = await send(A, ev.code, 'admin: self-destruct goes', { destructSeconds: 1 });
|
|
const sdHead = await send(A, ev.code, 'admin: self-destruct head', { destructSeconds: 4 });
|
|
A.emit('send-thread-message', { parentId: sdHead, content: 'admin: reply under timed head' });
|
|
const sdReply = await idOf('admin: reply under timed head');
|
|
await protect(A, ev.code, sdKept);
|
|
await protect(A, ev.code, sdReply);
|
|
assert.ok(await until(() => !row(sdGone), 6000), 'the unprotected self-destruct message went');
|
|
await wait(4000);
|
|
assert.ok(row(sdKept), 'the protected self-destruct message stayed');
|
|
assert.strictEqual(row(sdKept).destruct_at, null);
|
|
assert.ok(row(sdHead), 'the timed head holding a protected reply stayed');
|
|
assert.strictEqual(row(sdHead).destruct_at, null, 'its timer was turned off');
|
|
|
|
// Deleting the channel is refused while it holds protected messages.
|
|
refused = h.next(A, 'error-msg', (m) => /holds protected messages/.test(m));
|
|
A.emit('delete-channel', { code: ev.code });
|
|
assert.ok(await refused, 'delete-channel is refused');
|
|
assert.ok(db.prepare('SELECT 1 FROM channels WHERE id = ?').get(ev.id), 'the channel is still there');
|
|
|
|
// Account deletion keeps the protected message, unattributed.
|
|
D.emit('enter-channel', { code: ev.code });
|
|
await wait(100);
|
|
const daveKept = await send(D, ev.code, 'dave: kept');
|
|
const daveGone = await send(D, ev.code, 'dave: goes');
|
|
await protect(A, ev.code, daveKept);
|
|
const gone = await h.ask(D, 'self-delete-account', { password: 'access12345', scrubMessages: true });
|
|
assert.ok(gone && gone.success, JSON.stringify(gone));
|
|
assert.strictEqual(row(daveGone), undefined, 'the scrub removed the unprotected message');
|
|
assert.ok(row(daveKept), 'the protected message outlived the account');
|
|
assert.strictEqual(row(daveKept).user_id, null);
|
|
|
|
// Timers: a clear timer and a delete timer, both due now. The sweep runs
|
|
// once a minute.
|
|
const clr = await makeChannel(A, 'cleared');
|
|
const del = await makeChannel(A, 'expiring');
|
|
const clrKept = await send(A, clr.code, 'clear: kept');
|
|
const clrGone = await send(A, clr.code, 'clear: goes');
|
|
const delKept = await send(A, del.code, 'delete: kept');
|
|
const delGone = await send(A, del.code, 'delete: goes');
|
|
await protect(A, clr.code, clrKept);
|
|
await protect(A, del.code, delKept);
|
|
db.prepare("UPDATE channels SET expires_at = datetime('now', '-1 minute'), auto_delete_mode = 'clear', auto_delete_interval_hours = 1 WHERE id = ?").run(clr.id);
|
|
db.prepare("UPDATE channels SET expires_at = datetime('now', '-1 minute'), auto_delete_mode = 'delete' WHERE id = ?").run(del.id);
|
|
assert.ok(await until(() => !row(clrGone) && !row(delGone), 75000), 'both timers ran');
|
|
assert.ok(row(clrKept), 'the clear timer left the protected message');
|
|
assert.ok(row(delKept), 'the delete timer left the protected message');
|
|
const kept = db.prepare('SELECT id, expires_at FROM channels WHERE id = ?').get(del.id);
|
|
assert.ok(kept, 'the delete timer kept the channel');
|
|
assert.strictEqual(kept.expires_at, null, 'and turned its timer off');
|
|
|
|
// Unprotecting is the way back: then the delete goes through.
|
|
A.emit('enter-channel', { code: ev.code });
|
|
await wait(100);
|
|
const unprotected = h.next(A, 'message-unarchived', (d) => d && d.messageId === bobKept);
|
|
A.emit('unarchive-message', { messageId: bobKept });
|
|
assert.ok(await unprotected);
|
|
const finallyGone = h.next(A, 'message-deleted', (d) => d && d.messageId === bobKept);
|
|
A.emit('delete-message', { messageId: bobKept, channelCode: ev.code });
|
|
assert.ok(await finallyGone);
|
|
assert.strictEqual(row(bobKept), undefined);
|
|
});
|