fix tests and change invitation mechanics
Sonarqube Scanner / Prepare environment (push) Successful in 8s
Sonarqube Scanner / Build and test (push) Successful in 24s
Sonarqube Scanner / SonarQube analysis (push) Failing after 1m2s
Sonarqube Scanner / Deploy to Server (push) Successful in 6s

This commit is contained in:
Florian Zumpe
2026-10-19 17:21:50 +02:00
parent a66a795407
commit cacc63be66
13 changed files with 349 additions and 146 deletions
+62 -23
View File
@@ -1,5 +1,4 @@
import assert from 'node:assert/strict';
import nodeCrypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
@@ -58,7 +57,7 @@ async function fixture(t) {
test('shared draft clones a published route and protects its contents and ownership', async t => {
const { db, source, request } = await fixture(t);
const host = {}, guest = {}, outsider = {};
const { value: created, response } = await request(host, '/groups', 'POST', { routeId: source.id, creator_id: 'a'.repeat(64) }, 201);
const { value: created, response } = await request(host, '/groups', 'POST', { code: Array(5).fill('🐦'), routeId: source.id, creator_id: 'a'.repeat(64) }, 201);
assert.equal(created.code.length, 5);
assert.ok(created.code.every(value => GROUP_SYMBOLS.includes(value)));
assert.match(response.headers.get('set-cookie'), /HttpOnly/);
@@ -98,14 +97,14 @@ test('shared draft clones a published route and protects its contents and owners
await request(guest, `/routes/${route.id}`, 'DELETE', null, 403, { 'If-Match': '1' });
await request(guest, `/routes/0${route.id}`, 'DELETE', null, 403, { 'If-Match': '1' });
await request(guest, `/routes/${route.id}/restore`, 'POST', {}, 403, { 'If-Match': '1' });
const renewed = (await request(host, `/groups/${created.group.id}/code`, 'POST', {})).value;
const renewed = (await request(host, `/groups/${created.group.id}/code`, 'POST', { code: Array(5).fill('🪨') })).value;
await request(outsider, '/groups/join', 'POST', { code: created.code, creator_id: 'b'.repeat(64) }, 404);
await request(outsider, '/groups/join', 'POST', { code: renewed.code, creator_id: 'b'.repeat(64) });
await request(host, `/groups/${created.group.id}`, 'DELETE');
await request(guest, `/routes/${route.id}`, 'GET', null, 403);
assert.equal((await request(guest, '/groups')).value.groups.length, 0);
await request(host, `/routes/${route.id}`);
const reopened = (await request(host, `/groups/${created.group.id}/code`, 'POST', {})).value;
const reopened = (await request(host, `/groups/${created.group.id}/code`, 'POST', { code: Array(5).fill('🪨') })).value;
assert.equal(reopened.group.active, true);
await request(guest, '/groups/join', 'POST', { code: reopened.code, creator_id: 'a'.repeat(64) });
assert.equal((await request(guest, '/groups')).value.groups.length, 1);
@@ -115,10 +114,10 @@ test('invitation expiry, request origin, invalid IDs and expired sessions are en
const { db, source, request } = await fixture(t);
const host = {}, guest = {};
await request(host, '/groups', 'POST', { routeId: source.id }, 400);
await request(host, '/groups', 'POST', { routeId: source.id, creator_id: 'a'.repeat(64) }, 403, { Origin: 'https://other.test' });
await request(host, '/groups', 'POST', { routeId: source.id, creator_id: 'a'.repeat(64) }, 403, { Origin: 'null' });
await request(host, '/groups', 'POST', { routeId: 900, creator_id: 'a'.repeat(64) }, 404);
const created = (await request(host, '/groups', 'POST', { routeId: source.id, creator_id: 'a'.repeat(64) }, 201)).value;
await request(host, '/groups', 'POST', { code: Array(5).fill('🐦'), routeId: source.id, creator_id: 'a'.repeat(64) }, 403, { Origin: 'https://other.test' });
await request(host, '/groups', 'POST', { code: Array(5).fill('🐦'), routeId: source.id, creator_id: 'a'.repeat(64) }, 403, { Origin: 'null' });
await request(host, '/groups', 'POST', { code: Array(5).fill('🐦'), routeId: 900, creator_id: 'a'.repeat(64) }, 404);
const created = (await request(host, '/groups', 'POST', { code: Array(5).fill('🐦'), routeId: source.id, creator_id: 'a'.repeat(64) }, 201)).value;
await request(host, `/groups/${'a'.repeat(32)}/code`, 'POST', {}, 404);
db.prepare('UPDATE collaboration_groups SET code_expires_at = 0 WHERE id = ?').run(created.group.id);
await request(guest, '/groups/join', 'POST', { code: created.code, creator_id: 'b'.repeat(64) }, 404);
@@ -136,7 +135,7 @@ test('invitation expiry, request origin, invalid IDs and expired sessions are en
test('overlapping writes are locked until completion and revision changes once', async t => {
const { db, source, request } = await fixture(t);
const host = {};
const created = (await request(host, '/groups', 'POST', { routeId: source.id, creator_id: 'a'.repeat(64) }, 201)).value;
const created = (await request(host, '/groups', 'POST', { code: Array(5).fill('🐦'), routeId: source.id, creator_id: 'a'.repeat(64) }, 201)).value;
const guard = groupRouteGuard(db);
const req = { method: 'PUT', path: `/routes/${created.group.route.id}`, headers: { cookie: host.cookie, 'if-match': '0' } };
const res = new EventEmitter(); res.setHeader = () => {}; res.statusCode = 200;
@@ -153,7 +152,7 @@ test('group copying rolls back a failed media copy and rate limits invalid invit
const { db, source, request } = await fixture(t);
const image = db.prepare('SELECT path FROM poi_images LIMIT 1').get();
fs.rmSync(resolveStoredFile(image.path));
await request({}, '/groups', 'POST', { routeId: source.id, creator_id: 'a'.repeat(64) }, 404);
await request({}, '/groups', 'POST', { code: Array(5).fill('🐦'), routeId: source.id, creator_id: 'a'.repeat(64) }, 404);
assert.equal(db.prepare('SELECT COUNT(*) AS total FROM routes').get().total, 1);
assert.equal(db.prepare('SELECT COUNT(*) AS total FROM collaboration_groups').get().total, 0);
const client = {};
@@ -166,7 +165,7 @@ test('group copying rolls back a failed media copy and rate limits invalid invit
test('point lock files bind the device and session, require confirmed takeover and reject stale content', async t => {
const { db, source, request } = await fixture(t);
const host = {}, guest = {}, outsider = {};
const created = (await request(host, '/groups', 'POST', { routeId: source.id, creator_id: 'a'.repeat(64) }, 201)).value;
const created = (await request(host, '/groups', 'POST', { code: Array(5).fill('🐦'), routeId: source.id, creator_id: 'a'.repeat(64) }, 201)).value;
await request(guest, '/groups/join', 'POST', { code: created.code, creator_id: 'b'.repeat(64) });
const route = created.group.route, poi = route.pois[0];
const endpoint = `/groups/routes/${route.id}/pois/${poi.id}/lock`;
@@ -215,14 +214,14 @@ test('point lock files bind the device and session, require confirmed takeover a
test('different station edits do not conflict through the route revision, and in-flight writes prevent takeover', async t => {
const { db, source, request } = await fixture(t);
const host = {}, guest = {};
const created = (await request(host, '/groups', 'POST', { routeId: source.id, creator_id: 'a'.repeat(64) }, 201)).value;
const created = (await request(host, '/groups', 'POST', { code: Array(5).fill('🐦'), routeId: source.id, creator_id: 'a'.repeat(64) }, 201)).value;
await request(guest, '/groups/join', 'POST', { code: created.code, creator_id: 'b'.repeat(64) });
const route = created.group.route;
const second = createPoi(db, route.id, { title: 'Second stop' });
const lockPath = poiId => `/groups/routes/${route.id}/pois/${poiId}/lock`;
const first = route.pois[0];
const a = (await request(host, lockPath(first.id), 'POST', {})).value;
const b = (await request(guest, lockPath(second.id), 'POST', {})).value;
const a = (await request(host, lockPath(first.id), 'POST', { code: Array(5).fill('🪨') })).value;
const b = (await request(guest, lockPath(second.id), 'POST', { code: Array(5).fill('🪨') })).value;
await request(host, `/routes/${route.id}/pois/${first.id}`, 'PUT', { title: 'A' }, 200,
{ 'X-Poi-Lock': a.token, 'If-Poi-Match': a.version, 'If-Match': '0' });
await request(guest, `/routes/${route.id}/pois/${second.id}`, 'PUT', { title: 'B' }, 200,
@@ -243,9 +242,8 @@ test('different station edits do not conflict through the route revision, and in
test('five-symbol invitations allow repeated motifs and reject other code lengths', async t => {
const { source, request } = await fixture(t);
t.mock.method(nodeCrypto, 'randomInt', () => 5);
const host = {}, guest = {};
const created = (await request(host, '/groups', 'POST', { routeId: source.id, creator_id: 'a'.repeat(64) }, 201)).value;
const created = (await request(host, '/groups', 'POST', { code: Array(5).fill('🐦'), routeId: source.id, creator_id: 'a'.repeat(64) }, 201)).value;
assert.deepEqual(created.code, Array(5).fill('🐦'));
await request(guest, '/groups/join', 'POST', { code: Array(4).fill('🐦'), creator_id: 'b'.repeat(64) }, 404);
await request(guest, '/groups/join', 'POST', { code: Array(6).fill('🐦'), creator_id: 'b'.repeat(64) }, 404);
@@ -254,18 +252,18 @@ test('five-symbol invitations allow repeated motifs and reject other code length
});
test('invitations expire after thirty minutes, wait for active locks and rotate after sixty idle minutes', async t => {
test('invitations extend on editing, expire after sixty idle minutes and never rotate', async t => {
const { db, source, request } = await fixture(t);
const host = {}, guest = {};
const started = Date.now();
let clock = started;
t.mock.method(Date, 'now', () => clock);
const created = (await request(host, '/groups', 'POST', { routeId: source.id, creator_id: 'a'.repeat(64) }, 201)).value;
assert.equal(created.expiresAt, Math.floor(started / 1000) + 1800);
const created = (await request(host, '/groups', 'POST', { code: Array(5).fill('🐦'), routeId: source.id, creator_id: 'a'.repeat(64) }, 201)).value;
assert.equal(created.expiresAt, Math.floor(started / 1000) + 3600);
await request(guest, '/groups/join', 'POST', { code: created.code, creator_id: 'b'.repeat(64) });
const group = created.group, route = group.route, poi = route.pois[0];
const endpoint = `/groups/routes/${route.id}/pois/${poi.id}/lock`;
const lock = (await request(guest, endpoint, 'POST', {})).value;
const lock = (await request(guest, endpoint, 'POST', { code: Array(5).fill('🪨') })).value;
const file = path.join(config.storageDir, 'active', 'routes', String(route.id), 'locks', `${poi.id}.json`);
const firstActivity = JSON.parse(fs.readFileSync(file)).updated_at;
clock += 31 * 60_000;
@@ -273,7 +271,8 @@ test('invitations expire after thirty minutes, wait for active locks and rotate
const held = (await request(host, `/groups/${group.id}/invitation`)).value;
assert.equal(held.busy, true);
assert.equal(held.expiresAt, created.expiresAt);
await request({}, '/groups/join', 'POST', { code: created.code, creator_id: 'c'.repeat(64) }, 404);
assert.deepEqual(held.code, created.code);
assert.equal(held.expired, false);
await request(guest, `/groups/${group.id}/invitation`, 'GET', null, 403);
await request(host, `/groups/${group.id}/code`, 'POST', {}, 423);
await request(host, endpoint, 'GET', null, 423);
@@ -292,7 +291,47 @@ test('invitations expire after thirty minutes, wait for active locks and rotate
const renewed = (await request(host, `/groups/${group.id}/invitation`)).value;
assert.equal(renewed.busy, false);
assert.equal(renewed.code.length, 5);
assert.equal(renewed.expiresAt, Math.floor(clock / 1000) + 1800);
await request({}, '/groups/join', 'POST', { code: renewed.code, creator_id: 'c'.repeat(64) });
assert.equal(renewed.expiresAt, Math.floor(started / 1000) + 91 * 60);
assert.equal(renewed.expired, true);
assert.deepEqual(renewed.code, created.code);
await request({}, '/groups/join', 'POST', { code: renewed.code, creator_id: 'c'.repeat(64) }, 404);
await request(host, endpoint.replace(String(poi.id) + '/lock', '99999/lock'), 'GET', null, 404);
});
test('chosen codes persist across connections, collisions fail without copies and passive queries never extend expiry', async t => {
const { db, source, request } = await fixture(t);
const host = {}, guest = {};
const code = ['🐦', '🪨', '🐦', '🎃', '🍄'];
await request(host, '/groups', 'POST', { routeId: source.id, creator_id: 'a'.repeat(64) }, 400);
const created = (await request(host, '/groups', 'POST', { routeId: source.id, creator_id: 'a'.repeat(64), code }, 201)).value;
assert.deepEqual(created.code, code);
const second = openDatabase(db.location());
try { assert.deepEqual(JSON.parse(second.prepare('SELECT code FROM collaboration_invitations WHERE group_id = ?').get(created.group.id).code), code); }
finally { second.close(); }
const total = db.prepare('SELECT COUNT(*) AS total FROM routes').get().total;
const collision = await request(host, '/groups', 'POST', { routeId: source.id, creator_id: 'a'.repeat(64), code }, 409);
assert.equal(collision.value.details.code, 'code-taken');
assert.equal(db.prepare('SELECT COUNT(*) AS total FROM routes').get().total, total);
await request(host, `/groups/${created.group.id}/code`, 'POST', {}, 400);
let clock = Date.now();
t.mock.method(Date, 'now', () => clock);
clock += 5 * 60_000;
const status = (await request(host, `/groups/${created.group.id}/invitation`)).value;
assert.deepEqual(status.code, code);
assert.equal(status.expiresAt, created.expiresAt);
await request(guest, '/groups/join', 'POST', { code, creator_id: 'b'.repeat(64) });
const expiry = () => db.prepare('SELECT code_expires_at FROM collaboration_groups WHERE id = ?').get(created.group.id).code_expires_at;
assert.equal(expiry(), Math.floor(clock / 1000) + 3600);
clock += 5 * 60_000;
await request(host, '/groups');
await request(guest, `/routes/${created.group.route.id}`);
assert.equal(expiry(), Math.floor(clock / 1000) + 3300);
await request(guest, `/groups/${created.group.id}/edit`, 'POST', {});
assert.equal(expiry(), Math.floor(clock / 1000) + 3600);
await request({}, `/groups/${created.group.id}/edit`, 'POST', {}, 401);
clock += 5 * 60_000;
await request(guest, `/routes/${created.group.route.id}`, 'PUT', { name: 'Changed again' }, 200, { 'If-Match': '0' });
assert.equal(expiry(), Math.floor(clock / 1000) + 3600);
await request(host, `/groups/${created.group.id}`, 'DELETE');
assert.equal(db.prepare('SELECT COUNT(*) AS total FROM collaboration_invitations').get().total, 0);
});