Legg til Node/npm-versjon med sanntidssynk + Debian 13-oppsettsskript
Ny app i netops-todo-node/: Express + ws, WebSocket-push i stedet for polling, kanban med drag & drop, kommentarer, aktivitetslogg, optimistisk låsing, quick-add-parsing og presence. 12 integrasjonstester (node:test). setup-debian13.sh setter opp alt på en fersk LXC; README dekker Nginx Proxy Manager-oppsett. PHP-versjonen er uendret (kun handoff-peker). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
72a78b55fb
commit
6b9dbf8254
15 changed files with 3814 additions and 0 deletions
209
netops-todo-node/test/api.test.js
Normal file
209
netops-todo-node/test/api.test.js
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
'use strict';
|
||||
/**
|
||||
* Integrasjonstester: booter serveren på tilfeldig port med en midlertidig
|
||||
* datakatalog og tester REST-API + WebSocket-broadcast ende-til-ende.
|
||||
* Kjør med: npm test
|
||||
*/
|
||||
|
||||
const { test, before, after } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const WebSocket = require('ws');
|
||||
const { createServer } = require('../server');
|
||||
|
||||
let server, baseUrl, dataDir;
|
||||
|
||||
function req(method, p, body = null, headers = {}) {
|
||||
return fetch(baseUrl + p, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json', 'X-Remote-User': 'testuser', ...headers },
|
||||
body: body !== null ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'netops-test-'));
|
||||
({ server } = createServer({ dataDir }));
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
baseUrl = `http://127.0.0.1:${server.address().port}`;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
server.close();
|
||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('state starter tomt og identifiserer bruker fra header', async () => {
|
||||
const res = await req('GET', '/api/state');
|
||||
assert.equal(res.status, 200);
|
||||
const data = await res.json();
|
||||
assert.equal(data.user, 'testuser');
|
||||
assert.deepEqual(data.tasks, []);
|
||||
});
|
||||
|
||||
test('seed fyller tom database, og nekter når data finnes', async () => {
|
||||
let res = await req('POST', '/api/seed');
|
||||
assert.equal(res.status, 200);
|
||||
const { count } = await res.json();
|
||||
assert.equal(count, 16);
|
||||
|
||||
res = await req('POST', '/api/seed');
|
||||
assert.equal(res.status, 409);
|
||||
});
|
||||
|
||||
test('create normaliserer og setter audit-felter', async () => {
|
||||
const res = await req('POST', '/api/tasks', {
|
||||
title: 'Test BGP-peer', category: 'BGP', priority: 'P1',
|
||||
status: 'ugyldig-status', estHours: 5000, tags: ['x', '', 'y'],
|
||||
deadline: 'ikke-en-dato', desc: 'hei\x00der',
|
||||
});
|
||||
assert.equal(res.status, 201);
|
||||
const { task } = await res.json();
|
||||
assert.equal(task.status, 'todo'); // ugyldig → default
|
||||
assert.equal(task.estHours, 999); // klampet
|
||||
assert.deepEqual(task.tags, ['x', 'y']); // tomme fjernet
|
||||
assert.equal(task.deadline, ''); // ugyldig format
|
||||
assert.equal(task.desc, 'heider'); // kontrolltegn fjernet
|
||||
assert.equal(task.createdBy, 'testuser');
|
||||
assert.equal(task.v, 1);
|
||||
});
|
||||
|
||||
test('create uten tittel feiler', async () => {
|
||||
const res = await req('POST', '/api/tasks', { desc: 'mangler tittel' });
|
||||
assert.equal(res.status, 400);
|
||||
});
|
||||
|
||||
test('update med riktig versjon ok, feil versjon gir 409 med serverens task', async () => {
|
||||
let res = await req('POST', '/api/tasks', { title: 'Versjonstest' });
|
||||
const { task } = await res.json();
|
||||
|
||||
res = await req('PUT', `/api/tasks/${task.id}`, { ...task, status: 'progress', v: task.v });
|
||||
assert.equal(res.status, 200);
|
||||
const { task: t2 } = await res.json();
|
||||
assert.equal(t2.status, 'progress');
|
||||
assert.equal(t2.v, 2);
|
||||
|
||||
// Lagring fra utdatert versjon skal avvises
|
||||
res = await req('PUT', `/api/tasks/${task.id}`, { ...task, status: 'blocked', v: 1 });
|
||||
assert.equal(res.status, 409);
|
||||
const conflict = await res.json();
|
||||
assert.equal(conflict.task.v, 2);
|
||||
assert.equal(conflict.task.status, 'progress');
|
||||
});
|
||||
|
||||
test('done setter completed, gjenåpning nullstiller', async () => {
|
||||
let res = await req('POST', '/api/tasks', { title: 'Ferdigtest' });
|
||||
let { task } = await res.json();
|
||||
|
||||
res = await req('PUT', `/api/tasks/${task.id}`, { v: task.v, status: 'done' });
|
||||
({ task } = await res.json());
|
||||
assert.equal(task.completed, new Date().toISOString().slice(0, 10));
|
||||
assert.equal(task.completedBy, 'testuser');
|
||||
// Delvis update skal beholde øvrige felter
|
||||
assert.equal(task.title, 'Ferdigtest');
|
||||
|
||||
res = await req('PUT', `/api/tasks/${task.id}`, { v: task.v, status: 'todo' });
|
||||
({ task } = await res.json());
|
||||
assert.equal(task.completed, null);
|
||||
});
|
||||
|
||||
test('kommentarer: legg til, slett egne, nekt andres', async () => {
|
||||
let res = await req('POST', '/api/tasks', { title: 'Kommentartest' });
|
||||
const { task } = await res.json();
|
||||
|
||||
res = await req('POST', `/api/tasks/${task.id}/comments`, { text: 'Første kommentar' });
|
||||
assert.equal(res.status, 201);
|
||||
const { comment } = await res.json();
|
||||
assert.equal(comment.by, 'testuser');
|
||||
|
||||
// Annen bruker får ikke slette
|
||||
res = await req('DELETE', `/api/tasks/${task.id}/comments/${comment.id}`, null, { 'X-Remote-User': 'kari' });
|
||||
assert.equal(res.status, 400);
|
||||
|
||||
res = await req('DELETE', `/api/tasks/${task.id}/comments/${comment.id}`);
|
||||
assert.equal(res.status, 200);
|
||||
const { task: after } = await res.json();
|
||||
assert.equal(after.comments.length, 0);
|
||||
});
|
||||
|
||||
test('delete fjerner oppgave, 404 ved ukjent id', async () => {
|
||||
let res = await req('POST', '/api/tasks', { title: 'Slettetest' });
|
||||
const { task } = await res.json();
|
||||
res = await req('DELETE', `/api/tasks/${task.id}`);
|
||||
assert.equal(res.status, 200);
|
||||
res = await req('DELETE', `/api/tasks/${task.id}`);
|
||||
assert.equal(res.status, 404);
|
||||
});
|
||||
|
||||
test('aktivitetslogg føres', async () => {
|
||||
const res = await req('GET', '/api/state');
|
||||
const { activity } = await res.json();
|
||||
assert.ok(activity.length > 0);
|
||||
assert.ok(activity.some(a => a.action === 'create'));
|
||||
assert.ok(activity.some(a => a.action === 'done'));
|
||||
assert.ok(activity.some(a => a.action === 'comment'));
|
||||
});
|
||||
|
||||
test('websocket: mottar sync ved tilkobling og broadcast ved endringer', async () => {
|
||||
const wsUrl = baseUrl.replace('http', 'ws') + '/ws';
|
||||
const wsClient = new WebSocket(wsUrl, { headers: { 'X-Remote-User': 'kari' } });
|
||||
const messages = [];
|
||||
wsClient.on('message', raw => messages.push(JSON.parse(raw)));
|
||||
await new Promise((resolve, reject) => {
|
||||
wsClient.on('open', resolve);
|
||||
wsClient.on('error', reject);
|
||||
});
|
||||
|
||||
// Vent på hello + sync
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
const hello = messages.find(m => m.type === 'hello');
|
||||
assert.equal(hello.user, 'kari');
|
||||
assert.ok(messages.some(m => m.type === 'sync' && Array.isArray(m.tasks)));
|
||||
|
||||
// En REST-endring fra annen bruker skal pushes til kari
|
||||
const res = await req('POST', '/api/tasks', { title: 'WS-broadcast-test' });
|
||||
const { task } = await res.json();
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
const broadcastMsg = messages.find(m => m.type === 'task' && m.op === 'create' && m.task.id === task.id);
|
||||
assert.ok(broadcastMsg, 'broadcast ikke mottatt');
|
||||
assert.equal(broadcastMsg.by, 'testuser');
|
||||
assert.equal(broadcastMsg.activity.action, 'create');
|
||||
|
||||
wsClient.close();
|
||||
});
|
||||
|
||||
test('replace erstatter alt og persisterer til disk', async () => {
|
||||
const res = await req('POST', '/api/replace', [
|
||||
{ title: 'Eneste oppgave', category: 'NMS', priority: 'P4' },
|
||||
{ ugyldig: 'uten tittel' },
|
||||
]);
|
||||
const { count } = await res.json();
|
||||
assert.equal(count, 1);
|
||||
|
||||
// Vent på at skrivekøen tømmes, og verifiser fila på disk
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
const onDisk = JSON.parse(fs.readFileSync(path.join(dataDir, 'tasks.json'), 'utf8'));
|
||||
assert.equal(onDisk.length, 1);
|
||||
assert.equal(onDisk[0].title, 'Eneste oppgave');
|
||||
});
|
||||
|
||||
test('cookie-auth: login setter cookie, ugyldig navn avvises', async () => {
|
||||
let res = await fetch(baseUrl + '/api/login', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: 'jon.v' }),
|
||||
});
|
||||
assert.equal(res.status, 200);
|
||||
assert.match(res.headers.get('set-cookie'), /netops_user=jon\.v/);
|
||||
|
||||
// Brukeren identifiseres via cookien
|
||||
res = await fetch(baseUrl + '/api/state', { headers: { Cookie: 'netops_user=jon.v' } });
|
||||
assert.equal((await res.json()).user, 'jon.v');
|
||||
|
||||
res = await fetch(baseUrl + '/api/login', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: '<script>' }),
|
||||
});
|
||||
assert.equal(res.status, 400);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue