Update über den Git-Server, Tests, Repo ohne node_modules und Sicherungen

- Admin → Update: neuere Versionen (Tags) vom Git-Server anzeigen und per
  Knopf einspielen; ausgeführt vom Dienst auf dem Rechner
  (Docker/update-einrichten.sh, Docker/update-ausfuehren.sh), nur vorwärts,
  Fehler bei ausbleibendem Neustart, abgebrochene Läufe sperren nicht
- Tests (npm test): Zusammenführen paralleler Änderungen, Rechteprüfung
  beim Speichern; SESSIONS_DIR lenkt den Datenordner für Testserver um
- node_modules, Sicherungen und Uploads aus dem Repo genommen
- cors und requireRole() entfernt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
erikandClaude Opus 5.5 committed 2026-11-10 11:10:56 +01:00
1 parent e12936444d
commit fd44db2213
2357 files changed
+588 -514948

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
// Test: Rechteprüfung beim Speichern (fehlendesBearbeitungsrecht() in
// server.js). Startet einen echten Server mit leerer Datenbank in einem
// Wegwerf-Ordner (SESSIONS_DIR/DB_PATH), legt eine Rolle nur mit „Kommentar
// und Fortschritt“ an und prüft über POST /save-projects, was ein Konto mit
// dieser Rolle ändern darf. Aufruf: npm test
const test = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawn } = require('child_process');
const ordner = fs.mkdtempSync(path.join(os.tmpdir(), 'projektuebersicht-test-'));
const PORT = 3990 + Math.floor(Math.random() * 9);
const url = p => 'http://127.0.0.1:' + PORT + p;
let server;
async function anfrage(methode, pfad, cookie, body){
const r = await fetch(url(pfad), {
method: methode,
headers: Object.assign({ 'Content-Type': 'application/json' }, cookie ? { Cookie: cookie } : {}),
body: body === undefined ? undefined : JSON.stringify(body)
});
return { status: r.status, daten: await r.json().catch(() => null), cookie: (r.headers.get('set-cookie') || '').split(';')[0] };
}
async function anmelden(username, password){
const r = await anfrage('POST', '/login', null, { username, password });
assert.strictEqual(r.status, 200, 'Anmeldung ' + username + ': ' + JSON.stringify(r.daten));
return r.cookie;
}
test.before(async () => {
server = spawn(process.execPath, [path.join(__dirname, '../server.js')], {
env: Object.assign({}, process.env, { PORT: String(PORT), SESSIONS_DIR: ordner, DB_PATH: path.join(ordner, 'test.db'),
SPEICHER_MAX_MB: '', HAENGER_MAX_S: '' }),
stdio: 'ignore'
});
for(let i = 0; i < 50; i++){
try { if((await fetch(url('/health'))).ok) return; } catch(e){ /* startet noch */ }
await new Promise(r => setTimeout(r, 200));
}
throw new Error('Testserver startet nicht');
});
test.after(() => { if(server) server.kill(); fs.rmSync(ordner, { recursive: true, force: true }); });
test('Konto nur mit Kommentar-Recht: Kommentar ja, Terminplan nein', async () => {
const pw = /Passwort:\s+(\S+)/.exec(fs.readFileSync(path.join(ordner, 'INITIAL_ADMIN_PASSWORD.txt'), 'utf8'))[1];
const admin = await anmelden('admin', pw);
// Rolle mit „Anwendung nutzen“ und „Kommentar und Fortschritt“, sonst nichts.
const rolle = await anfrage('POST', '/rollen', admin, { name: 'Nur Kommentar' });
assert.strictEqual(rolle.status, 200, JSON.stringify(rolle.daten));
const key = rolle.daten.neu;
assert.strictEqual((await anfrage('PUT', '/rollen/' + key + '/rechte', admin, { rechte: ['app', 'wb.kommentar'], an: true })).status, 200);
const konto = await anfrage('POST', '/admin/users', admin, { username: 'leser', firstName: 'Lea', lastName: 'Leser',
role: key, department: 'ELT', password: 'geheim-1234' });
assert.strictEqual(konto.status, 200, JSON.stringify(konto.daten));
// Ein Projekt in der aktuellen Sitzung anlegen (als Administrator).
const geladen = (await anfrage('GET', '/load-all', admin)).daten;
const woche = geladen.weeks[geladen.weeks.length - 1];
woche.projects.push({ id: 'p1', projectUid: 'u1', nr: '99-0001', name: 'Testprojekt', group: 'Aktive Projekte',
gewerke: ['ELT'], plan: { todos: [] } });
const angelegt = await anfrage('POST', '/save-week', admin, { week: woche, baseUpdatedAt: woche.updatedAt });
assert.strictEqual(angelegt.status, 200, JSON.stringify(angelegt.daten));
const leser = await anmelden('leser', 'geheim-1234');
const projekt = () => anfrage('GET', '/load-all', leser).then(r => {
const w = r.daten.weeks[r.daten.weeks.length - 1];
return { label: w.label, p: w.projects.find(x => x.id === 'p1') };
});
// Aufgabe im Terminplan anlegen: braucht „Terminplan bearbeiten“.
let { label, p } = await projekt();
const base = p.updatedAt;
p.plan.todos.push({ id: 't1', title: 'Darf nicht gespeichert werden', start: '', end: '', assignees: [], dependsOn: [] });
const verboten = await anfrage('POST', '/save-projects', leser, { label, projects: [{ project: p, baseUpdatedAt: base }] });
assert.strictEqual(verboten.status, 403);
assert.strictEqual(verboten.daten.recht, 'proj.plan');
// Kommentar des Gewerks schreiben: erlaubt.
({ label, p } = await projekt());
assert.strictEqual(p.plan.todos.length, 0, 'die verbotene Aufgabe darf nicht gespeichert sein');
p.kommentare = { ELT: { status: 'Abstimmung mit TGA steht aus', statusAuthor: 'Lea Leser', statusAt: new Date().toISOString(), besprochen: false } };
const erlaubt = await anfrage('POST', '/save-projects', leser, { label, projects: [{ project: p, baseUpdatedAt: p.updatedAt }] });
assert.strictEqual(erlaubt.status, 200, JSON.stringify(erlaubt.daten));
});
+53
View File
@@ -0,0 +1,53 @@
// Test: Zusammenführen, wenn zwei Personen gleichzeitig dieselbe Sitzung
// bearbeiten (dreiwegeZusammenfuehren() in public/js/store.js). Das Modul ist
// ein Browser-Skript; es wird hier mit leeren Platzhaltern für window und
// Zeitgeber geladen. Aufruf: npm test
const test = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const ctx = vm.createContext({ window: { addEventListener(){} }, setInterval(){}, setTimeout(){}, console });
vm.runInContext(fs.readFileSync(path.join(__dirname, '../public/js/store.js'), 'utf8'), ctx);
const zusammen = (basis, eigen, fremd) => {
const konflikte = [];
// JSON-Runde: Objekte aus dem vm-Kontext mit denen von hier vergleichbar machen.
return { ergebnis: JSON.parse(JSON.stringify(ctx.dreiwegeZusammenfuehren(basis, eigen, fremd, '', konflikte))), konflikte };
};
const kopie = x => JSON.parse(JSON.stringify(x));
const basis = { label: 'KW 46 / 2026', projects: [
{ id: 'a', name: 'Feuerwache', plan: { todos: [{ id: 't1', title: 'Lastaufstellung', done: false }] } },
{ id: 'b', name: 'Rathaus', plan: { todos: [] } }
] };
test('Änderungen an verschiedenen Projekten bleiben beide erhalten', () => {
const eigen = kopie(basis); eigen.projects[0].plan.todos[0].done = true;
const fremd = kopie(basis); fremd.projects[1].plan.todos.push({ id: 't2', title: 'Brandschutz', done: false });
const { ergebnis, konflikte } = zusammen(basis, eigen, fremd);
assert.strictEqual(ergebnis.projects[0].plan.todos[0].done, true);
assert.deepStrictEqual(ergebnis.projects[1].plan.todos.map(t => t.id), ['t2']);
assert.deepStrictEqual(konflikte, []);
});
test('Neue Aufgaben beider Seiten im selben Projekt gehen nicht verloren', () => {
const eigen = kopie(basis); eigen.projects[0].plan.todos.push({ id: 'e1', title: 'Meine' });
const fremd = kopie(basis); fremd.projects[0].plan.todos.push({ id: 'f1', title: 'Fremde' });
const ids = zusammen(basis, eigen, fremd).ergebnis.projects[0].plan.todos.map(t => t.id).sort();
assert.deepStrictEqual(ids, ['e1', 'f1', 't1']);
});
test('Gleiches Feld unterschiedlich geändert: eigene Fassung gewinnt, Konflikt wird gemeldet', () => {
const eigen = kopie(basis); eigen.projects[0].plan.todos[0].title = 'Lastaufstellung Ebene 1';
const fremd = kopie(basis); fremd.projects[0].plan.todos[0].title = 'Lastaufstellung Ebene 2';
const { ergebnis, konflikte } = zusammen(basis, eigen, fremd);
assert.strictEqual(ergebnis.projects[0].plan.todos[0].title, 'Lastaufstellung Ebene 1');
assert.strictEqual(konflikte.length, 1);
});
test('Von der anderen Seite gelöschte, hier unveränderte Aufgabe bleibt gelöscht', () => {
const eigen = kopie(basis);
const fremd = kopie(basis); fremd.projects[0].plan.todos = [];
assert.deepStrictEqual(zusammen(basis, eigen, fremd).ergebnis.projects[0].plan.todos, []);
});