Files
alva/services/plattform/integrationstest.sh
T
Claude 64b2124cd8 Publikt kundgodkännande i delningslänken
Kunden kan nu godkänna eller avböja åtgärdsförslaget direkt i sin
Live Share-länk — API:ts enda skrivande publika väg, med sex spärrar
som var och en verifieras i integrationstestet:

1. endast delningar på kundnivå (partner-/internlänkar får aldrig svara
   åt kunden) och aldrig återkallade
2. ärendets ursprungliga delningskod saknar registrerad nivå och kan
   inte heller svara
3. det måste finnas ett åtgärdsförslag att svara på
4. ett besked per ärende — svaret kan inte ändras i efterhand
5. endast godkant/avbojt/delvis plus kommentar på högst 500 tecken;
   inget annat kan skrivas till loggen den vägen
6. takt-begränsning per delningskod (429)

Beskedet loggas som kundbeslut med kanal "Delningslänk" och avsändaren
"Kund via delningslänk"; verkstadens egna registreringar (telefon, på
plats …) fungerar precis som förut, och kvalitetsgrindens krav gäller
oförändrat. Beslutspanelen visas bara på den publika kundlänken —
interna och partnervyer förblir helt skrivskyddade.

73 vitest-tester, integrationstest (åtta nya kontroller för spärrarna)
och OpenAPI-validering gröna. Därmed är även den sista dokumenterade
avgränsningen utöver externa fordonsdatabaser avklarad.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
2026-08-03 12:25:14 +00:00

264 lines
18 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/k8s/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
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"
# 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
echo "Integrationstest: allt grönt"