group code popup
Sonarqube Scanner / Prepare environment (push) Successful in 6s
Sonarqube Scanner / Build and test (push) Successful in 27s
Sonarqube Scanner / SonarQube analysis (push) Failing after 1m14s
Sonarqube Scanner / Deploy to Server (push) Successful in 10s

This commit is contained in:
Florian Zumpe
2026-10-18 17:24:41 +02:00
parent 31768ae907
commit 0e4cdfbd54
18 changed files with 400 additions and 49 deletions
+60 -2
View File
@@ -1,4 +1,5 @@
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';
@@ -15,7 +16,7 @@ const { createPicture, createAudio } = await import('../src/services/media-servi
const { resolveStoredFile } = await import('../src/services/storage.js');
const { createApiRouter } = await import('../src/routes/api.js');
const { notFoundHandler, errorHandler } = await import('../src/middleware/errors.js');
const { groupRouteGuard, GROUP_SYMBOLS, SESSION_COOKIE } = await import('../src/services/collaboration.js');
const { groupRouteGuard, GROUP_SYMBOLS, SESSION_COOKIE, maintainCollaboration, startCollaborationMaintenance } = await import('../src/services/collaboration.js');
const express = (await import('express')).default;
const gpx = '<gpx><metadata><name>Waldweg</name></metadata><trk><trkseg><trkpt lat="50" lon="10"/><trkpt lat="50.01" lon="10.01"/></trkseg></trk></gpx>';
@@ -58,7 +59,7 @@ test('shared draft clones a published route and protects its contents and owners
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);
assert.equal(created.code.length, 8);
assert.equal(created.code.length, 5);
assert.ok(created.code.every(value => GROUP_SYMBOLS.includes(value)));
assert.match(response.headers.get('set-cookie'), /HttpOnly/);
assert.match(response.headers.get('set-cookie'), /SameSite=Strict/);
@@ -238,3 +239,60 @@ test('different station edits do not conflict through the route revision, and in
await request(guest, lockPath(first.id), 'POST', { replace: fresh.token });
assert.throws(() => req.validatePointWrite(), error => error.status === 423);
});
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;
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);
const joined = (await request(guest, '/groups/join', 'POST', { code: Array(5).fill('🐦'), creator_id: 'b'.repeat(64) })).value;
assert.equal(joined.group.id, created.group.id);
});
test('invitations expire after thirty minutes, wait for active locks and rotate after sixty idle minutes', 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);
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 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;
maintainCollaboration(db);
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);
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);
assert.equal(JSON.parse(fs.readFileSync(file)).updated_at, firstActivity, 'status queries must not count as activity');
await request(host, endpoint, 'POST', { heartbeat: true, token: lock.token }, 423);
await request(guest, endpoint, 'POST', { heartbeat: true, token: lock.token }, 204);
assert.notEqual(JSON.parse(fs.readFileSync(file)).updated_at, firstActivity);
clock = started + 90 * 60_000;
maintainCollaboration(db);
assert.equal(fs.existsSync(file), true, 'recent real activity preserves the lock');
clock = started + 91 * 60_000;
const stop = startCollaborationMaintenance(db); stop();
assert.equal(fs.existsSync(file), false, 'sixty idle minutes remove the actual lock file');
await request(host, endpoint, 'GET', null, 204);
await request(guest, endpoint, 'POST', { heartbeat: true, token: lock.token }, 423);
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) });
await request(host, endpoint.replace(String(poi.id) + '/lock', '99999/lock'), 'GET', null, 404);
});