a94c228cab
---- Garantier (ALVA-SPEC-040) ----------------------------------------- Skiljelinjen mellan LAGSTADGAT och AVTALAT bär allt annat, och den förväxlas dagligen åt bägge håll. Överraskande lite av det verkstaden kallar garanti är lagstadgat: nybilsgaranti avtalad. Ingen lag i EU eller USA kräver den. rostskyddsgaranti avtalad. Ingen lag kräver den, någonstans. lackgaranti avtalad. drivlinegaranti avtalad. avgasgaranti LAGSTADGAD i bägge, och den enda som är det. Att kalla rostskyddet lagstadgat är fel; att kalla avgasgarantin en tillverkarutfästelse är fel åt andra hållet, och det andra felet kostar kunden pengar — en katalysator på en sexårig bil offereras rutinmässigt som en betald reparation när den inte är det. Varje lagstadgad post bär källa och kontrolldatum: 2019/771 (2 år, säljaren — inte tillverkaren), MVBER 461/2010 förlängd till 2028, 715/2007 (5 år/100 000 km), 2024/1257 (8 år/160 000 km, batteri 80 % vid 5 år och 72 % vid 8), CAA § 207 (2/24 000 och 8/80 000 miles), Magnuson–Moss § 2302(c), CARB (3/50 000 och 7/70 000), ACC II (70 % SoH vid 8 år/100 000 miles). Bedömningen skiljer INOM, UTANFÖR och OKLART. Saknas underlag blir det oklart, aldrig utanför — att påstå att en garanti gått ut när underlaget inte räcker är det enda felet här som kostar kunden pengar. ---- Försäkring (ALVA-SPEC-041) ---------------------------------------- Modulen avgör ingenting och kan inte avgöra något. Vad som gäller bestäms av kundens eget försäkringsbrev och villkoren vid tecknandet — inte av vad ett bolag skriver i dag och inte av vad vi noterat. Testerna låser att resultatet inte innehåller något fält som liknar ett utfall, och att ett fordon utanför samtliga noterade gränser ändå inte får ett nej: registret kan vara ofullständigt och produkten en annan. Det som står överst är de sju villkoren som avgör en maskinskadefråga. Ålder och sträcka är två av dem; de fem andra avgör oftare. Registret med If, Folksam, Trygg-Hansa, Svedea, WaterCircles och Hedvig ligger under, med källa och avläsningsdatum per post, och märks som föråldrat efter 180 dagar i stället för att se aktuellt ut. ---- Felanmälan i ärendet (ALVA-PROC-0050) ----------------------------- En anmälningsknapp som bara finns i en portalvy nås aldrig av den som står med händerna i en bil. Den ligger därför i varje ärende, och tar med sitt sammanhang självt: ärende, metodik, plattformsversion och spår-id. Teknikern minns inte plattformsversionen, och att fråga efter den är att lägga över vårt problem på verkstaden. Sammanhanget HÄRLEDS och kan inte smugglas: ett regnr i anropet släpps inte igenom. En supportanmälan är inte ett skäl att flytta personuppgifter till ett annat system. Bevisat i integrationstestet. Anmälan är oföränderlig och statusen en projektion av inläggen — samma skäl som för fakturan: en anmälan vars historia kan skrivas om är inte ett underlag när någon frågar hur länge felet var känt. Verkstaden svarar i sitt eget ärende; status sätts av supporten med egen nyckel. ---- Historikkontrollen ------------------------------------------------- Kravet fanns redan: klienten öppnar inte metodiken förrän pre-diagnostiken är besvarad, och ett nej kräver skriven motivering. Det var dock aldrig BEVISAT att spärren håller. Genomgången prövar nu att metodiken inte går att nå medan historikfrågan står obesvarad — i det enda ögonblick det går att pröva, eftersom svaret därefter redan är avgivet. 429 enhetstester · 157 integrationskontroller mot riktig Postgres · genomgången 4/4 · portalspärren 16/16. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
910 lines
57 KiB
Bash
Executable File
910 lines
57 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Integrationstest för plattformstjänsten mot en riktig Postgres.
|
|
# Kör hela flödet: registrering (org + admin), synk av händelser,
|
|
# idempotens, append-only-triggern, organisationsisolering, publik
|
|
# delning med filtrering samt rollstyrd användarhantering.
|
|
#
|
|
# Krav: postgres (initdb/pg_ctl), node, curl. Kör: bash integrationstest.sh
|
|
set -euo pipefail
|
|
cd "$(dirname "$0")"
|
|
|
|
PGPORT=5499
|
|
APPPORT=8399
|
|
BAS="http://127.0.0.1:$APPPORT"
|
|
DATADIR=$(mktemp -d)
|
|
PGBIN=$(ls -d /usr/lib/postgresql/*/bin | head -1)
|
|
|
|
stada() {
|
|
kill "$SERVER_PID" 2>/dev/null || true
|
|
su postgres -c "$PGBIN/pg_ctl -D '$DATADIR' stop -m immediate" >/dev/null 2>&1 || true
|
|
rm -rf "$DATADIR"
|
|
}
|
|
trap stada EXIT
|
|
|
|
# ---- Postgres upp ----
|
|
chown postgres "$DATADIR"
|
|
su postgres -c "$PGBIN/initdb -D '$DATADIR' -A trust" >/dev/null
|
|
su postgres -c "$PGBIN/pg_ctl -D '$DATADIR' -o '-p $PGPORT -k /tmp -c listen_addresses=127.0.0.1' -l '$DATADIR/logg' start" >/dev/null
|
|
su postgres -c "$PGBIN/psql -h /tmp -p $PGPORT -d postgres -qc \"create role plattform login password 'test'\""
|
|
su postgres -c "$PGBIN/psql -h /tmp -p $PGPORT -d postgres -qc 'create database felsokning owner plattform'"
|
|
PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning -q -f ../../infra/postgres-init.sql
|
|
|
|
# ---- Tjänsten upp ----
|
|
DATABASE_URL="postgresql://plattform:test@127.0.0.1:$PGPORT/felsokning" \
|
|
JWT_SECRET=integrationshemlighet PORT=$APPPORT node server.mjs &
|
|
SERVER_PID=$!
|
|
sleep 1
|
|
|
|
falt() { node -pe "JSON.parse(require('fs').readFileSync(0,'utf8'))$1"; }
|
|
kontroll() { # kontroll <namn> <faktiskt> <forvantat>
|
|
if [ "$2" = "$3" ]; then echo "✓ $1"; else echo "✗ $1: fick '$2', väntade '$3'"; exit 1; fi
|
|
}
|
|
|
|
# 1. Registrera organisation A (admin)
|
|
SVAR=$(curl -s -X POST "$BAS/api/auth/registrera" -H 'Content-Type: application/json' \
|
|
-d '{"epost":"anna@a.se","losenord":"hemligt123","namn":"Anna","organisation":"Verkstad A"}')
|
|
TOKEN_A=$(echo "$SVAR" | falt .token); kontroll "registrering ger admin" "$(echo "$SVAR" | falt .roll)" "admin"
|
|
kontroll "organisationsnamn" "$(echo "$SVAR" | falt .organisation)" "Verkstad A"
|
|
|
|
# 2. Skapa ärende + händelser (inkl. internt kategoribyte)
|
|
curl -s -X POST "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"id":"arende-test1","nummer":1,"skapad":"2026-08-03T08:00:00Z","delningskod":"delkod123"}' >/dev/null
|
|
curl -s -X POST "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"handelser":[
|
|
{"id":"h1","tidpunkt":"2026-08-03T08:01:00Z","anvandare":"Anna","handelse":{"typ":"felbeskrivning","text":"Startar inte"}},
|
|
{"id":"h2","tidpunkt":"2026-08-03T08:02:00Z","anvandare":"Anna","handelse":{"typ":"kategori_byte","kategori":"provkorning"}}
|
|
]}' >/dev/null
|
|
ANTAL=$(curl -s "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_A" | falt .handelser.length)
|
|
kontroll "händelser sparade" "$ANTAL" "2"
|
|
|
|
# 3. Idempotens: samma push igen ändrar ingenting
|
|
curl -s -X POST "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"handelser":[{"id":"h1","tidpunkt":"2026-08-03T08:01:00Z","anvandare":"Anna","handelse":{"typ":"felbeskrivning","text":"ÄNDRAD"}}]}' >/dev/null
|
|
TEXT=$(curl -s "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_A" | falt '.handelser[0].handelse.text')
|
|
kontroll "händelser skrivs aldrig över" "$TEXT" "Startar inte"
|
|
|
|
# 4. Append-only-triggern stoppar direkta ändringar i databasen
|
|
if PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-qc "update felsokning_handelser set anvandare='hackad' where id='h1'" 2>/dev/null; then
|
|
echo "✗ append-only-trigger saknas"; exit 1
|
|
else
|
|
echo "✓ databastriggern avvisar update"
|
|
fi
|
|
|
|
# 5. Organisationsisolering: org B ser inte org A:s ärenden
|
|
TOKEN_B=$(curl -s -X POST "$BAS/api/auth/registrera" -H 'Content-Type: application/json' \
|
|
-d '{"epost":"bo@b.se","losenord":"hemligt123","namn":"Bo","organisation":"Verkstad B"}' | falt .token)
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_B")
|
|
kontroll "org B nekas org A:s händelser" "$KOD" "404"
|
|
ANTAL_B=$(curl -s "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_B" | falt .arenden.length)
|
|
kontroll "org B:s ärendelista är tom" "$ANTAL_B" "0"
|
|
|
|
# 6. Publik delning: utan inloggning, interna poster filtrerade
|
|
DELAT=$(curl -s "$BAS/api/delad/delkod123")
|
|
kontroll "delning nås utan inloggning" "$(echo "$DELAT" | falt .handelser.length)" "1"
|
|
kontroll "kategoribyte filtreras ur delning" "$(echo "$DELAT" | falt '.handelser[0].handelse.typ')" "felbeskrivning"
|
|
|
|
# 7. Rollstyrning: admin skapar tekniker; tekniker får inte hantera användare
|
|
curl -s -X POST "$BAS/api/anvandare" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"epost":"johan@a.se","losenord":"hemligt123","namn":"Johan","roll":"tekniker"}' >/dev/null
|
|
TOKEN_J=$(curl -s -X POST "$BAS/api/auth/logga-in" -H 'Content-Type: application/json' \
|
|
-d '{"epost":"johan@a.se","losenord":"hemligt123"}' | falt .token)
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/anvandare" -H "Authorization: Bearer $TOKEN_J" \
|
|
-H 'Content-Type: application/json' -d '{"epost":"x@a.se","losenord":"hemligt123","namn":"X","roll":"admin"}')
|
|
kontroll "tekniker nekas användarhantering" "$KOD" "403"
|
|
ANTAL_ANV=$(curl -s "$BAS/api/anvandare" -H "Authorization: Bearer $TOKEN_A" | falt .anvandare.length)
|
|
kontroll "org A har två användare" "$ANTAL_ANV" "2"
|
|
|
|
# 8. Tekniker i org A når ärendet (delad arbetsyta inom organisationen)
|
|
ANTAL=$(curl -s "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_J" | falt .handelser.length)
|
|
kontroll "tekniker i samma org når ärendet" "$ANTAL" "2"
|
|
|
|
# 9. Organisationsöversikten: tekniker nekas; arbetsledare får status ur loggen
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/oversikt" -H "Authorization: Bearer $TOKEN_J")
|
|
kontroll "tekniker nekas översikten" "$KOD" "403"
|
|
curl -s -X POST "$BAS/api/anvandare" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"epost":"lisa@a.se","losenord":"hemligt123","namn":"Lisa","roll":"arbetsledare"}' >/dev/null
|
|
TOKEN_L=$(curl -s -X POST "$BAS/api/auth/logga-in" -H 'Content-Type: application/json' \
|
|
-d '{"epost":"lisa@a.se","losenord":"hemligt123"}' | falt .token)
|
|
OVERSIKT=$(curl -s "$BAS/api/oversikt" -H "Authorization: Bearer $TOKEN_L")
|
|
kontroll "arbetsledaren ser organisationens ärenden" "$(echo "$OVERSIKT" | falt .arenden.length)" "1"
|
|
kontroll "översikten härleder felbeskrivning" "$(echo "$OVERSIKT" | falt '.arenden[0].felbeskrivning')" "Startar inte"
|
|
kontroll "översikten härleder status" "$(echo "$OVERSIKT" | falt '.arenden[0].avslutat')" "false"
|
|
kontroll "översikten räknar händelser" "$(echo "$OVERSIKT" | falt '.arenden[0].antal_handelser')" "2"
|
|
|
|
# 9b. Omfördelning: arbetsledaren listar användare och sätter ny ansvarig
|
|
ANTAL_ANV=$(curl -s "$BAS/api/anvandare" -H "Authorization: Bearer $TOKEN_L" | falt .anvandare.length)
|
|
kontroll "arbetsledaren kan lista användare" "$ANTAL_ANV" "3"
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/anvandare" -H "Authorization: Bearer $TOKEN_L" \
|
|
-H 'Content-Type: application/json' -d '{"epost":"y@a.se","losenord":"hemligt123","namn":"Y","roll":"tekniker"}')
|
|
kontroll "arbetsledaren nekas skapa användare" "$KOD" "403"
|
|
curl -s -X POST "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_L" -H 'Content-Type: application/json' \
|
|
-d '{"handelser":[{"id":"h-omf","tidpunkt":"2026-08-03T08:05:00Z","anvandare":"Lisa","handelse":{"typ":"ansvarig_satt","ansvarig":"Johan"}}]}' >/dev/null
|
|
kontroll "översikten visar ny ansvarig" "$(curl -s "$BAS/api/oversikt" -H "Authorization: Bearer $TOKEN_L" | falt '.arenden[0].ansvarig')" "Johan"
|
|
|
|
# 9c. Organisationsinställningar: alla läser, bara admin ändrar
|
|
INST=$(curl -s "$BAS/api/organisation" -H "Authorization: Bearer $TOKEN_J")
|
|
kontroll "inloggad läser organisationen" "$(echo "$INST" | falt .namn)" "Verkstad A"
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/organisation/installningar" \
|
|
-H "Authorization: Bearer $TOKEN_J" -H 'Content-Type: application/json' \
|
|
-d '{"objekttyper":["Fordon"],"identifieringsmetoder":["VIN"]}')
|
|
kontroll "tekniker nekas ändra inställningar" "$KOD" "403"
|
|
curl -s -X POST "$BAS/api/organisation/installningar" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"objekttyper":["Fordon","Hydraulik"],"identifieringsmetoder":["VIN","Manuell inmatning"]}' >/dev/null
|
|
INST=$(curl -s "$BAS/api/organisation" -H "Authorization: Bearer $TOKEN_J")
|
|
kontroll "inställningarna gäller hela organisationen" "$(echo "$INST" | falt '.installningar.objekttyper.join(",")')" "Fordon,Hydraulik"
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/organisation/installningar" \
|
|
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"objekttyper":[],"identifieringsmetoder":["VIN"]}')
|
|
kontroll "tomma listor avvisas" "$KOD" "400"
|
|
|
|
# 10. Live Share-behörighetsnivåer: kund/partner/intern + återkallelse
|
|
curl -s -X POST "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"handelser":[
|
|
{"id":"h3","tidpunkt":"2026-08-03T08:03:00Z","anvandare":"Anna","handelse":{"typ":"hypotes","text":"Trasigt relä","niva":"lag"}},
|
|
{"id":"h4","tidpunkt":"2026-08-03T08:04:00Z","anvandare":"Anna","handelse":{"typ":"arbetsorder_skannad","falt":[{"id":"kund_namn","etikett":"Namn","varde":"Kalle Kund","konfidens":0.97}]}}
|
|
]}' >/dev/null
|
|
kontroll "kundkoden filtrerar hypoteser och arbetsorder" "$(curl -s "$BAS/api/delad/delkod123" | falt .handelser.length)" "1"
|
|
|
|
PARTNERKOD=$(curl -s -X POST "$BAS/api/arenden/arende-test1/delningar" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' -d '{"niva":"partner"}' | falt .kod)
|
|
PARTNER=$(curl -s "$BAS/api/delad/$PARTNERKOD")
|
|
kontroll "partnernivån visar hypoteser men inte arbetsordern" "$(echo "$PARTNER" | falt .handelser.length)" "2"
|
|
kontroll "partnernivån döljer kategoribyten" "$(echo "$PARTNER" | falt '.handelser.some(h=>h.handelse.typ==="kategori_byte")')" "false"
|
|
kontroll "nivån följer med svaret" "$(echo "$PARTNER" | falt .niva)" "partner"
|
|
|
|
INTERNKOD=$(curl -s -X POST "$BAS/api/arenden/arende-test1/delningar" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' -d '{"niva":"intern"}' | falt .kod)
|
|
kontroll "internnivån visar allt" "$(curl -s "$BAS/api/delad/$INTERNKOD" | falt .handelser.length)" "5"
|
|
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/arenden/arende-test1/delningar" \
|
|
-H "Authorization: Bearer $TOKEN_B" -H 'Content-Type: application/json' -d '{"niva":"intern"}')
|
|
kontroll "org B kan inte skapa delning av org A:s ärende" "$KOD" "404"
|
|
|
|
curl -s -X POST "$BAS/api/delningar/$PARTNERKOD/aterkalla" -H "Authorization: Bearer $TOKEN_A" >/dev/null
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/delad/$PARTNERKOD")
|
|
kontroll "återkallad delning ger 404" "$KOD" "404"
|
|
|
|
# 10a. Fordonshistorik och felorsaksstatistik
|
|
curl -s -X POST "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"id":"arende-test2","nummer":2,"skapad":"2026-08-03T09:00:00Z"}' >/dev/null
|
|
curl -s -X POST "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"id":"arende-test3","nummer":3,"skapad":"2026-08-03T10:00:00Z"}' >/dev/null
|
|
curl -s -X POST "$BAS/api/arenden/arende-test2/handelser" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"handelser":[{"id":"f1","tidpunkt":"2026-08-03T09:01:00Z","anvandare":"Anna","handelse":{"typ":"objekt_identifierat","objekt":{"typ":"Personbil","identifierare":"XYZ999","identifieringsmetod":"Regnr","beskrivning":"VW Golf 2023"}}}]}' >/dev/null
|
|
curl -s -X POST "$BAS/api/arenden/arende-test3/handelser" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"handelser":[
|
|
{"id":"f2","tidpunkt":"2026-08-03T10:01:00Z","anvandare":"Johan","handelse":{"typ":"objekt_identifierat","objekt":{"typ":"Personbil","identifierare":"xyz999","identifieringsmetod":"Regnr","beskrivning":"VW Golf 2023"}}},
|
|
{"id":"f3","tidpunkt":"2026-08-03T10:02:00Z","anvandare":"Johan","handelse":{"typ":"felorsak","avvikelse":"Vattenpumpen läcker vid axeltätningen.","orsaker":["Normalt slitage","Ålder"],"underlag":["Foto"],"sakerhet":"hog","atgard":"Byt vattenpump."}}
|
|
]}' >/dev/null
|
|
# Det blindade fordonsindexet är en HÄRLEDD kolumn på ärenderaden, och
|
|
# den skrivs efter att ärendet skapats. En trigger som förbjöd all update
|
|
# gjorde den skrivningen omöjlig och lät hela synken falla med 500 —
|
|
# osynligt här, eftersom svaret kastades bort. Statuskoden granskas nu.
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/arenden/arende-test2/handelser" \
|
|
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"handelser":[{"id":"f1b","tidpunkt":"2026-08-03T09:03:00Z","anvandare":"Anna","handelse":{"typ":"observation","text":"Fortsatt läckage vid axeltätningen."}}]}')
|
|
kontroll "synk av händelser lyckas" "$KOD" "200"
|
|
|
|
# Skyddet är kolumnvis, inte bortplockat: identiteten är fortfarande låst.
|
|
if PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-qc "update felsokning_arenden set nummer = 999 where id='arende-test2'" 2>/dev/null; then
|
|
echo "✗ ärendets identitet går att ändra"; exit 1
|
|
else
|
|
echo "✓ ärendets identitet är fortfarande oföränderlig"
|
|
fi
|
|
if PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-qc "delete from felsokning_arenden where id='arende-test2'" 2>/dev/null; then
|
|
echo "✗ ärenden går att radera"; exit 1
|
|
else
|
|
echo "✓ ärenden går fortfarande inte att radera"
|
|
fi
|
|
|
|
HIST=$(curl -s "$BAS/api/fordon/XYZ999/historik" -H "Authorization: Bearer $TOKEN_J")
|
|
kontroll "fordonshistoriken hittar båda ärendena (case-okänsligt)" "$(echo "$HIST" | falt .arenden.length)" "2"
|
|
kontroll "historiken bär felorsakerna" "$(echo "$HIST" | falt '.arenden.flatMap(a=>a.felorsaker).length')" "1"
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/fordon/XYZ999/historik" -H "Authorization: Bearer $TOKEN_B")
|
|
kontroll "org B ser inte org A:s fordonshistorik" "$(curl -s "$BAS/api/fordon/XYZ999/historik" -H "Authorization: Bearer $TOKEN_B" | falt .arenden.length)" "0"
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/statistik/felorsaker" -H "Authorization: Bearer $TOKEN_J")
|
|
kontroll "tekniker nekas felorsaksstatistiken" "$KOD" "403"
|
|
STAT=$(curl -s "$BAS/api/statistik/felorsaker" -H "Authorization: Bearer $TOKEN_L")
|
|
kontroll "statistiken räknar orsakskategorier" "$(echo "$STAT" | falt .orsaker.length)" "2"
|
|
kontroll "statistiken är organisationsknuten data" "$(echo "$STAT" | falt '.orsaker[0].antal')" "1"
|
|
|
|
# 10b. ECM Knowledge Library: regelpaketet serveras till inloggade klienter
|
|
REGLER=$(curl -s "$BAS/api/ecm/regler" -H "Authorization: Bearer $TOKEN_J")
|
|
kontroll "regelpaketet serveras" "$(echo "$REGLER" | falt .version)" "2.0"
|
|
kontroll "regelpaketet innehåller garantiregler" "$(echo "$REGLER" | falt '.arendetypRegler.Garanti.length')" "3"
|
|
|
|
# 10c. Publikt kundgodkännande — den enda skrivande publika vägen
|
|
# Ärendets ursprungliga delningskod saknar registrerad nivå och får
|
|
# därför aldrig svara (bara riktiga kundlänkar duger).
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/delkod123/beslut" \
|
|
-H 'Content-Type: application/json' -d '{"beslut":"godkant"}')
|
|
kontroll "legacy-delningskod kan inte svara" "$KOD" "404"
|
|
|
|
# Kundlänk utan åtgärdsförslag: inget att svara på
|
|
KUNDKOD=$(curl -s -X POST "$BAS/api/arenden/arende-test1/delningar" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' -d '{"niva":"kund"}' | falt .kod)
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/$KUNDKOD/beslut" \
|
|
-H 'Content-Type: application/json' -d '{"beslut":"godkant"}')
|
|
kontroll "beslut utan åtgärdsförslag avvisas" "$KOD" "409"
|
|
|
|
# Lägg in ett åtgärdsförslag
|
|
curl -s -X POST "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"handelser":[{"id":"h-forslag","tidpunkt":"2026-08-03T08:06:00Z","anvandare":"Anna","handelse":{"typ":"atgardsforslag","beskrivning":"Byt reläet i kupémodulen.","uppskattadKostnad":"1 450 kr"}}]}' >/dev/null
|
|
|
|
# Partnerlänk får INTE svara åt kunden
|
|
PKOD=$(curl -s -X POST "$BAS/api/arenden/arende-test1/delningar" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' -d '{"niva":"partner"}' | falt .kod)
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/$PKOD/beslut" \
|
|
-H 'Content-Type: application/json' -d '{"beslut":"godkant"}')
|
|
kontroll "partnerlänk kan inte svara åt kunden" "$KOD" "404"
|
|
|
|
# Ogiltigt beslutsvärde avvisas
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/$KUNDKOD/beslut" \
|
|
-H 'Content-Type: application/json' -d '{"beslut":"kanske"}')
|
|
kontroll "ogiltigt beslutsvärde avvisas" "$KOD" "400"
|
|
|
|
# Kunden godkänner via sin länk
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/$KUNDKOD/beslut" \
|
|
-H 'Content-Type: application/json' -d '{"beslut":"godkant","kommentar":"Kör på."}')
|
|
kontroll "kunden kan godkänna via sin länk" "$KOD" "200"
|
|
BESLUT=$(curl -s "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_A" \
|
|
| falt '.handelser.filter(h=>h.handelse.typ==="kundbeslut").map(h=>h.handelse.kanal+"/"+h.anvandare).join(",")')
|
|
kontroll "beskedet loggas med kanal och avsändare" "$BESLUT" "Delningslänk/Kund via delningslänk"
|
|
|
|
# Ett beslut per ärende — svaret kan inte ändras i efterhand
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/$KUNDKOD/beslut" \
|
|
-H 'Content-Type: application/json' -d '{"beslut":"avbojt"}')
|
|
kontroll "beskedet kan inte ändras i efterhand" "$KOD" "409"
|
|
|
|
# Återkallad delning kan inte svara
|
|
curl -s -X POST "$BAS/api/delningar/$KUNDKOD/aterkalla" -H "Authorization: Bearer $TOKEN_A" >/dev/null
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/$KUNDKOD/beslut" \
|
|
-H 'Content-Type: application/json' -d '{"beslut":"godkant"}')
|
|
kontroll "återkallad länk kan inte svara" "$KOD" "404"
|
|
|
|
# Takt-begränsning slår till efter upprepade försök
|
|
NYKOD=$(curl -s -X POST "$BAS/api/arenden/arende-test1/delningar" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' -d '{"niva":"kund"}' | falt .kod)
|
|
SISTA=""
|
|
for i in 1 2 3 4 5 6 7; do
|
|
SISTA=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/$NYKOD/beslut" \
|
|
-H 'Content-Type: application/json' -d '{"beslut":"godkant"}')
|
|
done
|
|
kontroll "takt-begränsning stoppar upprepade försök" "$SISTA" "429"
|
|
|
|
# 10d. Märkesspecifika kopplingar: kundens egna credentials
|
|
# Registret är läsbart för alla inloggade (inställningssidan behöver
|
|
# veta vilka leverantörer som finns) men innehåller inga uppgifter.
|
|
LEV=$(curl -s "$BAS/api/integrationer/leverantorer" -H "Authorization: Bearer $TOKEN_J")
|
|
kontroll "leverantörsregistret är läsbart för tekniker" \
|
|
"$(echo "$LEV" | falt '.leverantorer.some(l=>l.id==="generisk_vin")')" "true"
|
|
kontroll "registret pekar ut hemliga fält" \
|
|
"$(echo "$LEV" | falt '.leverantorer.every(l=>l.falt.some(f=>f.hemlig===true))')" "true"
|
|
|
|
# Uppgifterna är administratörens ensak
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/integrationer" -H "Authorization: Bearer $TOKEN_J")
|
|
kontroll "tekniker ser inte kopplingarnas uppgifter" "$KOD" "403"
|
|
|
|
# Utan konfigurerad krypteringsnyckel sparas ingenting — fail closed
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/integrationer" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"leverantor":"generisk_vin","uppgifter":{"bas_url":"https://x.se/{vin}","api_nyckel":"k"}}')
|
|
kontroll "utan krypteringsnyckel sparas inga uppgifter" "$KOD" "503"
|
|
|
|
# Starta om tjänsten med krypteringsnyckel konfigurerad
|
|
kill "$SERVER_PID" 2>/dev/null || true
|
|
wait "$SERVER_PID" 2>/dev/null || true
|
|
DATABASE_URL="postgresql://plattform:test@127.0.0.1:$PGPORT/felsokning" \
|
|
JWT_SECRET=integrationshemlighet PORT=$APPPORT \
|
|
INTEGRATION_NYCKEL=$(node -pe "require('crypto').randomBytes(32).toString('hex')") \
|
|
node server.mjs &
|
|
SERVER_PID=$!
|
|
sleep 1
|
|
|
|
# Okänd leverantör och ofullständiga uppgifter avvisas
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/integrationer" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' -d '{"leverantor":"hittepa","uppgifter":{"a":"b"}}')
|
|
kontroll "okänd leverantör avvisas" "$KOD" "400"
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/integrationer" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' -d '{"leverantor":"generisk_vin","uppgifter":{"bas_url":"https://x.se/{vin}"}}')
|
|
kontroll "ofullständiga uppgifter avvisas" "$KOD" "400"
|
|
|
|
# Administratören sparar organisationens egna credentials
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/integrationer" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"leverantor":"generisk_vin","uppgifter":{"bas_url":"http://127.0.0.1:9/vin/{vin}","api_nyckel":"sk-verkstad-123456"}}')
|
|
kontroll "administratören kan spara credentials" "$KOD" "200"
|
|
|
|
# Hemligheten lämnar aldrig servern i klartext
|
|
INT=$(curl -s "$BAS/api/integrationer" -H "Authorization: Bearer $TOKEN_A")
|
|
kontroll "krypteringen är konfigurerad" "$(echo "$INT" | falt .krypteringKonfigurerad)" "true"
|
|
kontroll "hemligt fält maskeras i svaret" "$(echo "$INT" | falt '.integrationer[0].uppgifter.api_nyckel')" "••••3456"
|
|
kontroll "öppet fält visas som det är" "$(echo "$INT" | falt '.integrationer[0].uppgifter.bas_url')" "http://127.0.0.1:9/vin/{vin}"
|
|
|
|
# … och ligger krypterad i databasen
|
|
RAD=$(PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-tAc "select uppgifter_krypt from integrationer where leverantor='generisk_vin'")
|
|
case "$RAD" in
|
|
*sk-verkstad-123456*) echo "✗ uppgifterna ligger i klartext i databasen"; exit 1 ;;
|
|
*) echo "✓ uppgifterna ligger krypterade i databasen" ;;
|
|
esac
|
|
|
|
# Uppslag: identifieraren valideras, okonfigurerad koppling ger 404
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/integrationer/generisk_vin/uppslag" \
|
|
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' -d '{"identifierare":"x"}')
|
|
kontroll "ogiltig identifierare avvisas" "$KOD" "400"
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/integrationer/volvo_vida/uppslag" \
|
|
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' -d '{"identifierare":"YV1DZ8256F2123456"}')
|
|
kontroll "okonfigurerad koppling ger 404" "$KOD" "404"
|
|
|
|
# Bas-URL:en pekar inåt (127.0.0.1) — uppslaget får inte bli en väg in i
|
|
# klustret. Anropet ska stoppas innan det görs och rapporteras ärligt.
|
|
SVAR=$(curl -s -X POST "$BAS/api/integrationer/generisk_vin/uppslag" \
|
|
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' -d '{"identifierare":"YV1DZ8256F2123456"}')
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/integrationer/generisk_vin/uppslag" \
|
|
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' -d '{"identifierare":"YV1DZ8256F2123456"}')
|
|
kontroll "uppslag mot intern adress avvisas (502)" "$KOD" "502"
|
|
kontroll "felet säger varför" "$(echo "$SVAR" | falt '.error.includes("intern adress")')" "true"
|
|
STATUS=$(curl -s "$BAS/api/integrationer" -H "Authorization: Bearer $TOKEN_A" \
|
|
| falt '.integrationer[0].senaste_status.slice(0,3)')
|
|
kontroll "senaste testresultat sparas på kopplingen" "$STATUS" "fel"
|
|
|
|
# Kopplingar är organisationsknutna
|
|
ANTAL_B=$(curl -s "$BAS/api/integrationer" -H "Authorization: Bearer $TOKEN_B" | falt .integrationer.length)
|
|
kontroll "org B ser inte org A:s kopplingar" "$ANTAL_B" "0"
|
|
|
|
# Borttagning
|
|
curl -s -X DELETE "$BAS/api/integrationer/generisk_vin" -H "Authorization: Bearer $TOKEN_A" >/dev/null
|
|
ANTAL=$(curl -s "$BAS/api/integrationer" -H "Authorization: Bearer $TOKEN_A" | falt .integrationer.length)
|
|
kontroll "kopplingen kan tas bort" "$ANTAL" "0"
|
|
|
|
# 10e. Kontospärr, återkallelse och takt-begränsning på inloggning
|
|
# Skapa en tekniker att stänga av.
|
|
SVAR=$(curl -s -X POST "$BAS/api/anvandare" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"epost":"karin@a.se","losenord":"hemligt123","namn":"Karin","roll":"tekniker"}')
|
|
KARIN_ID=$(echo "$SVAR" | falt .id)
|
|
kontroll "ny användare är aktiv" "$(echo "$SVAR" | falt .aktiv)" "true"
|
|
|
|
TOKEN_K=$(curl -s -X POST "$BAS/api/auth/logga-in" -H 'Content-Type: application/json' \
|
|
-d '{"epost":"karin@a.se","losenord":"hemligt123"}' | falt .token)
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_K")
|
|
kontroll "teknikern kommer in" "$KOD" "200"
|
|
|
|
# Tekniker får inte stänga av någon
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/anvandare/$KARIN_ID/avaktivera" \
|
|
-H "Authorization: Bearer $TOKEN_K")
|
|
kontroll "tekniker kan inte stänga av konton" "$KOD" "403"
|
|
|
|
# Administratören kan inte stänga av sig själv
|
|
ANNA_ID=$(curl -s "$BAS/api/anvandare" -H "Authorization: Bearer $TOKEN_A" \
|
|
| falt '.anvandare.find(a=>a.epost==="anna@a.se").id')
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/anvandare/$ANNA_ID/avaktivera" \
|
|
-H "Authorization: Bearer $TOKEN_A")
|
|
kontroll "admin kan inte stänga av sig själv" "$KOD" "400"
|
|
|
|
# Org B kan inte röra org A:s användare
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/anvandare/$KARIN_ID/avaktivera" \
|
|
-H "Authorization: Bearer $TOKEN_B")
|
|
kontroll "org B kan inte stänga av org A:s användare" "$KOD" "404"
|
|
|
|
# Avstängningen gäller OMEDELBART för redan utfärdad token
|
|
curl -s -X POST "$BAS/api/anvandare/$KARIN_ID/avaktivera" -H "Authorization: Bearer $TOKEN_A" >/dev/null
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_K")
|
|
kontroll "utfärdad token slutar gälla direkt vid avstängning" "$KOD" "401"
|
|
|
|
# … och inloggning stängs
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/auth/logga-in" -H 'Content-Type: application/json' \
|
|
-d '{"epost":"karin@a.se","losenord":"hemligt123"}')
|
|
kontroll "avstängt konto kan inte logga in" "$KOD" "403"
|
|
|
|
# Öppnas kontot igen fungerar inloggning, men den gamla token är död för gott
|
|
curl -s -X POST "$BAS/api/anvandare/$KARIN_ID/aktivera" -H "Authorization: Bearer $TOKEN_A" >/dev/null
|
|
TOKEN_K2=$(curl -s -X POST "$BAS/api/auth/logga-in" -H 'Content-Type: application/json' \
|
|
-d '{"epost":"karin@a.se","losenord":"hemligt123"}' | falt .token)
|
|
kontroll "återöppnat konto kan logga in" "$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_K2")" "200"
|
|
kontroll "den återkallade token förblir ogiltig" "$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_K")" "401"
|
|
|
|
# Logga ut på alla enheter återkallar den egna sessionen
|
|
curl -s -X POST "$BAS/api/auth/logga-ut-alla" -H "Authorization: Bearer $TOKEN_K2" >/dev/null
|
|
kontroll "logga-ut-alla dödar den egna token" "$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_K2")" "401"
|
|
|
|
# Takt-begränsning: efter tio misslyckade försök spärras kontot en stund
|
|
SISTA=""
|
|
for i in $(seq 1 11); do
|
|
SISTA=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/auth/logga-in" \
|
|
-H 'Content-Type: application/json' -d '{"epost":"karin@a.se","losenord":"fel-losenord"}')
|
|
done
|
|
kontroll "upprepade misslyckade inloggningar spärras" "$SISTA" "429"
|
|
# Spärren gäller kontot även med RÄTT lösenord — annars vore den meningslös
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/auth/logga-in" \
|
|
-H 'Content-Type: application/json' -d '{"epost":"karin@a.se","losenord":"hemligt123"}')
|
|
kontroll "spärren gäller även rätt lösenord" "$KOD" "429"
|
|
# Ett annat konto påverkas inte av spärren på det första
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/auth/logga-in" \
|
|
-H 'Content-Type: application/json' -d '{"epost":"anna@a.se","losenord":"hemligt123"}')
|
|
kontroll "andra konton påverkas inte" "$KOD" "200"
|
|
|
|
# 10f. Bilagor: innehållet ligger utanför händelsen, hashen i loggen
|
|
PNG=$(mktemp); printf '\x89PNG\r\n\x1a\nTESTBILD-1' > "$PNG"
|
|
SVAR=$(curl -s -X POST "$BAS/api/arenden/arende-test1/bilagor" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: image/png' --data-binary "@$PNG")
|
|
BIL_ID=$(echo "$SVAR" | falt .id)
|
|
BIL_HASH=$(echo "$SVAR" | falt .hash)
|
|
kontroll "bilagan får en hash" "${#BIL_HASH}" "64"
|
|
kontroll "hashen är innehållets" "$BIL_HASH" "$(sha256sum "$PNG" | cut -d' ' -f1)"
|
|
|
|
# Samma innehåll igen: ny referens, men bara ett lagrat innehåll
|
|
SVAR2=$(curl -s -X POST "$BAS/api/arenden/arende-test1/bilagor" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: image/png' --data-binary "@$PNG")
|
|
kontroll "identiskt innehåll ger samma hash" "$(echo "$SVAR2" | falt .hash)" "$BIL_HASH"
|
|
ANTAL_INNEHALL=$(PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-tAc "select count(*) from bilage_innehall where hash='$BIL_HASH'")
|
|
kontroll "innehållsadresserat — lagras en gång" "$ANTAL_INNEHALL" "1"
|
|
|
|
# Hämtning ger tillbaka exakt samma bytes
|
|
curl -s "$BAS/api/bilagor/$BIL_ID" -H "Authorization: Bearer $TOKEN_A" -o /tmp/hamtad.png
|
|
kontroll "hämtat innehåll är identiskt" "$(sha256sum /tmp/hamtad.png | cut -d' ' -f1)" "$BIL_HASH"
|
|
|
|
# Fel mediatyp och tom kropp avvisas
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/arenden/arende-test1/bilagor" \
|
|
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/pdf' --data-binary "@$PNG")
|
|
kontroll "endast bilder och video tas emot" "$KOD" "415"
|
|
|
|
# Organisationsgränsen gäller bilagor
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/bilagor/$BIL_ID" -H "Authorization: Bearer $TOKEN_B")
|
|
kontroll "org B kommer inte åt org A:s bilaga" "$KOD" "404"
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/arenden/arende-test1/bilagor" \
|
|
-H "Authorization: Bearer $TOKEN_B" -H 'Content-Type: image/png' --data-binary "@$PNG")
|
|
kontroll "org B kan inte ladda upp till org A:s ärende" "$KOD" "404"
|
|
|
|
# Manipulerat innehåll upptäcks: hashen i loggen är facit
|
|
PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-qc "update bilage_innehall set data = decode('4d414e4950554c45524154', 'hex') where hash='$BIL_HASH'"
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/bilagor/$BIL_ID" -H "Authorization: Bearer $TOKEN_A")
|
|
kontroll "utbytt innehåll upptäcks och lämnas inte ut" "$KOD" "409"
|
|
PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-qc "update bilage_innehall set data = pg_read_binary_file('$PNG') where hash='$BIL_HASH'" 2>/dev/null \
|
|
|| PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-qc "delete from bilage_innehall where hash='$BIL_HASH'"
|
|
|
|
# Delningsfiltret gäller även bilagor: en bilaga som hör till en intern
|
|
# händelsetyp får inte hämtas via kundlänken.
|
|
BIL2=$(curl -s -X POST "$BAS/api/arenden/arende-test1/bilagor" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: image/png' --data-binary "@$PNG" | falt .id)
|
|
BIL3=$(curl -s -X POST "$BAS/api/arenden/arende-test1/bilagor" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: image/png' --data-binary "@$PNG" | falt .id)
|
|
curl -s -X POST "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d "{\"handelser\":[
|
|
{\"id\":\"h-foto\",\"tidpunkt\":\"2026-08-03T08:07:00Z\",\"anvandare\":\"Anna\",\"handelse\":{\"typ\":\"foto\",\"beskrivning\":\"Hjul\",\"bilagaId\":\"$BIL2\"}},
|
|
{\"id\":\"h-ao\",\"tidpunkt\":\"2026-08-03T08:08:00Z\",\"anvandare\":\"Anna\",\"handelse\":{\"typ\":\"arbetsorder_skannad\",\"falt\":[],\"bilagaId\":\"$BIL3\"}}
|
|
]}" >/dev/null
|
|
NYKUND=$(curl -s -X POST "$BAS/api/arenden/arende-test1/delningar" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' -d '{"niva":"kund"}' | falt .kod)
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/delad/$NYKUND/bilagor/$BIL2")
|
|
kontroll "kunden når bilagan till ett foto" "$KOD" "200"
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/delad/$NYKUND/bilagor/$BIL3")
|
|
kontroll "kunden når INTE arbetsorderbilden" "$KOD" "404"
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/delad/$NYKUND/bilagor/$BIL_ID")
|
|
kontroll "bilaga utan händelse lämnas inte ut publikt" "$KOD" "404"
|
|
rm -f "$PNG" /tmp/hamtad.png
|
|
|
|
# 11. API-first: OpenAPI-specen serveras live, utan inloggning
|
|
SPEC=$(curl -s "$BAS/api/openapi.yaml")
|
|
case "$SPEC" in
|
|
"openapi: 3.0.3"*) echo "✓ OpenAPI-specen serveras på /api/openapi.yaml" ;;
|
|
*) echo "✗ OpenAPI-specen saknas"; exit 1 ;;
|
|
esac
|
|
|
|
# 12. Fakturering (ALVA-PROC-0001)
|
|
#
|
|
# Två egenskaper avgör om det här är ett bokföringsunderlag eller en
|
|
# anteckning: att beloppet HÄRLEDS ur organisationens tillstånd i stället
|
|
# för att tas emot, och att en utfärdad faktura aldrig ändras. Bägge
|
|
# prövas nedan, tillsammans med gränsen mellan kund och utfärdare.
|
|
|
|
ORG_A=$(PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-tAc "select id from organisationer where namn='Verkstad A'")
|
|
PERIOD='{"fran":"2026-01-01","till":"2026-12-31"}'
|
|
|
|
# Utan nyckel i miljön kan ingen faktura utfärdas — fallerar stängt.
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/fakturor" -H 'Content-Type: application/json' \
|
|
-H 'X-Fakturering: gissning' -d "{\"organisation_id\":\"$ORG_A\",\"period\":$PERIOD}")
|
|
kontroll "utan konfigurerad utfärdarnyckel utfärdas ingenting" "$KOD" "403"
|
|
|
|
# Starta om med utfärdarnyckel konfigurerad
|
|
kill "$SERVER_PID" 2>/dev/null || true
|
|
wait "$SERVER_PID" 2>/dev/null || true
|
|
DATABASE_URL="postgresql://plattform:test@127.0.0.1:$PGPORT/felsokning" \
|
|
JWT_SECRET=integrationshemlighet PORT=$APPPORT \
|
|
INTEGRATION_NYCKEL=$(node -pe "require('crypto').randomBytes(32).toString('hex')") \
|
|
FAKTURERING_NYCKEL=utfardarnyckel \
|
|
node server.mjs &
|
|
SERVER_PID=$!
|
|
sleep 1
|
|
|
|
# Kundens administratör är inte utfärdare, hur inloggad den än är.
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/fakturor" \
|
|
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d "{\"organisation_id\":\"$ORG_A\",\"period\":$PERIOD}")
|
|
kontroll "kundens admin kan inte utfärda sin egen faktura" "$KOD" "403"
|
|
|
|
# Fel nyckel duger inte heller.
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/fakturor" -H 'Content-Type: application/json' \
|
|
-H 'X-Fakturering: fel-nyckel-samma-langd' -d "{\"organisation_id\":\"$ORG_A\",\"period\":$PERIOD}")
|
|
kontroll "fel utfärdarnyckel avvisas" "$KOD" "403"
|
|
|
|
# Beloppet tas aldrig emot. Ett anrop som försöker sätta det avvisas
|
|
# högt i stället för att tigas ihjäl.
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/fakturor" -H 'Content-Type: application/json' \
|
|
-H 'X-Fakturering: utfardarnyckel' \
|
|
-d "{\"organisation_id\":\"$ORG_A\",\"period\":$PERIOD,\"totalt\":1}")
|
|
kontroll "angivet belopp avvisas" "$KOD" "400"
|
|
|
|
# En bakvänd period ger annars noll månader och en faktura som SER
|
|
# rimlig ut — värre än ett avslag.
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/fakturor" -H 'Content-Type: application/json' \
|
|
-H 'X-Fakturering: utfardarnyckel' \
|
|
-d "{\"organisation_id\":\"$ORG_A\",\"period\":{\"fran\":\"2026-12-31\",\"till\":\"2026-01-01\"}}")
|
|
kontroll "bakvänd period avvisas" "$KOD" "400"
|
|
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/fakturor" -H 'Content-Type: application/json' \
|
|
-H 'X-Fakturering: utfardarnyckel' \
|
|
-d "{\"organisation_id\":\"00000000-0000-0000-0000-000000000000\",\"period\":$PERIOD}")
|
|
kontroll "okänd organisation avvisas" "$KOD" "404"
|
|
|
|
# Utfärda på riktigt.
|
|
FAKTURA=$(curl -s -X POST "$BAS/api/fakturor" -H 'Content-Type: application/json' \
|
|
-H 'X-Fakturering: utfardarnyckel' -d "{\"organisation_id\":\"$ORG_A\",\"period\":$PERIOD,\"utfardad\":\"2026-01-15\"}")
|
|
FAKTURA_ID=$(echo "$FAKTURA" | falt .id)
|
|
kontroll "beteckningen följer nomenklaturen" "$(echo "$FAKTURA" | falt .beteckning)" "ALVA-INV-0001"
|
|
kontroll "förfallodagen följer betalningsvillkoret" "$(echo "$FAKTURA" | falt .forfaller)" "2026-02-14"
|
|
|
|
# Kärnan: summan ska vara densamma som modulen räknar fram ur DATABASENS
|
|
# egna aktiva konton. Ett hårdkodat facit hade bara testat aritmetiken;
|
|
# det här testar att beloppet verkligen härleds ur tillståndet.
|
|
AKTIVA=$(PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-tAc "select count(*) from anvandare where organisation_id='$ORG_A' and aktiv")
|
|
VANTAT=$(node -e "
|
|
import('./fakturering.mjs').then(({ fakturera }) => {
|
|
const f = fakturera({ nummer: 1, org: { namn: 'Verkstad A', moduler: [] }, aktiva: $AKTIVA,
|
|
period: { fran: '2026-01-01', till: '2026-12-31' }, utfardad: '2026-01-15' });
|
|
console.log(f.totalt);
|
|
});
|
|
")
|
|
kontroll "summan härleds ur organisationens aktiva konton" "$(echo "$FAKTURA" | falt .totalt)" "$VANTAT"
|
|
|
|
# Kunden läser sina egna fakturor, och bara sina egna.
|
|
LISTA=$(curl -s "$BAS/api/fakturor" -H "Authorization: Bearer $TOKEN_A")
|
|
kontroll "kunden ser sin faktura" "$(echo "$LISTA" | falt .fakturor.length)" "1"
|
|
kontroll "statusen är utfärdad" "$(echo "$LISTA" | falt '.fakturor[0].status')" "utfardad"
|
|
kontroll "org B ser inga fakturor" "$(curl -s "$BAS/api/fakturor" -H "Authorization: Bearer $TOKEN_B" | falt .fakturor.length)" "0"
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/fakturor" -H "Authorization: Bearer $TOKEN_J")
|
|
kontroll "tekniker når inte kommersiella uppgifter" "$KOD" "403"
|
|
|
|
# Oföränderligheten är en egenskap i databasen, inte en ambition i koden.
|
|
if PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-qc "update fakturor set totalt = 0 where id='$FAKTURA_ID'" 2>/dev/null; then
|
|
echo "✗ fakturor går att ändra i databasen"; exit 1
|
|
else
|
|
echo "✓ databastriggern avvisar ändring av utfärdad faktura"
|
|
fi
|
|
|
|
# Betalning: registreras av en människa, med en referens som går att
|
|
# spåra, och skrivs som en händelse — aldrig som en uppdatering.
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/fakturor/$FAKTURA_ID/betald" \
|
|
-H 'X-Fakturering: utfardarnyckel' -H 'Content-Type: application/json' -d '{}')
|
|
kontroll "betalning utan referens avvisas" "$KOD" "400"
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/fakturor/$FAKTURA_ID/betald" \
|
|
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' -d '{"referens":"BG 123"}')
|
|
kontroll "kunden kan inte bokföra sin egen betalning" "$KOD" "403"
|
|
|
|
curl -s -X POST "$BAS/api/fakturor/$FAKTURA_ID/betald" -H 'X-Fakturering: utfardarnyckel' \
|
|
-H 'Content-Type: application/json' -d '{"referens":"BG 5402-1178","betaldatum":"2026-02-03"}' >/dev/null
|
|
STATUS=$(curl -s "$BAS/api/fakturor" -H "Authorization: Bearer $TOKEN_A" | falt '.fakturor[0].status')
|
|
kontroll "statusen härleds ur betalningshändelsen" "$STATUS" "betald"
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/fakturor/$FAKTURA_ID/betald" \
|
|
-H 'X-Fakturering: utfardarnyckel' -H 'Content-Type: application/json' -d '{"referens":"BG 5402-1178"}')
|
|
kontroll "en betald faktura betalas inte igen" "$KOD" "409"
|
|
|
|
# Rättelse: originalet rörs inte, en kreditfaktura pekar tillbaka.
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/fakturor/$FAKTURA_ID/kreditera" \
|
|
-H 'X-Fakturering: utfardarnyckel' -H 'Content-Type: application/json' -d '{"orsak":"fel"}')
|
|
kontroll "kreditering utan granskbart skäl avvisas" "$KOD" "400"
|
|
KREDIT=$(curl -s -X POST "$BAS/api/fakturor/$FAKTURA_ID/kreditera" -H 'X-Fakturering: utfardarnyckel' \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"orsak":"Antalet aktiva användare var felaktigt vid utfärdandet.","utfardad":"2026-02-10"}')
|
|
kontroll "kreditfakturan får nästa nummer" "$(echo "$KREDIT" | falt .beteckning)" "ALVA-INV-0002"
|
|
kontroll "kreditfakturan pekar tillbaka" "$(echo "$KREDIT" | falt .krediterar)" "ALVA-INV-0001"
|
|
kontroll "kreditfakturan har omvänt tecken" "$(echo "$KREDIT" | falt .totalt)" "-$VANTAT"
|
|
|
|
LISTA=$(curl -s "$BAS/api/fakturor" -H "Authorization: Bearer $TOKEN_A")
|
|
kontroll "originalet står kvar med sitt belopp" \
|
|
"$(echo "$LISTA" | falt '.fakturor.find(f => f.beteckning === "ALVA-INV-0001").totalt')" "$VANTAT"
|
|
kontroll "originalets status är nu krediterad" \
|
|
"$(echo "$LISTA" | falt '.fakturor.find(f => f.beteckning === "ALVA-INV-0001").status')" "krediterad"
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/fakturor/$FAKTURA_ID/kreditera" \
|
|
-H 'X-Fakturering: utfardarnyckel' -H 'Content-Type: application/json' \
|
|
-d '{"orsak":"Samma skäl en gång till, vilket inte ska gå."}')
|
|
kontroll "en krediterad faktura krediteras inte igen" "$KOD" "409"
|
|
|
|
# Nummerserien är utan luckor — annars är den inte ett underlag.
|
|
LUCKOR=$(PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-tAc "select (select max(nummer) from fakturor) - (select count(*) from fakturor)")
|
|
kontroll "nummerserien är utan luckor" "$LUCKOR" "0"
|
|
|
|
# 13. Motspelande hyresgäst (TÜV T-1)
|
|
#
|
|
# Revisionens huvudfynd: händelse-id sattes av klienten och var
|
|
# förutsägbart, primärnyckeln var global, och insert skedde med
|
|
# `on conflict do nothing`. Org B kunde ockupera id på SITT EGET ärende
|
|
# som org A snart skulle använda — och org A:s händelse föll tyst bort
|
|
# med statuskod 200.
|
|
#
|
|
# Ingen befintlig svit kunde se det: varje test kör en organisation, eller
|
|
# två isolerade. Ingen modellerar den enas skrivningar som indata till den
|
|
# andras misslyckande. Det gör den här.
|
|
|
|
IDN="mshhkoq9-0 mshhkoq9-1 mshhkoq9-2"
|
|
FORSTA=$(echo "$IDN" | cut -d' ' -f1)
|
|
|
|
curl -s -X POST "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_B" -H 'Content-Type: application/json' \
|
|
-d '{"id":"arende-motspel-b","nummer":90,"skapad":"2026-08-06T08:00:00Z"}' >/dev/null
|
|
curl -s -X POST "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"id":"arende-motspel-a","nummer":91,"skapad":"2026-08-06T08:00:00Z"}' >/dev/null
|
|
|
|
# Org B ockuperar id:na på sitt eget ärende.
|
|
POSTER=$(node -pe "
|
|
JSON.stringify({ handelser: '$IDN'.split(' ').map((id) => ({
|
|
id, tidpunkt: '2026-08-06T08:01:00Z', anvandare: 'Bo',
|
|
handelse: { typ: 'observation', text: 'Ockuperad.' } })) })
|
|
")
|
|
curl -s -X POST "$BAS/api/arenden/arende-motspel-b/handelser" -H "Authorization: Bearer $TOKEN_B" \
|
|
-H 'Content-Type: application/json' -d "$POSTER" >/dev/null
|
|
|
|
# Org A dokumenterar en felorsak med exakt samma id.
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/arenden/arende-motspel-a/handelser" \
|
|
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d "{\"handelser\":[{\"id\":\"$FORSTA\",\"tidpunkt\":\"2026-08-06T08:02:00Z\",\"anvandare\":\"Anna\",
|
|
\"handelse\":{\"typ\":\"felorsak\",\"avvikelse\":\"Bromsledning skavd mot chassi.\",
|
|
\"orsaker\":[\"Montagefel\"],\"underlag\":[\"Foto\"],\"sakerhet\":\"hog\",
|
|
\"atgard\":\"Byt ledning och satt ny klammer.\"}}]}")
|
|
kontroll "org A:s skrivning lyckas trots ockuperat id" "$KOD" "200"
|
|
ANTAL=$(curl -s "$BAS/api/arenden/arende-motspel-a/handelser" -H "Authorization: Bearer $TOKEN_A" | falt .handelser.length)
|
|
kontroll "org A:s felorsak finns i loggen" "$ANTAL" "1"
|
|
ANTAL_B=$(curl -s "$BAS/api/arenden/arende-motspel-b/handelser" -H "Authorization: Bearer $TOKEN_B" | falt .handelser.length)
|
|
kontroll "org B:s egna poster är orörda" "$ANTAL_B" "3"
|
|
|
|
# Samma id, samma innehåll, en gång till: en ofarlig omsändning ska förbli tyst.
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/arenden/arende-motspel-b/handelser" \
|
|
-H "Authorization: Bearer $TOKEN_B" -H 'Content-Type: application/json' -d "$POSTER")
|
|
kontroll "identisk omsandning ar fortfarande idempotent" "$KOD" "200"
|
|
|
|
# Samma id, ANNAT innehåll: ska falla högt i stället för att tigas ihjäl.
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/arenden/arende-motspel-b/handelser" \
|
|
-H "Authorization: Bearer $TOKEN_B" -H 'Content-Type: application/json' \
|
|
-d "{\"handelser\":[{\"id\":\"$FORSTA\",\"tidpunkt\":\"2026-08-06T08:03:00Z\",\"anvandare\":\"Bo\",
|
|
\"handelse\":{\"typ\":\"observation\",\"text\":\"NAGOT HELT ANNAT\"}}]}")
|
|
kontroll "samma id med annat innehall ger 409" "$KOD" "409"
|
|
ANTAL_B=$(curl -s "$BAS/api/arenden/arende-motspel-b/handelser" -H "Authorization: Bearer $TOKEN_B" | falt .handelser.length)
|
|
kontroll "ingenting skrevs vid 409" "$ANTAL_B" "3"
|
|
|
|
# Samma attack en niva upp: arende-id ar lika forutsagbart.
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' -d '{"id":"arende-motspel-b","nummer":92,"skapad":"2026-08-06T09:00:00Z"}')
|
|
kontroll "arende-id upptaget av annan org ger 409" "$KOD" "409"
|
|
|
|
# 14. Matkedjan slas upp i registret (TÜV T-2)
|
|
#
|
|
# Evidensgraden E4 kraver att det gar att visa VAD som matte och att
|
|
# instrumentet var kalibrerat. Bagge falten kom fran klienten och lagrades
|
|
# oprovade — en organisation utan ett enda registrerat matdon kunde skicka
|
|
# kalibrering 2099-12-31 och fa E4.
|
|
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/arenden/arende-motspel-a/handelser" \
|
|
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"handelser":[{"id":"m-falsk","tidpunkt":"2026-08-06T08:10:00Z","anvandare":"Anna",
|
|
"handelse":{"typ":"matvarde","beskrivning":"Kokpunkt","varde":"238","enhet":"C",
|
|
"matdonId":"finns-inte-i-registret","matdonBeteckning":"Kalibrerad testare 9000",
|
|
"matdonKalibreradTill":"2099-12-31"}}]}')
|
|
kontroll "okant matdon avvisas" "$KOD" "400"
|
|
|
|
# Ett riktigt matdon, med ett kalibreringsdatum registret ager.
|
|
MATDON=$(curl -s -X POST "$BAS/api/matdon" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"beteckning":"Bromsvatsketestare BT-2","serienummer":"SN-4471","kalibrerad_till":"2027-06-30"}' | falt .id)
|
|
curl -s -X POST "$BAS/api/arenden/arende-motspel-a/handelser" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' \
|
|
-d "{\"handelser\":[{\"id\":\"m-akta\",\"tidpunkt\":\"2026-08-06T08:11:00Z\",\"anvandare\":\"Anna\",
|
|
\"handelse\":{\"typ\":\"matvarde\",\"beskrivning\":\"Kokpunkt\",\"varde\":\"238\",\"enhet\":\"C\",
|
|
\"matdonId\":\"$MATDON\",\"matdonBeteckning\":\"PAHITTAD BETECKNING\",
|
|
\"matdonKalibreradTill\":\"2099-12-31\"}}]}" >/dev/null
|
|
|
|
HAMTAT=$(curl -s "$BAS/api/arenden/arende-motspel-a/handelser" -H "Authorization: Bearer $TOKEN_A")
|
|
kontroll "beteckningen kommer fran registret" \
|
|
"$(echo "$HAMTAT" | falt '.handelser.find(h=>h.id==="m-akta").handelse.matdonBeteckning')" "Bromsvatsketestare BT-2"
|
|
kontroll "kalibreringen kommer fran registret" \
|
|
"$(echo "$HAMTAT" | falt '.handelser.find(h=>h.id==="m-akta").handelse.matdonKalibreradTill')" "2027-06-30"
|
|
|
|
# Ett matvarde utan matdon far inte bara pasta en beteckning heller.
|
|
curl -s -X POST "$BAS/api/arenden/arende-motspel-a/handelser" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"handelser":[{"id":"m-utan","tidpunkt":"2026-08-06T08:12:00Z","anvandare":"Anna",
|
|
"handelse":{"typ":"matvarde","beskrivning":"Tryck","varde":"2.4","enhet":"bar",
|
|
"matdonBeteckning":"PASTADD","matdonKalibreradTill":"2099-12-31"}}]}' >/dev/null
|
|
HAMTAT=$(curl -s "$BAS/api/arenden/arende-motspel-a/handelser" -H "Authorization: Bearer $TOKEN_A")
|
|
kontroll "pastadd kalibrering utan matdon tas bort" \
|
|
"$(echo "$HAMTAT" | falt '.handelser.find(h=>h.id==="m-utan").handelse.matdonKalibreradTill ?? "borta"')" "borta"
|
|
|
|
# 15. Underlagen om atkomst och radering ar append-only (TÜV T-7)
|
|
#
|
|
# Radniva-triggern fyrar per RAD. En tom tabell later darfor `delete`
|
|
# lyckas utan att skyddet ens provats — forsta versionen av det har testet
|
|
# var gron pa raderingar just darfor, och bevisade ingenting. Bagge
|
|
# tabellerna maste ha innehall innan de provas.
|
|
PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning -qc \
|
|
"insert into raderingar (organisation_id, subjekt_hash, begard, begard_av, antal_arenden)
|
|
select id, 'prov', now(), 'Integrationstest', 0 from organisationer limit 1"
|
|
for TABELL in atkomstlogg raderingar; do
|
|
RADER=$(PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-tAc "select count(*) from $TABELL")
|
|
if [ "$RADER" = "0" ]; then echo "✗ $TABELL ar tom — provet sager ingenting"; exit 1; fi
|
|
if PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-qc "delete from $TABELL" 2>/dev/null; then
|
|
echo "✗ $TABELL gar att radera"; exit 1
|
|
else
|
|
echo "✓ $TABELL gar inte att radera"
|
|
fi
|
|
done
|
|
if PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-qc "update personnycklar set subjekt = 'annat'" 2>/dev/null; then
|
|
echo "✗ personnyckelns subjekt gar att flytta"; exit 1
|
|
else
|
|
echo "✓ personnyckelns tillhorighet kan inte andras"
|
|
fi
|
|
# Men raderingen sjalv maste fungera — det ar vad radering ÄR.
|
|
PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-qc "delete from personnycklar where subjekt = 'arende-motspel-b'" >/dev/null
|
|
echo "✓ personnycklar gar fortfarande att forstora"
|
|
|
|
# 16. Utgangstid kravs i token (TÜV T-11)
|
|
EVIG=$(node -e "
|
|
const c = require('crypto');
|
|
const b = (o) => Buffer.from(JSON.stringify(o)).toString('base64url');
|
|
const h = b({ alg: 'HS256', typ: 'JWT' });
|
|
const p = b({ sub: '00000000-0000-0000-0000-000000000000', org: '0', roll: 'admin', tv: 0 });
|
|
const s = c.createHmac('sha256', 'integrationshemlighet').update(h + '.' + p).digest('base64url');
|
|
console.log(h + '.' + p + '.' + s);
|
|
")
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/arenden" -H "Authorization: Bearer $EVIG")
|
|
kontroll "token utan utgangstid avvisas" "$KOD" "401"
|
|
|
|
# 17. Gallringen verkstalls (TÜV T-4)
|
|
NYCKLAR_FORE=$(PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-tAc "select count(*) from personnycklar")
|
|
PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-qc "update felsokning_arenden set gallras_efter = now() - interval '1 day' where id = 'arende-motspel-a'"
|
|
DATABASE_URL="postgresql://plattform:test@127.0.0.1:$PGPORT/felsokning" \
|
|
node gallring.mjs > /tmp/gallring.json
|
|
FORSTORDA=$(cat /tmp/gallring.json | falt .forstorda)
|
|
kontroll "gallringen forstorde nyckeln for det passerade arendet" "$FORSTORDA" "1"
|
|
NYCKLAR_EFTER=$(PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-tAc "select count(*) from personnycklar")
|
|
kontroll "en nyckel farre finns kvar" "$NYCKLAR_EFTER" "$((NYCKLAR_FORE - 1))"
|
|
SPAR=$(PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-tAc "select count(*) from raderingar where begard_av like 'Gallring%'")
|
|
kontroll "gallringen lamnade spar i raderingsregistret" "$SPAR" "1"
|
|
# Ett arende som INTE passerat far inte roras.
|
|
DATABASE_URL="postgresql://plattform:test@127.0.0.1:$PGPORT/felsokning" \
|
|
node gallring.mjs > /tmp/gallring2.json
|
|
kontroll "andra korningen gallrar ingenting" "$(cat /tmp/gallring2.json | falt .forstorda)" "0"
|
|
|
|
# 18. Kuvertkryptering av personnycklarna (TÜV T-3)
|
|
#
|
|
# Nycklarna lag i klartext i samma databas som chiffertexten de skyddade,
|
|
# och aterstallningstestet slog fast att de foljer med en aterstallning.
|
|
# Kuverterade under en huvudnyckel utanfor databasen ger en aterstalld
|
|
# dump nycklar som inte gar att oppna utan den.
|
|
kill "$SERVER_PID" 2>/dev/null || true
|
|
wait "$SERVER_PID" 2>/dev/null || true
|
|
HUVUD=$(node -pe "require('crypto').randomBytes(32).toString('hex')")
|
|
DATABASE_URL="postgresql://plattform:test@127.0.0.1:$PGPORT/felsokning" \
|
|
JWT_SECRET=integrationshemlighet PORT=$APPPORT \
|
|
INTEGRATION_NYCKEL=$(node -pe "require('crypto').randomBytes(32).toString('hex')") \
|
|
PERSONNYCKEL_HUVUD=$HUVUD \
|
|
node server.mjs &
|
|
SERVER_PID=$!
|
|
sleep 1
|
|
|
|
curl -s -X POST "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"id":"arende-kuvert","nummer":93,"skapad":"2026-08-06T10:00:00Z"}' >/dev/null
|
|
curl -s -X POST "$BAS/api/arenden/arende-kuvert/handelser" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"handelser":[{"id":"k1","tidpunkt":"2026-08-06T10:01:00Z","anvandare":"Anna",
|
|
"handelse":{"typ":"objekt_identifierat","objekt":{"typ":"Personbil","identifierare":"KUV123",
|
|
"identifieringsmetod":"Regnr","beskrivning":"VW Golf"}}}]}' >/dev/null
|
|
|
|
LAGRAD=$(PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-tAc "select encode(nyckel, 'escape') from personnycklar where subjekt = 'arende-kuvert'")
|
|
case "$LAGRAD" in
|
|
v1.*) echo "✓ nyckeln ligger kuverterad i databasen" ;;
|
|
*) echo "✗ nyckeln ligger i klartext: ${LAGRAD:0:12}"; exit 1 ;;
|
|
esac
|
|
|
|
# Med huvudnyckeln gar identifieraren fortfarande att lasa.
|
|
IDENT=$(curl -s "$BAS/api/arenden/arende-kuvert/handelser" -H "Authorization: Bearer $TOKEN_A" \
|
|
| falt '.handelser[0].handelse.objekt.identifierare')
|
|
kontroll "identifieraren gar att lasa med huvudnyckeln" "$IDENT" "KUV123"
|
|
|
|
# Utan huvudnyckeln — en aterstalld dump pa en annan maskin — gar den inte.
|
|
kill "$SERVER_PID" 2>/dev/null || true
|
|
wait "$SERVER_PID" 2>/dev/null || true
|
|
DATABASE_URL="postgresql://plattform:test@127.0.0.1:$PGPORT/felsokning" \
|
|
JWT_SECRET=integrationshemlighet PORT=$APPPORT node server.mjs >/dev/null 2>&1 &
|
|
SERVER_PID=$!
|
|
sleep 1
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/arenden/arende-kuvert/handelser" -H "Authorization: Bearer $TOKEN_A")
|
|
kontroll "utan huvudnyckel lamnas inget ut" "$KOD" "500"
|
|
|
|
# 19. Support och felanmälan (ALVA-PROC-0050)
|
|
kill "$SERVER_PID" 2>/dev/null || true
|
|
wait "$SERVER_PID" 2>/dev/null || true
|
|
DATABASE_URL="postgresql://plattform:test@127.0.0.1:$PGPORT/felsokning" \
|
|
JWT_SECRET=integrationshemlighet PORT=$APPPORT SUPPORT_NYCKEL=supportnyckel node server.mjs &
|
|
SERVER_PID=$!
|
|
sleep 1
|
|
|
|
# Teknikern far anmala. En troskel har ger farre anmalningar, inte farre fel.
|
|
ANMALAN=$(curl -s -X POST "$BAS/api/support" -H "Authorization: Bearer $TOKEN_J" -H 'Content-Type: application/json' \
|
|
-d '{"typ":"felanmalan","rubrik":"Matsteget vagrar komma","arendeId":"arende-test1",
|
|
"beskrivning":"Kontrollen tar inte emot 2,4 men accepterar 2.4. Verkstaden skriver komma.",
|
|
"sammanhang":{"metodikId":"vibration","regnr":"ABC123","plattformsversion":"ALVA 1.0"}}')
|
|
SUP_ID=$(echo "$ANMALAN" | falt .id)
|
|
kontroll "teknikern kan anmala" "$(echo "$ANMALAN" | falt .beteckning)" "ALVA-SUP-0001"
|
|
kontroll "nya anmalningar ar mottagna" "$(echo "$ANMALAN" | falt .status)" "mottagen"
|
|
|
|
LISTA=$(curl -s "$BAS/api/support" -H "Authorization: Bearer $TOKEN_A")
|
|
kontroll "anmalan syns i organisationen" "$(echo "$LISTA" | falt .arenden.length)" "1"
|
|
kontroll "sammanhanget bar metodiken" "$(echo "$LISTA" | falt '.arenden[0].sammanhang.metodik')" "vibration"
|
|
kontroll "sammanhanget bar sparId" "$(echo "$LISTA" | falt '.arenden[0].sammanhang.spar ? "ja" : "nej"')" "ja"
|
|
# Det viktigaste: ett regnr kan inte smugglas in i en supportanmalan.
|
|
kontroll "identifierande falt slapps inte igenom" \
|
|
"$(echo "$LISTA" | falt '.arenden[0].sammanhang.regnr ?? "borta"')" "borta"
|
|
|
|
kontroll "org B ser inga anmalningar" "$(curl -s "$BAS/api/support" -H "Authorization: Bearer $TOKEN_B" | falt .arenden.length)" "0"
|
|
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/support" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' -d '{"typ":"felanmalan","rubrik":"fel","beskrivning":"gar ej"}')
|
|
kontroll "for tunn anmalan avvisas" "$KOD" "400"
|
|
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/support" -H "Authorization: Bearer $TOKEN_B" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"typ":"felanmalan","rubrik":"Nagot om org A","arendeId":"arende-test1","beskrivning":"En anmalan ar inte en vag runt organisationsgransen."}')
|
|
kontroll "anmalan kan inte peka pa annan orgs arende" "$KOD" "404"
|
|
|
|
# Verkstaden svarar i sitt eget arende; status ar var sida av bordet.
|
|
curl -s -X POST "$BAS/api/support/$SUP_ID/inlagg" -H "Authorization: Bearer $TOKEN_A" \
|
|
-H 'Content-Type: application/json' -d '{"text":"Hander aven i Chrome."}' >/dev/null
|
|
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/support/$SUP_ID/inlagg" \
|
|
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"text":"Vi tittar.","status":"under_arbete"}')
|
|
kontroll "verkstaden kan inte satta status" "$KOD" "403"
|
|
|
|
curl -s -X POST "$BAS/api/support/$SUP_ID/inlagg" -H 'X-Support: supportnyckel' \
|
|
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
|
-d '{"text":"Ratta i nasta utgava.","status":"atgardad"}' >/dev/null
|
|
LISTA=$(curl -s "$BAS/api/support" -H "Authorization: Bearer $TOKEN_A")
|
|
kontroll "statusen harleds ur inlaggen" "$(echo "$LISTA" | falt '.arenden[0].status')" "atgardad"
|
|
kontroll "svaret finns kvar bredvid statusbytet" "$(echo "$LISTA" | falt '.arenden[0].inlagg.length')" "2"
|
|
|
|
if PGPASSWORD=test "$PGBIN/psql" -h 127.0.0.1 -p $PGPORT -U plattform -d felsokning \
|
|
-qc "update supportarenden set rubrik = 'omskrivet'" 2>/dev/null; then
|
|
echo "✗ supportarenden gar att skriva om"; exit 1
|
|
else
|
|
echo "✓ supportarenden gar inte att skriva om"
|
|
fi
|
|
|
|
echo "Integrationstest: allt grönt"
|