| 1 | #!/usr/bin/env bash |
| 2 | # SPDX-License-Identifier: AGPL-3.0-or-later |
| 3 | # |
| 4 | # S40 cutover smoke test. Exercises the public-facing routes that |
| 5 | # matter at launch: landing page, signup/login forms render with |
| 6 | # a fresh CSRF token, health endpoints respond, the docs subdomain |
| 7 | # is reachable, the API authenticates a known PAT. |
| 8 | # |
| 9 | # Usage: |
| 10 | # deploy/cutover/smoke.sh https://shithub.sh |
| 11 | # |
| 12 | # Optional env (when set, the script also exercises the API): |
| 13 | # SHITHUB_SMOKE_PAT — a valid shp_ token for `user:read` |
| 14 | # SHITHUB_SMOKE_DOCS — docs subdomain URL (default: docs.<base>) |
| 15 | # |
| 16 | # Exit status: |
| 17 | # 0 — all green |
| 18 | # 1 — at least one check failed |
| 19 | # 2 — usage error |
| 20 | |
| 21 | set -euo pipefail |
| 22 | |
| 23 | if [[ $# -lt 1 ]]; then |
| 24 | echo "usage: $0 <base-url>" >&2 |
| 25 | exit 2 |
| 26 | fi |
| 27 | |
| 28 | BASE="$1" |
| 29 | DOCS="${SHITHUB_SMOKE_DOCS:-${BASE/shithub./docs.shithub.}}" |
| 30 | fail=0 |
| 31 | |
| 32 | say() { printf '\n=== %s ===\n' "$*"; } |
| 33 | ok() { printf ' PASS: %s\n' "$*"; } |
| 34 | bad() { printf ' FAIL: %s\n' "$*"; fail=$((fail + 1)); } |
| 35 | |
| 36 | # 1. Landing. |
| 37 | say "GET $BASE/" |
| 38 | body=$(curl -fsS -o - -w "\n%{http_code}\n" "$BASE/" 2>&1) || { bad "landing fetch"; body=""; } |
| 39 | if [[ "$body" == *"shithub"* ]]; then ok "body contains shithub"; else bad "body missing shithub"; fi |
| 40 | |
| 41 | # 2. Health endpoints. /readyz proves DB + storage are reachable. |
| 42 | say "GET $BASE/-/health" |
| 43 | curl -fsS "$BASE/-/health" >/dev/null && ok "/-/health 200" || bad "/-/health" |
| 44 | say "GET $BASE/healthz" |
| 45 | curl -fsS "$BASE/healthz" >/dev/null && ok "/healthz 200" || bad "/healthz" |
| 46 | say "GET $BASE/readyz" |
| 47 | curl -fsS "$BASE/readyz" >/dev/null && ok "/readyz 200" || bad "/readyz" |
| 48 | |
| 49 | # 3. Signup form renders. |
| 50 | say "GET $BASE/signup" |
| 51 | body=$(curl -fsS "$BASE/signup") || { bad "signup fetch"; body=""; } |
| 52 | if [[ "$body" == *"csrf_token"* ]]; then ok "CSRF token present"; else bad "no csrf_token in signup form"; fi |
| 53 | |
| 54 | # 4. Login form renders. |
| 55 | say "GET $BASE/login" |
| 56 | body=$(curl -fsS "$BASE/login") || { bad "login fetch"; body=""; } |
| 57 | if [[ "$body" == *"username"* ]] && [[ "$body" == *"password"* ]]; then |
| 58 | ok "login form fields present" |
| 59 | else |
| 60 | bad "login form missing username/password" |
| 61 | fi |
| 62 | |
| 63 | # 5. TLS posture. Strict-Transport-Security must be set. |
| 64 | say "TLS / HSTS" |
| 65 | hdrs=$(curl -fsS -I "$BASE/" 2>&1) || { bad "headers fetch"; hdrs=""; } |
| 66 | if grep -qi "strict-transport-security" <<<"$hdrs"; then |
| 67 | ok "HSTS header set" |
| 68 | else |
| 69 | bad "HSTS header missing" |
| 70 | fi |
| 71 | if grep -qi "x-content-type-options" <<<"$hdrs"; then |
| 72 | ok "X-Content-Type-Options set" |
| 73 | else |
| 74 | bad "X-Content-Type-Options missing" |
| 75 | fi |
| 76 | |
| 77 | # 6. Docs subdomain. |
| 78 | say "GET $DOCS/" |
| 79 | curl -fsS -o /dev/null "$DOCS/" && ok "docs site 200" || bad "docs site" |
| 80 | |
| 81 | # 7. API (only if a PAT is provided). |
| 82 | if [[ -n "${SHITHUB_SMOKE_PAT:-}" ]]; then |
| 83 | say "GET $BASE/api/v1/user (with PAT)" |
| 84 | body=$(curl -fsS -H "Authorization: Bearer $SHITHUB_SMOKE_PAT" "$BASE/api/v1/user") || { bad "api fetch"; body=""; } |
| 85 | if [[ "$body" == *'"username"'* ]]; then |
| 86 | ok "API returned a user object" |
| 87 | else |
| 88 | bad "API response unexpected: $body" |
| 89 | fi |
| 90 | else |
| 91 | printf ' SKIP: API check (set SHITHUB_SMOKE_PAT to run)\n' |
| 92 | fi |
| 93 | |
| 94 | printf '\n' |
| 95 | if [[ "$fail" -eq 0 ]]; then |
| 96 | echo "smoke: all checks passed" |
| 97 | exit 0 |
| 98 | fi |
| 99 | echo "smoke: $fail check(s) FAILED" |
| 100 | exit 1 |