'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: '