Valgfri app-auth ved siden av proxy-modus (default uendret). Signert sesjons-cookie (scrypt-passord, HMAC, ingen avhengigheter), gating av API + WebSocket, og en selvstendig neon-login (public/login.html) for uautentiserte. Trusted bypass: en proxy-identitet (X-Remote-User / Basic Auth) regnes som innlogget uten app-passord (TRUST_PROXY_HEADER, default på), så man kan bootstrappe: logg inn via NPM Basic Auth → sett app-passord i konto-modalen → skru av Basic Auth. Konto-UI for sett/endre passord + logg ut. AUTH_MODE leses per createServer-instans (testbart). 34 integrasjonstester (8 nye for app-auth), alle grønne. README/CLAUDE.md/setup-skript oppdatert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
510 lines
21 KiB
JavaScript
510 lines
21 KiB
JavaScript
'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('update med comment legger til status-kommentar og aktivitets-detalj', async () => {
|
|
let res = await req('POST', '/api/tasks', { title: 'Kommentar-ved-status' });
|
|
const { task } = await res.json();
|
|
|
|
res = await req('PUT', `/api/tasks/${task.id}`, { v: task.v, status: 'done', comment: 'Verifisert i lab, rullet ut' });
|
|
assert.equal(res.status, 200);
|
|
const { task: done } = await res.json();
|
|
assert.equal(done.status, 'done');
|
|
assert.equal(done.comments.length, 1);
|
|
assert.equal(done.comments[0].kind, 'status');
|
|
assert.equal(done.comments[0].status, 'done');
|
|
assert.equal(done.comments[0].text, 'Verifisert i lab, rullet ut');
|
|
|
|
// Aktivitetsloggen skal nevne kommentaren
|
|
res = await req('GET', '/api/state');
|
|
const { activity } = await res.json();
|
|
const entry = activity.find(a => a.taskId === task.id && a.action === 'done');
|
|
assert.ok(entry.detail.some(d => d.includes('Verifisert i lab')));
|
|
});
|
|
|
|
test('update uten comment legger ikke til kommentar', async () => {
|
|
let res = await req('POST', '/api/tasks', { title: 'Ingen-kommentar' });
|
|
const { task } = await res.json();
|
|
res = await req('PUT', `/api/tasks/${task.id}`, { v: task.v, status: 'progress' });
|
|
const { task: upd } = await res.json();
|
|
assert.equal(upd.comments.length, 0);
|
|
});
|
|
|
|
test('dagsplan: tom som standard, sett ordnet liste, hent tilbake', async () => {
|
|
// Lag to oppgaver å planlegge
|
|
const a = (await (await req('POST', '/api/tasks', { title: 'Plan A' })).json()).task;
|
|
const b = (await (await req('POST', '/api/tasks', { title: 'Plan B' })).json()).task;
|
|
|
|
let res = await req('GET', '/api/plan?date=2026-06-15');
|
|
assert.equal(res.status, 200);
|
|
assert.deepEqual((await res.json()).items, []);
|
|
|
|
res = await req('PUT', '/api/plan', { date: '2026-06-15', items: [b.id, a.id] });
|
|
assert.equal(res.status, 200);
|
|
assert.deepEqual((await res.json()).items, [b.id, a.id]); // rekkefølge bevart
|
|
|
|
res = await req('GET', '/api/plan?date=2026-06-15');
|
|
assert.deepEqual((await res.json()).items, [b.id, a.id]);
|
|
});
|
|
|
|
test('dagsplan: er per bruker', async () => {
|
|
const a = (await (await req('POST', '/api/tasks', { title: 'Privat plan' })).json()).task;
|
|
await req('PUT', '/api/plan', { date: '2026-06-16', items: [a.id] }, { 'X-Remote-User': 'jon' });
|
|
|
|
// Annen bruker ser sin egen (tomme) plan
|
|
const res = await req('GET', '/api/plan?date=2026-06-16', null, { 'X-Remote-User': 'kari' });
|
|
assert.deepEqual((await res.json()).items, []);
|
|
});
|
|
|
|
test('dagsplan: ugyldige/dupliserte/ukjente ider filtreres bort', async () => {
|
|
const a = (await (await req('POST', '/api/tasks', { title: 'Gyldig plan-task' })).json()).task;
|
|
const res = await req('PUT', '/api/plan', {
|
|
date: '2026-06-17', items: [a.id, a.id, 't_finnesikke', 12345, null],
|
|
});
|
|
assert.deepEqual((await res.json()).items, [a.id]); // dedupet + kun eksisterende
|
|
});
|
|
|
|
test('dagsplan: sletting av oppgave fjerner den fra planen', async () => {
|
|
const a = (await (await req('POST', '/api/tasks', { title: 'Skal slettes fra plan' })).json()).task;
|
|
await req('PUT', '/api/plan', { date: '2026-06-18', items: [a.id] });
|
|
await req('DELETE', `/api/tasks/${a.id}`);
|
|
const res = await req('GET', '/api/plan?date=2026-06-18');
|
|
assert.deepEqual((await res.json()).items, []);
|
|
});
|
|
|
|
test('dagsplan: ugyldig dato faller tilbake til i dag', async () => {
|
|
const res = await req('GET', '/api/plan?date=tull');
|
|
const data = await res.json();
|
|
assert.match(data.date, /^\d{4}-\d{2}-\d{2}$/);
|
|
});
|
|
|
|
test('dagsplan: navn lagres og hentes', async () => {
|
|
const a = (await (await req('POST', '/api/tasks', { title: 'Navngitt plan-task' })).json()).task;
|
|
let res = await req('PUT', '/api/plan', { date: '2026-06-20', items: [a.id], name: 'Vedlikeholdsvindu DC-OSL' });
|
|
assert.equal((await res.json()).name, 'Vedlikeholdsvindu DC-OSL');
|
|
res = await req('GET', '/api/plan?date=2026-06-20');
|
|
const data = await res.json();
|
|
assert.equal(data.name, 'Vedlikeholdsvindu DC-OSL');
|
|
assert.deepEqual(data.items, [a.id]);
|
|
});
|
|
|
|
test('dagsplan: flytt slår sammen til måldato og tømmer kilden', async () => {
|
|
const a = (await (await req('POST', '/api/tasks', { title: 'Flytt A' })).json()).task;
|
|
const b = (await (await req('POST', '/api/tasks', { title: 'Flytt B' })).json()).task;
|
|
await req('PUT', '/api/plan', { date: '2026-07-01', items: [a.id], name: 'Mandagsplan' });
|
|
await req('PUT', '/api/plan', { date: '2026-07-02', items: [b.id] });
|
|
|
|
let res = await req('PUT', '/api/plan/move', { from: '2026-07-01', to: '2026-07-02' });
|
|
assert.equal(res.status, 200);
|
|
const moved = await res.json();
|
|
assert.deepEqual(moved.items, [b.id, a.id]); // måldatoens eksisterende først, så kildens
|
|
|
|
// Kilden er tom nå
|
|
res = await req('GET', '/api/plan?date=2026-07-01');
|
|
assert.deepEqual((await res.json()).items, []);
|
|
});
|
|
|
|
test('dagsplan: deling — privat plan er skjult for andre, delt er synlig', async () => {
|
|
const a = (await (await req('POST', '/api/tasks', { title: 'Delt plan-task' })).json()).task;
|
|
// jon lager en delt plan (default shared=true)
|
|
await req('PUT', '/api/plan', { date: '2026-07-10', items: [a.id] }, { 'X-Remote-User': 'jon' });
|
|
// kari kan se den
|
|
let res = await req('GET', '/api/plan?date=2026-07-10&user=jon', null, { 'X-Remote-User': 'kari' });
|
|
assert.equal(res.status, 200);
|
|
assert.deepEqual((await res.json()).items, [a.id]);
|
|
|
|
// jon gjør den privat
|
|
res = await req('PUT', '/api/plan/share', { date: '2026-07-10', shared: false }, { 'X-Remote-User': 'jon' });
|
|
assert.equal((await res.json()).shared, false);
|
|
// kari får 403
|
|
res = await req('GET', '/api/plan?date=2026-07-10&user=jon', null, { 'X-Remote-User': 'kari' });
|
|
assert.equal(res.status, 403);
|
|
// jon ser fortsatt sin egen
|
|
res = await req('GET', '/api/plan?date=2026-07-10', null, { 'X-Remote-User': 'jon' });
|
|
assert.deepEqual((await res.json()).items, [a.id]);
|
|
});
|
|
|
|
test('dagsplan: planIndex viser delte planer + egne private', async () => {
|
|
const a = (await (await req('POST', '/api/tasks', { title: 'Index-task' })).json()).task;
|
|
// jon: privat plan på en egen dato
|
|
await req('PUT', '/api/plan', { date: '2026-07-15', items: [a.id] }, { 'X-Remote-User': 'jon' });
|
|
await req('PUT', '/api/plan/share', { date: '2026-07-15', shared: false }, { 'X-Remote-User': 'jon' });
|
|
|
|
// jon ser sin private i sin planIndex
|
|
let res = await req('GET', '/api/state', null, { 'X-Remote-User': 'jon' });
|
|
let idx = (await res.json()).planIndex;
|
|
assert.ok(idx[a.id] && idx[a.id].some(e => e.user === 'jon' && e.date === '2026-07-15'));
|
|
|
|
// kari ser den IKKE (privat, ikke hennes)
|
|
res = await req('GET', '/api/state', null, { 'X-Remote-User': 'kari' });
|
|
idx = (await res.json()).planIndex;
|
|
assert.ok(!idx[a.id] || !idx[a.id].some(e => e.user === 'jon' && e.date === '2026-07-15'));
|
|
});
|
|
|
|
test('kategorier: legg til egendefinert, og bruk den på en oppgave', async () => {
|
|
let res = await req('POST', '/api/categories', { name: 'Trådløst' });
|
|
assert.equal(res.status, 201);
|
|
const { category } = await res.json();
|
|
assert.match(category.id, /^[A-Z0-9]+$/);
|
|
assert.equal(category.name, 'Trådløst');
|
|
|
|
// Kategorien er nå i /api/state
|
|
res = await req('GET', '/api/state');
|
|
const cats = (await res.json()).categories;
|
|
assert.ok(cats.some(c => c.id === category.id && c.custom));
|
|
|
|
// Den kan brukes på en oppgave (godtas av validering)
|
|
res = await req('POST', '/api/tasks', { title: 'Wifi-oppgave', category: category.id });
|
|
assert.equal((await res.json()).task.category, category.id);
|
|
});
|
|
|
|
test('kategorier: tomt navn avvises, ukjent kategori-id på oppgave faller til default', async () => {
|
|
let res = await req('POST', '/api/categories', { name: ' ' });
|
|
assert.equal(res.status, 400);
|
|
res = await req('POST', '/api/tasks', { title: 'Ukjent kat', category: 'FINNESIKKE' });
|
|
assert.equal((await res.json()).task.category, 'L3');
|
|
});
|
|
|
|
test('websocket: delt plan broadcastes til alle, privat kun til eier', async () => {
|
|
const wsUrl = baseUrl.replace('http', 'ws') + '/ws';
|
|
const mk = user => new Promise((resolve, reject) => {
|
|
const c = new WebSocket(wsUrl, { headers: { 'X-Remote-User': user } });
|
|
c.msgs = [];
|
|
c.on('message', raw => c.msgs.push(JSON.parse(raw)));
|
|
c.on('open', () => resolve(c));
|
|
c.on('error', reject);
|
|
});
|
|
const jon = await mk('testuser'); // samme bruker som req() bruker
|
|
const kari = await mk('kari');
|
|
await new Promise(r => setTimeout(r, 150));
|
|
|
|
const a = (await (await req('POST', '/api/tasks', { title: 'WS-delt-plan' })).json()).task;
|
|
// Delt plan (default) → begge får den
|
|
await req('PUT', '/api/plan', { date: '2026-08-01', items: [a.id] });
|
|
await new Promise(r => setTimeout(r, 150));
|
|
assert.ok(jon.msgs.some(m => m.type === 'plan' && m.date === '2026-08-01' && m.items.includes(a.id)), 'jon fikk delt plan');
|
|
assert.ok(kari.msgs.some(m => m.type === 'plan' && m.date === '2026-08-01' && m.items.includes(a.id)), 'kari fikk delt plan');
|
|
|
|
// Gjør privat → kari får tom melding (fjerner badge), jon får ekte
|
|
jon.msgs.length = 0; kari.msgs.length = 0;
|
|
await req('PUT', '/api/plan/share', { date: '2026-08-01', shared: false });
|
|
await new Promise(r => setTimeout(r, 150));
|
|
const jonMsg = jon.msgs.find(m => m.type === 'plan' && m.date === '2026-08-01');
|
|
const kariMsg = kari.msgs.find(m => m.type === 'plan' && m.date === '2026-08-01');
|
|
assert.ok(jonMsg && jonMsg.items.includes(a.id), 'jon ser fortsatt sin private plan');
|
|
assert.ok(kariMsg && kariMsg.items.length === 0, 'kari får tom plan-melding for privat');
|
|
|
|
jon.close(); kari.close();
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
// ---------- App-modus innlogging (AUTH_MODE=app) ----------
|
|
let appServer, appUrl, appDir;
|
|
|
|
function aReq(method, p, body, headers = {}) {
|
|
return fetch(appUrl + p, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
});
|
|
}
|
|
function cookieFrom(res) {
|
|
const sc = res.headers.get('set-cookie') || '';
|
|
const m = sc.match(/netops_session=([^;]*)/);
|
|
return m ? `netops_session=${m[1]}` : '';
|
|
}
|
|
|
|
before(async () => {
|
|
appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'netops-app-'));
|
|
({ server: appServer } = createServer({ dataDir: appDir, authMode: 'app', sessionTtlDays: 7 }));
|
|
await new Promise(r => appServer.listen(0, '127.0.0.1', r));
|
|
appUrl = `http://127.0.0.1:${appServer.address().port}`;
|
|
});
|
|
after(() => {
|
|
appServer.close();
|
|
fs.rmSync(appDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('app-auth: uten innlogging gir 401 på API med login-flagg', async () => {
|
|
const res = await aReq('GET', '/api/state');
|
|
assert.equal(res.status, 401);
|
|
assert.equal((await res.json()).login, true);
|
|
});
|
|
|
|
test('app-auth: navigasjon uten innlogging serverer login-siden', async () => {
|
|
const res = await aReq('GET', '/', undefined, { Accept: 'text/html' });
|
|
assert.equal(res.status, 200);
|
|
const html = await res.text();
|
|
assert.match(html, /NetOps/);
|
|
assert.match(html, /api\/auth\/login/); // login-skjemaet
|
|
});
|
|
|
|
test('app-auth: proxy-header gir trusted bypass', async () => {
|
|
const res = await aReq('GET', '/api/state', undefined, { 'X-Remote-User': 'jon' });
|
|
assert.equal(res.status, 200);
|
|
const data = await res.json();
|
|
assert.equal(data.user, 'jon');
|
|
assert.equal(data.auth.via, 'proxy');
|
|
assert.equal(data.auth.hasPassword, false);
|
|
});
|
|
|
|
test('app-auth: bootstrap — sett passord via bypass, logg så inn med passord', async () => {
|
|
// Sett passord mens vi er innlogget via proxy
|
|
let res = await aReq('POST', '/api/auth/password', { password: 'hemmelig123' }, { 'X-Remote-User': 'jon' });
|
|
assert.equal(res.status, 200);
|
|
assert.ok(cookieFrom(res), 'fikk sesjons-cookie ved passord-setting');
|
|
|
|
// Feil passord avvises
|
|
res = await aReq('POST', '/api/auth/login', { user: 'jon', password: 'feil' });
|
|
assert.equal(res.status, 401);
|
|
|
|
// Riktig passord gir sesjons-cookie (uten proxy-header)
|
|
res = await aReq('POST', '/api/auth/login', { user: 'jon', password: 'hemmelig123' });
|
|
assert.equal(res.status, 200);
|
|
const cookie = cookieFrom(res);
|
|
assert.ok(cookie);
|
|
|
|
// Sesjons-cookien gir tilgang uten proxy-header
|
|
res = await aReq('GET', '/api/state', undefined, { Cookie: cookie });
|
|
assert.equal(res.status, 200);
|
|
const data = await res.json();
|
|
assert.equal(data.user, 'jon');
|
|
assert.equal(data.auth.via, 'session');
|
|
assert.equal(data.auth.hasPassword, true);
|
|
});
|
|
|
|
test('app-auth: for kort passord avvises', async () => {
|
|
const res = await aReq('POST', '/api/auth/password', { password: 'kort' }, { 'X-Remote-User': 'kari' });
|
|
assert.equal(res.status, 400);
|
|
});
|
|
|
|
test('app-auth: logout tømmer cookien', async () => {
|
|
const res = await aReq('POST', '/api/auth/logout');
|
|
assert.match(res.headers.get('set-cookie') || '', /netops_session=;|Max-Age=0/);
|
|
});
|
|
|
|
test('app-auth: TRUST_PROXY_HEADER=false ignorerer proxy-header', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'netops-app2-'));
|
|
const { server } = createServer({ dataDir: dir, authMode: 'app', trustProxyHeader: false });
|
|
await new Promise(r => server.listen(0, '127.0.0.1', r));
|
|
const url = `http://127.0.0.1:${server.address().port}`;
|
|
try {
|
|
const res = await fetch(url + '/api/state', { headers: { 'X-Remote-User': 'jon' } });
|
|
assert.equal(res.status, 401); // bypass av
|
|
} finally {
|
|
server.close();
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('proxy-modus (default): ingen guard, alt åpent', async () => {
|
|
// baseUrl-serveren kjører i proxy-modus — bekreft at /api/state er åpent uten cookie
|
|
const res = await fetch(baseUrl + '/api/state', { headers: { 'X-Remote-User': 'whoever' } });
|
|
assert.equal(res.status, 200);
|
|
assert.equal((await res.json()).auth.mode, 'proxy');
|
|
});
|