Files
foodster/scripts/smoke.sh
T
Esa Kataja b67c4edd5a Ask before deleting, and put the row actions on icons
Muokkaa and Poista become a pencil and a bin, which stops the catalog rows
being two words wide. Both carry a Finnish aria-label and title, so nothing
is lost by dropping the text.

An icon is easier to hit by accident than a word, so neither delete happens
immediately now. A tapped bin turns that row's actions into "Poista? Kyllä /
Peruuta", and a logged meal asks "Poistetaanko merkintä?" before it goes.
The meal is the more destructive of the two: a dish is only soft-deleted and
its name still resolves in old entries, while the log row is dropped outright.

Both confirmations are plain links and forms, so they work with the back
button and need no client code.

Also fixes a test that was passing for the wrong reason. The delete check only
asserted a 303 and took the first dish id on the page, which stopped being the
one it had just created when the catalog became grouped and alphabetical — so
it was deleting an unrelated dish. It now finds that dish's own id, and a new
refute helper asserts the dish is actually gone afterwards.
2026-09-05 21:34:18 +03:00

244 lines
8.6 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"
# 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"
check "history is on the same page as the logger" \
"$(curl -s -u ":$pass" "http://$addr/")" "Aiemmin"
# 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"
# 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 -------------------------------------------
check "adding a main redirects" \
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
-d 'nimi=uunikala&kategoria=fish&lisukkeita=1' "http://$addr/ruuat/paaruoka")" "303"
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" \
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
-d 'nimi=lohkoperunat' "http://$addr/ruuat/lisuke")" "303"
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" \
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
-d "id=$uusi&tyyppi=paa" "http://$addr/ruuat/poista")" "303"
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"