check / check (push) Successful in 46s
BREAKING CHANGE: PASSWORD is gone and AUTH and CERTRESOLVER are required. The server's compose.yaml and .env must be updated in the same deploy — the new image ignores PASSWORD, and the old one refuses to start without it. Authelia now sits in front of Traefik, so the app was asking for a second password at the same door. Two prompts, and the weaker of the two was the one holding a single shared secret with no sessions, no MFA and no revocation. Deleting it is the whole change: Authelia already does this properly, once, for every service on the host. Gone: auth(), challenge(), the whole of throttle.go and its tests, and golang.org/x/time with them. routes() returns the bare mux, /healthz is an ordinary route on it, and the smoke script drops sixty -u flags. Roughly 230 lines removed and nothing written to replace them. What holds the app up now, both asserted in compose.yaml: - The router names the Authelia middleware through AUTH. Traefik takes a router out of service when its middleware does not resolve, so a typo or an unset variable fails shut rather than serving the app open. - The container still publishes no ports, so the proxy is the only thing that can reach it. Publishing 8080 would now bypass authentication outright, not merely TLS — the comment there says so. certresolver replaces the bare tls=true, parameterised as CERTRESOLVER: the server had been carrying that label by hand since the first deploy. Naming a resolver implies tls=true, so it stays one label. TestAuth and TestHealthzSkipsAuth are replaced by one test asserting every route answers without credentials — a 401 from here would now mean auth had crept back in.
336 lines
12 KiB
Bash
Executable File
336 lines
12 KiB
Bash
Executable File
#!/bin/sh
|
|
# End-to-end check of a running Foodster: static assets, the logger and the
|
|
# bundle import flow. Builds its own binary, uses a scratch database and a
|
|
# spare port, and cleans up after itself, so it never touches a real instance.
|
|
#
|
|
# There is nothing to authenticate as: the app is served behind Authelia and
|
|
# has no login of its own.
|
|
#
|
|
# Run it with `make smoke`.
|
|
|
|
set -eu
|
|
|
|
cd "$(dirname "$0")/.."
|
|
|
|
addr=127.0.0.1:8099
|
|
|
|
# Dates are relative, never literal. A hardcoded one turns into "some day in
|
|
# the past" at the next midnight, and the assertions quietly start meaning
|
|
# something else.
|
|
d0=$(date +%F)
|
|
d1=$(date -d yesterday +%F)
|
|
tmp=$(mktemp -d)
|
|
trap 'kill ${srv:-0} 2>/dev/null || true; rm -rf "$tmp"' EXIT
|
|
|
|
go build -o "$tmp/foodster" ./cmd/foodster
|
|
|
|
DB="$tmp/smoke.db" ADDR="$addr" \
|
|
"$tmp/foodster" >"$tmp/server.log" 2>&1 &
|
|
srv=$!
|
|
|
|
i=0
|
|
while ! curl -sf "http://$addr/healthz" >/dev/null 2>&1; do
|
|
i=$((i + 1))
|
|
if [ "$i" -gt 50 ]; then
|
|
echo "server did not start:"
|
|
cat "$tmp/server.log"
|
|
exit 1
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
|
|
fail=0
|
|
|
|
# check <name> <haystack> <needle>
|
|
check() {
|
|
if printf '%s' "$2" | grep -qF -- "$3"; then
|
|
echo " ok $1"
|
|
else
|
|
echo " FAIL $1 (expected to find: $3)"
|
|
fail=1
|
|
fi
|
|
}
|
|
|
|
# refute <name> <haystack> <needle>
|
|
refute() {
|
|
if printf '%s' "$2" | grep -qF -- "$3"; then
|
|
echo " FAIL $1 (should not contain: $3)"
|
|
fail=1
|
|
else
|
|
echo " ok $1"
|
|
fi
|
|
}
|
|
|
|
echo "smoke: http://$addr"
|
|
|
|
check "healthz answers" \
|
|
"$(curl -s -o /dev/null -w '%{http_code}' "http://$addr/healthz")" "200"
|
|
|
|
check "datastar client is served" \
|
|
"$(curl -s -o /dev/null -w '%{http_code}' "http://$addr/static/datastar.js")" "200"
|
|
|
|
check "favicon is served" \
|
|
"$(curl -s -o /dev/null -w '%{http_code}' "http://$addr/static/favicon.svg")" "200"
|
|
|
|
check "theme script is served" \
|
|
"$(curl -s -o /dev/null -w '%{http_code}' "http://$addr/static/theme.js")" "200"
|
|
|
|
home=$(curl -s "http://$addr/")
|
|
check "the header carries the brand" "$home" "Foodster"
|
|
# ENV is unset here, so this instance is production and unmarked.
|
|
check "production tabs are not tagged" "$home" "<title>Foodster</title>"
|
|
check "dark is the default without JavaScript" "$home" '<html lang="fi" data-theme="dark">'
|
|
check "the theme toggle is present" "$home" "data-theme-toggle"
|
|
check "both theme icons ship so CSS can pick one" "$home" 'class="i-moon"'
|
|
|
|
check "apple touch icon is served" \
|
|
"$(curl -s -o /dev/null -w '%{http_code}' "http://$addr/static/apple-touch-icon.png")" "200"
|
|
|
|
# A manifest served as octet-stream is silently ignored by the browser.
|
|
check "manifest has the right content type" \
|
|
"$(curl -s -o /dev/null -w '%{content_type}' "http://$addr/static/manifest.webmanifest")" \
|
|
"application/manifest+json"
|
|
|
|
check "catalog starts empty" \
|
|
"$(curl -s "http://$addr/ruuat")" "0 pääruokaa"
|
|
|
|
out=$(curl -s -F "tiedosto=@seeds/testi.json" "http://$addr/ruuat/tuonti")
|
|
check "file upload imports the seed bundle" "$out" "Lisätty 22, ohitettu 0"
|
|
check "counts update after import" "$out" "16 pääruokaa, 6 lisuketta"
|
|
|
|
check "re-import refuses duplicates" \
|
|
"$(curl -s -F "tiedosto=@seeds/testi.json" "http://$addr/ruuat/tuonti")" \
|
|
"jo listalla"
|
|
|
|
check "pasted JSON imports" \
|
|
"$(curl -s -F 'json={"mains":[],"sides":[{"name":"Perunasalaatti"}]}' \
|
|
"http://$addr/ruuat/tuonti")" "Lisätty 1"
|
|
|
|
check "unknown category is reported" \
|
|
"$(curl -s -F 'json={"mains":[{"name":"Rikki","categories":["kana"]}],"sides":[]}' \
|
|
"http://$addr/ruuat/tuonti")" "tuntematon kategoria"
|
|
|
|
check "empty submit is explained" \
|
|
"$(curl -s -F 'json=' "http://$addr/ruuat/tuonti")" "Ei tuotavaa"
|
|
|
|
check "malformed JSON is explained" \
|
|
"$(curl -s -F 'json={nope' "http://$addr/ruuat/tuonti")" "JSON ei kelpaa"
|
|
|
|
# ---- the log flow, against the dishes imported above --------------------
|
|
|
|
board=$(curl -s "http://$addr/")
|
|
check "board lists imported dishes" "$board" "Lihapullat"
|
|
|
|
# Tähteet is loggable but is not food: on the board, never in the catalog.
|
|
check "leftovers are on the board" "$board" "Tähteet"
|
|
refute "leftovers are not in the catalog" \
|
|
"$(curl -s "http://$addr/ruuat")" "Tähteet"
|
|
|
|
# Pull a real dish id out of the board rather than assuming one.
|
|
ruoka=$(printf '%s' "$board" | grep -o 'ruoka=[0-9]*' | head -n1 | cut -d= -f2)
|
|
if [ -z "$ruoka" ]; then
|
|
echo " FAIL could not find a dish link on the board"
|
|
fail=1
|
|
ruoka=1
|
|
fi
|
|
|
|
check "picking a dish opens the sides step" \
|
|
"$(curl -s "http://$addr/?ruoka=$ruoka")" "Tallenna"
|
|
|
|
check "saving redirects back to the day" \
|
|
"$(curl -s -o /dev/null -w '%{http_code}' \
|
|
-d "pvm=$d0&ruoka=$ruoka" "http://$addr/kirjaa")" "303"
|
|
|
|
check "the saved day shows what was eaten" \
|
|
"$(curl -s "http://$addr/?pvm=$d0")" "kirjattu"
|
|
|
|
# The selected day expands inside the list rather than in a panel above it,
|
|
# so the rows below do not shift when one is tapped.
|
|
day=$(curl -s "http://$addr/?pvm=$d0")
|
|
check "the selected day expands in place" "$day" 'class="open"'
|
|
check "and stays in the list rather than being lifted out" "$day" "kirjattu"
|
|
|
|
# ---- the day list patches in place instead of navigating ----------------
|
|
|
|
dayp=$(curl -s -H 'Datastar-Request: true' "http://$addr/paiva?pvm=$d0")
|
|
check "opening a day patches the list" "$dayp" 'id="paivat"'
|
|
refute "and returns a fragment, not a page" "$dayp" "<html"
|
|
|
|
check "picking a dish patches to the sides step" \
|
|
"$(curl -s -H 'Datastar-Request: true' \
|
|
"http://$addr/paiva?pvm=$d0&ruoka=$ruoka")" "Tallenna"
|
|
|
|
check "saving from Datastar patches back" \
|
|
"$(curl -s -H 'Datastar-Request: true' \
|
|
-d "pvm=$d1&ruoka=$ruoka" "http://$addr/kirjaa")" 'id="paivat"'
|
|
|
|
check "deleting from Datastar patches back" \
|
|
"$(curl -s -H 'Datastar-Request: true' \
|
|
-d "pvm=$d1" "http://$addr/poista")" 'id="paivat"'
|
|
|
|
# Without the header it must still redirect, for no JavaScript.
|
|
check "a plain save still redirects to the day" \
|
|
"$(curl -s -o /dev/null -w '%{redirect_url}' \
|
|
-d "pvm=$d1&ruoka=$ruoka" "http://$addr/kirjaa")" "pvm=$d1"
|
|
|
|
# Deleting a logged meal drops the row outright, so it asks first.
|
|
saved=$(curl -s "http://$addr/?pvm=$d0&poista=1")
|
|
check "deleting a meal asks first" "$saved" "Poistetaanko merkintä?"
|
|
# Assert the entry is still shown, rather than that no gap row exists anywhere
|
|
# on the page: other days are legitimately unlogged and render their own.
|
|
check "and the entry is still there while asking" "$saved" "kirjattu"
|
|
|
|
check "deleting redirects back" \
|
|
"$(curl -s -o /dev/null -w '%{http_code}' \
|
|
-d "pvm=$d0" "http://$addr/poista")" "303"
|
|
|
|
check "the day is empty again" \
|
|
"$(curl -s "http://$addr/?pvm=$d0")" "Etsi"
|
|
|
|
check "search filters the board" \
|
|
"$(curl -s "http://$addr/?haku=keitto")" "keitto"
|
|
|
|
# ---- live search: Datastar sends signals as JSON in ?datastar= -----------
|
|
|
|
live=$(curl -s --get --data-urlencode 'datastar={"haku":"keitto"}' "http://$addr/etsi")
|
|
check "live search returns the board fragment" "$live" 'id="lauta"'
|
|
check "live search applies the term" "$live" "keitto"
|
|
refute "live search excludes non-matches" "$live" "Lihapullat"
|
|
refute "the fragment is not a whole page" "$live" "<html"
|
|
|
|
check "live search is served as html for Datastar to patch" \
|
|
"$(curl -s -o /dev/null -w '%{content_type}' \
|
|
--get --data-urlencode 'datastar={"haku":"keitto"}' "http://$addr/etsi")" \
|
|
"text/html"
|
|
|
|
cat_live=$(curl -s --get --data-urlencode 'datastar={"haku":"riisi"}' "http://$addr/ruuat/etsi")
|
|
check "catalog live search returns its fragment" "$cat_live" 'id="ruokalista"'
|
|
check "catalog live search matches sides too" "$cat_live" "Riisi"
|
|
refute "catalog live search excludes non-matches" "$cat_live" "Lihapullat"
|
|
|
|
# The plain form still works without JavaScript.
|
|
check "catalog search works as a plain form too" \
|
|
"$(curl -s "http://$addr/ruuat?haku=riisi")" "Riisi"
|
|
|
|
# Nothing was eaten tomorrow. A future date is clamped rather than logged.
|
|
future=$(date -d '+30 days' +%Y-%m-%d)
|
|
check "a future date falls back to today" \
|
|
"$(curl -s "http://$addr/?pvm=$future")" "$(date +%-d.%-m.%Y)"
|
|
|
|
check "saving a future date is clamped too" \
|
|
"$(curl -s -o /dev/null -w '%{redirect_url}' \
|
|
-d "pvm=$future&ruoka=$ruoka" "http://$addr/kirjaa")" "/"
|
|
|
|
check "tomorrow was not written to the log" \
|
|
"$(curl -s "http://$addr/?pvm=$future")" "$(date +%-d.%-m.%Y)"
|
|
|
|
# Clean up the entry that clamped onto today.
|
|
curl -s -o /dev/null -d "pvm=$(date +%Y-%m-%d)" "http://$addr/poista"
|
|
|
|
# ---- adding a dish without leaving Kirjaa --------------------------------
|
|
|
|
miss=$(curl -s "http://$addr/?haku=Poronkariste")
|
|
check "a search with no hits offers to add it" "$miss" "Ei osumia. Lisätäänkö?"
|
|
check "the add form is prefilled with the search" "$miss" 'value="Poronkariste"'
|
|
|
|
check "quick add goes straight to the sides step" \
|
|
"$(curl -s -o /dev/null -w '%{redirect_url}' \
|
|
-d 'nimi=Poronkariste&kategoria=meat&lisukkeita=1' "http://$addr/lisaa")" \
|
|
"ruoka="
|
|
|
|
check "quick add rejects a dish with no category" \
|
|
"$(curl -s -d 'nimi=Kategoriaton' "http://$addr/lisaa")" \
|
|
"Valitse vähintään yksi kategoria."
|
|
|
|
check "the quick-added dish is on the board" \
|
|
"$(curl -s "http://$addr/")" "Poronkariste"
|
|
|
|
# ---- catalog CRUD from the UI -------------------------------------------
|
|
|
|
# Assert where it redirects, not just that it does: these pointed at the old
|
|
# /ruoat spelling for a while and every 303-only check was happy.
|
|
check "adding a main redirects back to the catalog" \
|
|
"$(curl -s -o /dev/null -w '%{redirect_url}' \
|
|
-d 'nimi=uunikala&kategoria=fish&lisukkeita=1' "http://$addr/ruuat/paaruoka")" \
|
|
"/ruuat"
|
|
|
|
catalog=$(curl -s "http://$addr/ruuat")
|
|
check "the new main is listed, sentence-cased" "$catalog" "Uunikala"
|
|
|
|
check "a duplicate name is refused" \
|
|
"$(curl -s -d 'nimi=UUNIKALA&kategoria=fish' "http://$addr/ruuat/paaruoka")" \
|
|
"Nimi on jo listalla."
|
|
|
|
check "a main with no category is refused" \
|
|
"$(curl -s -d 'nimi=Kategoriaton' "http://$addr/ruuat/paaruoka")" \
|
|
"Valitse vähintään yksi kategoria."
|
|
|
|
check "a nameless dish is refused" \
|
|
"$(curl -s -d 'nimi=+++&kategoria=fish' "http://$addr/ruuat/paaruoka")" \
|
|
"Anna nimi."
|
|
|
|
check "adding a side redirects back to the catalog" \
|
|
"$(curl -s -o /dev/null -w '%{redirect_url}' \
|
|
-d 'nimi=lohkoperunat' "http://$addr/ruuat/lisuke")" \
|
|
"/ruuat"
|
|
|
|
check "the new side is listed" \
|
|
"$(curl -s "http://$addr/ruuat")" "Lohkoperunat"
|
|
|
|
# The id of Uunikala specifically: the catalog is grouped and alphabetical, so
|
|
# the first id on the page belongs to some other dish entirely.
|
|
uusi=$(printf '%s' "$catalog" | grep -o 'Uunikala.*' | grep -o 'muokkaa=[0-9]*' | head -n1 | cut -d= -f2)
|
|
if [ -z "$uusi" ]; then
|
|
echo " FAIL could not find Uunikala's id in the catalog"
|
|
fail=1
|
|
uusi=0
|
|
fi
|
|
check "the edit form is prefilled" \
|
|
"$(curl -s "http://$addr/ruuat?muokkaa=$uusi")" "Muokkaa pääruokaa"
|
|
|
|
# A bin icon is easy to hit by accident, so the row asks before anything goes.
|
|
check "the bin asks before deleting" \
|
|
"$(curl -s "http://$addr/ruuat?poista=$uusi&tyyppi=paa")" "Poista?"
|
|
|
|
check "the dish is still there while it asks" \
|
|
"$(curl -s "http://$addr/ruuat?poista=$uusi&tyyppi=paa")" "Uunikala"
|
|
|
|
# ---- the catalog patches in place instead of navigating -----------------
|
|
|
|
# A delete confirmation halfway down a long list must not send the browser
|
|
# back to the top, so these answer with a Datastar patch rather than a page.
|
|
patch=$(curl -s -H 'Datastar-Request: true' \
|
|
"http://$addr/ruuat/nayta?poista=$uusi&tyyppi=paa")
|
|
check "asking to delete patches rather than navigates" "$patch" "event: datastar-patch-elements"
|
|
check "the patch carries the list" "$patch" 'id="ruokalista"'
|
|
check "and both forms, so an open one closes" "$patch" 'id="paaruoka"'
|
|
check "the row it patches in is asking" "$patch" "Poista?"
|
|
|
|
check "patches are served as an event stream" \
|
|
"$(curl -s -o /dev/null -w '%{content_type}' -H 'Datastar-Request: true' \
|
|
"http://$addr/ruuat/nayta")" "text/event-stream"
|
|
|
|
check "deleting from Datastar patches too" \
|
|
"$(curl -s -H 'Datastar-Request: true' \
|
|
-d "id=$uusi&tyyppi=paa" "http://$addr/ruuat/poista")" \
|
|
"event: datastar-patch-elements"
|
|
|
|
refute "and the dish is gone from the patched list" \
|
|
"$(curl -s -H 'Datastar-Request: true' "http://$addr/ruuat/nayta")" \
|
|
"Uunikala"
|
|
|
|
# Without the header it must still be an ordinary redirect, for no JavaScript.
|
|
check "a plain form post still redirects" \
|
|
"$(curl -s -o /dev/null -w '%{redirect_url}' \
|
|
-d 'nimi=Testiruoka&kategoria=fish' "http://$addr/ruuat/paaruoka")" \
|
|
"/ruuat"
|
|
|
|
refute "the dish is gone once confirmed" \
|
|
"$(curl -s "http://$addr/ruuat")" "Uunikala"
|
|
|
|
if [ "$fail" -ne 0 ]; then
|
|
echo "smoke: FAILED"
|
|
exit 1
|
|
fi
|
|
echo "smoke: all passed"
|