Adding, editing or deleting a dish redirected to /ruoat, which stopped existing when the tab was renamed to Ruuat. Every one of those actions ended on a 404. Shipped in v20260905-4. The rename was done with a scripted replace across views.templ, main.go and the smoke script; handlers.go was not in the list. The tests did not catch it because they asserted only that the response was a 303. A redirect to a dead URL is still a 303. They now assert the target.
284 lines
11 KiB
Bash
Executable File
284 lines
11 KiB
Bash
Executable File
#!/bin/sh
|
|
# End-to-end check of a running Foodster: auth, static assets 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.
|
|
#
|
|
# Run it with `make smoke`.
|
|
|
|
set -eu
|
|
|
|
cd "$(dirname "$0")/.."
|
|
|
|
addr=127.0.0.1:8099
|
|
pass=smoke
|
|
tmp=$(mktemp -d)
|
|
trap 'kill ${srv:-0} 2>/dev/null || true; rm -rf "$tmp"' EXIT
|
|
|
|
go build -o "$tmp/foodster" ./cmd/foodster
|
|
|
|
FOODSTER_PASSWORD="$pass" FOODSTER_DB="$tmp/smoke.db" FOODSTER_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 "unauthenticated request is refused" \
|
|
"$(curl -s -o /dev/null -w '%{http_code}' "http://$addr/")" "401"
|
|
|
|
check "healthz needs no password" \
|
|
"$(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}' -u ":$pass" "http://$addr/static/datastar.js")" "200"
|
|
|
|
check "favicon is served" \
|
|
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" "http://$addr/static/favicon.svg")" "200"
|
|
|
|
check "theme script is served" \
|
|
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" "http://$addr/static/theme.js")" "200"
|
|
|
|
home=$(curl -s -u ":$pass" "http://$addr/")
|
|
check "the header carries the brand" "$home" "Foodster"
|
|
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}' -u ":$pass" "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}' -u ":$pass" "http://$addr/static/manifest.webmanifest")" \
|
|
"application/manifest+json"
|
|
|
|
check "catalog starts empty" \
|
|
"$(curl -s -u ":$pass" "http://$addr/ruuat")" "0 pääruokaa"
|
|
|
|
out=$(curl -s -u ":$pass" -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 -u ":$pass" -F "tiedosto=@seeds/testi.json" "http://$addr/ruuat/tuonti")" \
|
|
"jo listalla"
|
|
|
|
check "pasted JSON imports" \
|
|
"$(curl -s -u ":$pass" -F 'json={"mains":[],"sides":[{"name":"Perunasalaatti"}]}' \
|
|
"http://$addr/ruuat/tuonti")" "Lisätty 1"
|
|
|
|
check "unknown category is reported" \
|
|
"$(curl -s -u ":$pass" -F 'json={"mains":[{"name":"Rikki","categories":["kana"]}],"sides":[]}' \
|
|
"http://$addr/ruuat/tuonti")" "tuntematon kategoria"
|
|
|
|
check "empty submit is explained" \
|
|
"$(curl -s -u ":$pass" -F 'json=' "http://$addr/ruuat/tuonti")" "Ei tuotavaa"
|
|
|
|
check "malformed JSON is explained" \
|
|
"$(curl -s -u ":$pass" -F 'json={nope' "http://$addr/ruuat/tuonti")" "JSON ei kelpaa"
|
|
|
|
# ---- the log flow, against the dishes imported above --------------------
|
|
|
|
board=$(curl -s -u ":$pass" "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 -u ":$pass" "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 -u ":$pass" "http://$addr/?ruoka=$ruoka")" "Tallenna"
|
|
|
|
check "saving redirects back to the day" \
|
|
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
|
|
-d "pvm=2026-09-05&ruoka=$ruoka" "http://$addr/kirjaa")" "303"
|
|
|
|
check "the saved day shows what was eaten" \
|
|
"$(curl -s -u ":$pass" "http://$addr/?pvm=2026-09-05")" "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 -u ":$pass" "http://$addr/?pvm=2026-09-05")
|
|
check "the selected day expands in place" "$day" 'id="paiva-2026-09-05"'
|
|
check "and stays in the list rather than being lifted out" "$day" "kirjattu"
|
|
check "links inside it keep the anchor" "$day" "#paiva-2026-09-05"
|
|
|
|
check "saving returns to the day, not the top" \
|
|
"$(curl -s -o /dev/null -w '%{redirect_url}' -u ":$pass" \
|
|
-d "pvm=2026-09-04&ruoka=$ruoka" "http://$addr/kirjaa")" "#paiva-2026-09-04"
|
|
|
|
# Deleting a logged meal drops the row outright, so it asks first.
|
|
saved=$(curl -s -u ":$pass" "http://$addr/?pvm=2026-09-05&poista=1")
|
|
check "deleting a meal asks first" "$saved" "Poistetaanko merkintä?"
|
|
refute "and does not delete while asking" "$saved" "Ei merkintää"
|
|
|
|
check "deleting redirects back" \
|
|
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
|
|
-d "pvm=2026-09-05" "http://$addr/poista")" "303"
|
|
|
|
check "the day is empty again" \
|
|
"$(curl -s -u ":$pass" "http://$addr/?pvm=2026-09-05")" "Etsi"
|
|
|
|
check "search filters the board" \
|
|
"$(curl -s -u ":$pass" "http://$addr/?haku=keitto")" "keitto"
|
|
|
|
# ---- live search: Datastar sends signals as JSON in ?datastar= -----------
|
|
|
|
live=$(curl -s -u ":$pass" --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}' -u ":$pass" \
|
|
--get --data-urlencode 'datastar={"haku":"keitto"}' "http://$addr/etsi")" \
|
|
"text/html"
|
|
|
|
cat_live=$(curl -s -u ":$pass" --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 -u ":$pass" "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 -u ":$pass" "http://$addr/?pvm=$future")" "$(date +%-d.%-m.%Y)"
|
|
|
|
check "saving a future date is clamped too" \
|
|
"$(curl -s -o /dev/null -w '%{redirect_url}' -u ":$pass" \
|
|
-d "pvm=$future&ruoka=$ruoka" "http://$addr/kirjaa")" "/"
|
|
|
|
check "tomorrow was not written to the log" \
|
|
"$(curl -s -u ":$pass" "http://$addr/?pvm=$future")" "$(date +%-d.%-m.%Y)"
|
|
|
|
# Clean up the entry that clamped onto today.
|
|
curl -s -o /dev/null -u ":$pass" -d "pvm=$(date +%Y-%m-%d)" "http://$addr/poista"
|
|
|
|
# ---- adding a dish without leaving Kirjaa --------------------------------
|
|
|
|
miss=$(curl -s -u ":$pass" "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}' -u ":$pass" \
|
|
-d 'nimi=Poronkariste&kategoria=meat&lisukkeita=1' "http://$addr/lisaa")" \
|
|
"ruoka="
|
|
|
|
check "quick add rejects a dish with no category" \
|
|
"$(curl -s -u ":$pass" -d 'nimi=Kategoriaton' "http://$addr/lisaa")" \
|
|
"Valitse vähintään yksi kategoria."
|
|
|
|
check "the quick-added dish is on the board" \
|
|
"$(curl -s -u ":$pass" "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}' -u ":$pass" \
|
|
-d 'nimi=uunikala&kategoria=fish&lisukkeita=1' "http://$addr/ruuat/paaruoka")" \
|
|
"/ruuat"
|
|
|
|
catalog=$(curl -s -u ":$pass" "http://$addr/ruuat")
|
|
check "the new main is listed, sentence-cased" "$catalog" "Uunikala"
|
|
|
|
check "a duplicate name is refused" \
|
|
"$(curl -s -u ":$pass" -d 'nimi=UUNIKALA&kategoria=fish' "http://$addr/ruuat/paaruoka")" \
|
|
"Nimi on jo listalla."
|
|
|
|
check "a main with no category is refused" \
|
|
"$(curl -s -u ":$pass" -d 'nimi=Kategoriaton' "http://$addr/ruuat/paaruoka")" \
|
|
"Valitse vähintään yksi kategoria."
|
|
|
|
check "a nameless dish is refused" \
|
|
"$(curl -s -u ":$pass" -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}' -u ":$pass" \
|
|
-d 'nimi=lohkoperunat' "http://$addr/ruuat/lisuke")" \
|
|
"/ruuat"
|
|
|
|
check "the new side is listed" \
|
|
"$(curl -s -u ":$pass" "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 -u ":$pass" "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 -u ":$pass" "http://$addr/ruuat?poista=$uusi&tyyppi=paa")" "Poista?"
|
|
|
|
check "the dish is still there while it asks" \
|
|
"$(curl -s -u ":$pass" "http://$addr/ruuat?poista=$uusi&tyyppi=paa")" "Uunikala"
|
|
|
|
check "confirming the delete redirects back to the catalog" \
|
|
"$(curl -s -o /dev/null -w '%{redirect_url}' -u ":$pass" \
|
|
-d "id=$uusi&tyyppi=paa" "http://$addr/ruuat/poista")" \
|
|
"/ruuat"
|
|
|
|
refute "the dish is gone once confirmed" \
|
|
"$(curl -s -u ":$pass" "http://$addr/ruuat")" "Uunikala"
|
|
|
|
if [ "$fail" -ne 0 ]; then
|
|
echo "smoke: FAILED"
|
|
exit 1
|
|
fi
|
|
echo "smoke: all passed"
|