3b83fc94e9
Anmärkningen var att en inloggning som SER ut att autentisera ger portalen bakom den en auktoritet den inte har. Rättelsen är inte att ta bort skylten utan att göra spärren verklig där det finns något att spärra mot. Med plattformen konfigurerad autentiserar inloggningen mot /api/auth/logga-in, varje portalväg går genom Portalvakt, och fakturavyn läser organisationens egna fakturor ur GET /api/fakturor. Misslyckas hämtningen visas felet — inte exemplet: data som ser äkta ut men inte är det är värre än ett synligt fel. Utloggningen finns i portalhuvudet och rensar sessionen på riktigt. Utan konfigurerad plattform finns ingen session att kräva, och demonstrationsmärkningen visas exakt i det läget. ---- Varför garantin inte är ett källkodstest ------------------------ Sviten kan bara läsa koden och se att vakten är inkopplad. Släpper den igenom ändå — fel villkor, fel ordning, ett <Navigate> som aldrig hinner rendera — ser koden likadan ut. Och en spärr som inte spärrar är precis det m-9 handlade om. e2e/portalsparr.mjs bygger därför med plattformen konfigurerad och tittar var besökaren faktiskt hamnar: fem vägar spärrade utan session, fem öppna med, utloggning som rensar token och stänger portalen igen. Mutationstestad — att plocka bort vakten från en enda route fäller den. Bevisad: 374 enhetstester, typkontroll, genomgången 4/4 ärenden, portalspärren 14/14. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
154 lines
5.4 KiB
JavaScript
154 lines
5.4 KiB
JavaScript
// Portalspärren (revision 2, m-9).
|
|
//
|
|
// Anmärkningen var att en inloggning som SER ut att autentisera ger
|
|
// portalen bakom den en auktoritet den inte har. Rättelsen är att spärren
|
|
// blir verklig där det finns något att spärra mot: är plattformen
|
|
// konfigurerad krävs en giltig session, annars är portalen uttryckligen
|
|
// märkt som demonstration.
|
|
//
|
|
// ---- Varför en egen körning och inte ett enhetstest ------------------
|
|
//
|
|
// Sviten kan bara läsa källkod och se att vakten är inkopplad. Släpper
|
|
// den ändå igenom — fel villkor, fel ordning, ett <Navigate> som aldrig
|
|
// hinner rendera — ser koden likadan ut. Och en spärr som inte spärrar
|
|
// är exakt det m-9 handlade om.
|
|
//
|
|
// Bygget görs därför här, med plattformen konfigurerad, och kontrollen
|
|
// tittar var man faktiskt hamnar. Adressen pekar på ett värdnamn som inte
|
|
// finns: spärren ska hålla utan att någon server svarar.
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
import { createServer } from "node:http";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { dirname, extname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { chromium } from "playwright";
|
|
|
|
const ROT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
const DIST = join(ROT, "dist");
|
|
const PORT = 4271;
|
|
const BAS = `http://127.0.0.1:${PORT}`;
|
|
|
|
const VYER = [
|
|
"/alva/portal",
|
|
"/alva/portal/analys",
|
|
"/alva/portal/kunskapskallor",
|
|
"/alva/portal/integration",
|
|
"/alva/portal/fakturor",
|
|
];
|
|
|
|
// Nycklarna är plattformsklientens egna (src/felsokning/plattform.ts).
|
|
// Skulle de byta namn ska den här kontrollen falla, inte tyst sluta mäta:
|
|
// en session som läggs under fel nyckel ger samma utfall som ingen
|
|
// session, och då hade "spärrad" sett ut som ett godkänt resultat.
|
|
const TOKEN_NYCKEL = "gf-plattform-token";
|
|
const KONTO_NYCKEL = "gf-plattform-konto";
|
|
|
|
const TYPER = {
|
|
".html": "text/html; charset=utf-8",
|
|
".js": "text/javascript",
|
|
".css": "text/css",
|
|
".woff2": "font/woff2",
|
|
".svg": "image/svg+xml",
|
|
};
|
|
|
|
if (!process.env.GENOMGANG_HOPPA_BYGG) {
|
|
execFileSync("npx", ["vite", "build"], {
|
|
cwd: ROT,
|
|
stdio: ["ignore", "ignore", "inherit"],
|
|
env: {
|
|
...process.env,
|
|
VITE_HASH_ROUTER: "1",
|
|
VITE_PLATTFORM_URL: "https://plattform.invalid",
|
|
VITE_SUPABASE_URL: "https://portalsparr.invalid",
|
|
VITE_SUPABASE_PUBLISHABLE_KEY: "portalsparr",
|
|
},
|
|
});
|
|
}
|
|
|
|
if (!existsSync(join(DIST, "index.html"))) {
|
|
console.error(`Inget bygge i ${DIST}.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const server = createServer((req, res) => {
|
|
const vag = decodeURIComponent(req.url.split("?")[0]);
|
|
const fil = join(DIST, vag === "/" ? "index.html" : vag);
|
|
const mal = existsSync(fil) && extname(fil) ? fil : join(DIST, "index.html");
|
|
res.writeHead(200, { "Content-Type": TYPER[extname(mal)] ?? "application/octet-stream" });
|
|
res.end(readFileSync(mal));
|
|
}).listen(PORT);
|
|
|
|
const webblasare = await chromium.launch({
|
|
executablePath: process.env.PLAYWRIGHT_CHROMIUM ?? undefined,
|
|
});
|
|
|
|
let fel = 0;
|
|
const kontroll = (namn, faktiskt, vantat) => {
|
|
if (faktiskt === vantat) console.log(`✓ ${namn}`);
|
|
else {
|
|
fel++;
|
|
console.log(`✗ ${namn}: fick '${faktiskt}', väntade '${vantat}'`);
|
|
}
|
|
};
|
|
const hash = (sida) => new URL(sida.url()).hash;
|
|
|
|
try {
|
|
// ---- Utan session -----------------------------------------------------
|
|
{
|
|
const ctx = await webblasare.newContext();
|
|
const s = await ctx.newPage();
|
|
for (const vy of VYER) {
|
|
await s.goto(`${BAS}/#${vy}`, { waitUntil: "networkidle" });
|
|
await s.waitForTimeout(300);
|
|
kontroll(`${vy} spärras`, hash(s), "#/alva/logga-in");
|
|
}
|
|
await ctx.close();
|
|
}
|
|
|
|
// ---- Med session ------------------------------------------------------
|
|
//
|
|
// Sessionen läggs in som inloggningen skulle ha lagt in den. Spärren
|
|
// ska öppna — annars är den inte en spärr utan ett hinder.
|
|
{
|
|
const ctx = await webblasare.newContext();
|
|
const s = await ctx.newPage();
|
|
await s.goto(`${BAS}/#/alva`, { waitUntil: "networkidle" });
|
|
await s.evaluate(
|
|
([tn, kn]) => {
|
|
localStorage.setItem(tn, "provtoken");
|
|
localStorage.setItem(kn, JSON.stringify({ namn: "Anna", roll: "admin", organisation: "Verkstad A" }));
|
|
},
|
|
[TOKEN_NYCKEL, KONTO_NYCKEL],
|
|
);
|
|
|
|
for (const vy of VYER) {
|
|
await s.goto(`${BAS}/#${vy}`, { waitUntil: "networkidle" });
|
|
await s.waitForTimeout(300);
|
|
kontroll(`${vy} öppnas med session`, hash(s), `#${vy}`);
|
|
}
|
|
|
|
// Utloggningen ska både finnas och verkligen avsluta sessionen. En
|
|
// knapp som navigerar utan att rensa token vore samma sken igen.
|
|
await s.goto(`${BAS}/#/alva/portal`, { waitUntil: "networkidle" });
|
|
await s.waitForTimeout(300);
|
|
const knapp = s.getByRole("button", { name: /sign out/i });
|
|
kontroll("utloggning finns i portalen", await knapp.count(), 1);
|
|
await knapp.click();
|
|
await s.waitForTimeout(500);
|
|
kontroll("utloggning leder till inloggningen", hash(s), "#/alva/logga-in");
|
|
kontroll("token är borta", await s.evaluate((tn) => localStorage.getItem(tn), TOKEN_NYCKEL), null);
|
|
|
|
await s.goto(`${BAS}/#/alva/portal`, { waitUntil: "networkidle" });
|
|
await s.waitForTimeout(300);
|
|
kontroll("portalen är stängd igen efter utloggning", hash(s), "#/alva/logga-in");
|
|
await ctx.close();
|
|
}
|
|
} finally {
|
|
await webblasare.close();
|
|
server.close();
|
|
}
|
|
|
|
console.log(fel === 0 ? "\nportalspärren håller" : `\n${fel} fel`);
|
|
process.exit(fel === 0 ? 0 : 1);
|