@biffo/cli 0.273.12 → 0.273.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_skeletons/plugin-template/scripts/routing-smoke-test.sh +208 -0
- package/_skeletons/plugin-template/scripts/routing-smoke-test.test.sh +207 -0
- package/_skeletons/sibling-template/.github/workflows/ci.yml +8 -0
- package/_skeletons/sibling-template/.github/workflows/deploy.yml +17 -0
- package/_skeletons/sibling-template/scripts/routing-smoke-test.sh +208 -0
- package/_skeletons/sibling-template/scripts/routing-smoke-test.test.sh +207 -0
- package/package.json +1 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
#
|
|
3
|
+
# Does the sibling actually route, not just deploy?
|
|
4
|
+
#
|
|
5
|
+
# ## Why this exists (#737 item 3)
|
|
6
|
+
#
|
|
7
|
+
# The Lambda smoke test added by #162 (`deploy.yml`'s "Smoke test the deployed
|
|
8
|
+
# Lambda" step) proves the Lambda boots and answers a direct API Gateway
|
|
9
|
+
# request. It says nothing about the CDN path a real user hits:
|
|
10
|
+
# `dev.<domain>/<path>`. One sibling went fully green three separate times
|
|
11
|
+
# while that path was broken in three different ways:
|
|
12
|
+
#
|
|
13
|
+
# 1. CDN registration no-op — the path never got a CloudFront behaviour,
|
|
14
|
+
# so the request silently fell through to
|
|
15
|
+
# the PARENT's default behaviour (the root
|
|
16
|
+
# app) instead of 404ing.
|
|
17
|
+
# 2. Path-prefix mismatch — the app's basePath didn't match the prefix
|
|
18
|
+
# CloudFront forwards, so the prefixed path
|
|
19
|
+
# 404'd even though the app was live.
|
|
20
|
+
# 3. Bare-path routing — the prefix resolved with a trailing slash
|
|
21
|
+
# but not without one (or vice versa).
|
|
22
|
+
#
|
|
23
|
+
# A generic "does the origin respond" check passes in all three cases —
|
|
24
|
+
# variant 1 gets a real 200 (from the WRONG origin), and variants 2/3 differ
|
|
25
|
+
# only in exactly which shape of the URL is broken. So this checks three
|
|
26
|
+
# specific routes, not one generic one, and prints how many it checked and
|
|
27
|
+
# which — "routing OK" over a denominator of zero is the #1363 class.
|
|
28
|
+
#
|
|
29
|
+
# ## Fail-closed
|
|
30
|
+
#
|
|
31
|
+
# 0 = every route checked resolved correctly.
|
|
32
|
+
# 1 = a route was checked and did NOT resolve correctly (a real routing bug).
|
|
33
|
+
# 2 = could not tell — DNS didn't resolve, the deployment was unreachable, or
|
|
34
|
+
# the expected path could not be determined. 2 is NEVER a pass; callers
|
|
35
|
+
# (deploy.yml) must treat it as a failure exactly like 1.
|
|
36
|
+
#
|
|
37
|
+
# ## Usage
|
|
38
|
+
#
|
|
39
|
+
# ROUTING_BASE_URL=https://dev.example.com \
|
|
40
|
+
# ROUTING_PATH_PREFIX=reports \
|
|
41
|
+
# sh scripts/routing-smoke-test.sh
|
|
42
|
+
#
|
|
43
|
+
# ROUTING_BASE_URL — the parent CDN origin the sibling is served through
|
|
44
|
+
# (CORE_PORTAL_URL in deploy.yml). Required.
|
|
45
|
+
# ROUTING_PATH_PREFIX — the sibling's routed path segment (PATH_PREFIX in
|
|
46
|
+
# deploy.yml). Required to be SET, but may be the empty
|
|
47
|
+
# string — the root application sibling (issue #306)
|
|
48
|
+
# legitimately has one. Unset (vs set-empty) is treated
|
|
49
|
+
# as "cannot determine the expected path" and exits 2,
|
|
50
|
+
# because those are different facts: one is a known
|
|
51
|
+
# root sibling, the other is a workflow that forgot to
|
|
52
|
+
# pass the variable.
|
|
53
|
+
# ROUTING_CURL — override the curl binary/wrapper (tests use this to
|
|
54
|
+
# point at a stub).
|
|
55
|
+
#
|
|
56
|
+
# Run this file's own tests: sh scripts/routing-smoke-test.test.sh
|
|
57
|
+
|
|
58
|
+
set -u
|
|
59
|
+
|
|
60
|
+
CURL=${ROUTING_CURL:-curl}
|
|
61
|
+
CHECKED=0
|
|
62
|
+
FAILED=0
|
|
63
|
+
CANNOT_TELL=0
|
|
64
|
+
|
|
65
|
+
_report() {
|
|
66
|
+
# _report <status> <label> <detail>
|
|
67
|
+
status=$1
|
|
68
|
+
label=$2
|
|
69
|
+
detail=$3
|
|
70
|
+
case "$status" in
|
|
71
|
+
ok) echo " [OK] $label — $detail" ;;
|
|
72
|
+
fail) echo " [FAIL] $label — $detail"; FAILED=$((FAILED + 1)) ;;
|
|
73
|
+
cannot-tell) echo " [????] $label — $detail"; CANNOT_TELL=$((CANNOT_TELL + 1)) ;;
|
|
74
|
+
esac
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# _fetch <url> -> prints "<http_status>\n<body_sha256>" or "" on transport
|
|
78
|
+
# failure (DNS, connection refused, timeout — curl exit != 0).
|
|
79
|
+
_fetch() {
|
|
80
|
+
url=$1
|
|
81
|
+
out=$("$CURL" -sS --max-time 15 --retry 2 --retry-delay 2 \
|
|
82
|
+
-o /tmp/routing-smoke-body.$$ -w '%{http_code}' "$url" 2>/tmp/routing-smoke-err.$$)
|
|
83
|
+
rc=$?
|
|
84
|
+
if [ "$rc" -ne 0 ]; then
|
|
85
|
+
err=$(cat /tmp/routing-smoke-err.$$ 2>/dev/null)
|
|
86
|
+
rm -f /tmp/routing-smoke-body.$$ /tmp/routing-smoke-err.$$
|
|
87
|
+
echo "TRANSPORT_ERROR: $err"
|
|
88
|
+
return 1
|
|
89
|
+
fi
|
|
90
|
+
body_hash=$(cksum /tmp/routing-smoke-body.$$ 2>/dev/null | awk '{print $1"-"$2}')
|
|
91
|
+
rm -f /tmp/routing-smoke-body.$$ /tmp/routing-smoke-err.$$
|
|
92
|
+
echo "$out $body_hash"
|
|
93
|
+
return 0
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if [ -z "${ROUTING_BASE_URL+x}" ] || [ -z "$ROUTING_BASE_URL" ]; then
|
|
97
|
+
echo "::error::routing-smoke-test: ROUTING_BASE_URL is not set — cannot determine which deployment to check." >&2
|
|
98
|
+
exit 2
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
if [ -z "${ROUTING_PATH_PREFIX+x}" ]; then
|
|
102
|
+
echo "::error::routing-smoke-test: ROUTING_PATH_PREFIX is unset (not even empty) — cannot determine the expected path. If this is genuinely the root sibling, pass ROUTING_PATH_PREFIX='' explicitly." >&2
|
|
103
|
+
exit 2
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
BASE=${ROUTING_BASE_URL%/}
|
|
107
|
+
PREFIX=$ROUTING_PATH_PREFIX
|
|
108
|
+
|
|
109
|
+
echo "routing-smoke-test: base=$BASE prefix='${PREFIX}'"
|
|
110
|
+
|
|
111
|
+
# --- Route A: bare path, no trailing slash --------------------------------
|
|
112
|
+
# Directly the "bare-path routing" recorded variant.
|
|
113
|
+
if [ -n "$PREFIX" ]; then
|
|
114
|
+
BARE_URL="$BASE/$PREFIX"
|
|
115
|
+
else
|
|
116
|
+
BARE_URL="$BASE"
|
|
117
|
+
fi
|
|
118
|
+
CHECKED=$((CHECKED + 1))
|
|
119
|
+
result=$(_fetch "$BARE_URL")
|
|
120
|
+
if [ $? -ne 0 ]; then
|
|
121
|
+
_report cannot-tell "bare path ($BARE_URL)" "$result"
|
|
122
|
+
else
|
|
123
|
+
code=${result%% *}
|
|
124
|
+
case "$code" in
|
|
125
|
+
2*) _report ok "bare path ($BARE_URL)" "HTTP $code" ;;
|
|
126
|
+
*) _report fail "bare path ($BARE_URL)" "HTTP $code — expected 2xx" ;;
|
|
127
|
+
esac
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# --- Route B: trailing slash, path prefix serving -------------------------
|
|
131
|
+
# Directly the "path-prefix mismatch" recorded variant: the app's basePath
|
|
132
|
+
# not matching what CloudFront forwards shows up as this route 404ing even
|
|
133
|
+
# though the app is live and the bare path (or root) is fine.
|
|
134
|
+
if [ -n "$PREFIX" ]; then
|
|
135
|
+
PREFIXED_URL="$BASE/$PREFIX/"
|
|
136
|
+
CHECKED=$((CHECKED + 1))
|
|
137
|
+
result=$(_fetch "$PREFIXED_URL")
|
|
138
|
+
if [ $? -ne 0 ]; then
|
|
139
|
+
_report cannot-tell "path prefix ($PREFIXED_URL)" "$result"
|
|
140
|
+
else
|
|
141
|
+
code=${result%% *}
|
|
142
|
+
case "$code" in
|
|
143
|
+
2*) _report ok "path prefix ($PREFIXED_URL)" "HTTP $code" ;;
|
|
144
|
+
*) _report fail "path prefix ($PREFIXED_URL)" "HTTP $code — expected 2xx" ;;
|
|
145
|
+
esac
|
|
146
|
+
fi
|
|
147
|
+
else
|
|
148
|
+
echo " [SKIP] path prefix — PATH_PREFIX is empty (root sibling, issue #306); trailing-slash form is identical to the bare-path check above, by design."
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# --- Route C: CDN behaviour actually registered ---------------------------
|
|
152
|
+
# The CDN-no-op variant: if no distinct CloudFront behaviour was ever
|
|
153
|
+
# registered for this path, the request falls through to the PARENT's
|
|
154
|
+
# default behaviour and returns a real 200 — from the WRONG origin. A bare
|
|
155
|
+
# status check cannot see this; it must compare against a known-different
|
|
156
|
+
# response. Only meaningful when PREFIX is non-empty (the root sibling IS
|
|
157
|
+
# the default behaviour, so there is nothing to differ from — that case is
|
|
158
|
+
# recorded as unproven below, not silently skipped as a pass).
|
|
159
|
+
if [ -n "$PREFIX" ]; then
|
|
160
|
+
ROOT_URL="$BASE/"
|
|
161
|
+
CHECKED=$((CHECKED + 1))
|
|
162
|
+
prefixed_result=$(_fetch "$BASE/$PREFIX/")
|
|
163
|
+
prefixed_rc=$?
|
|
164
|
+
root_result=$(_fetch "$ROOT_URL")
|
|
165
|
+
root_rc=$?
|
|
166
|
+
if [ "$prefixed_rc" -ne 0 ] || [ "$root_rc" -ne 0 ]; then
|
|
167
|
+
_report cannot-tell "CDN behaviour registered (vs $ROOT_URL)" "could not fetch one or both paths for comparison"
|
|
168
|
+
else
|
|
169
|
+
prefixed_hash=${prefixed_result#* }
|
|
170
|
+
root_hash=${root_result#* }
|
|
171
|
+
prefixed_code=${prefixed_result%% *}
|
|
172
|
+
case "$prefixed_code" in
|
|
173
|
+
2*)
|
|
174
|
+
if [ "$prefixed_hash" = "$root_hash" ]; then
|
|
175
|
+
_report fail "CDN behaviour registered" "response at $BASE/$PREFIX/ is byte-identical to $ROOT_URL — CloudFront is serving the PARENT's default behaviour, not this sibling (registration no-op)"
|
|
176
|
+
else
|
|
177
|
+
_report ok "CDN behaviour registered" "response at $BASE/$PREFIX/ differs from $ROOT_URL"
|
|
178
|
+
fi
|
|
179
|
+
;;
|
|
180
|
+
*)
|
|
181
|
+
_report fail "CDN behaviour registered" "prefixed path did not return 2xx (HTTP $prefixed_code), cannot compare bodies"
|
|
182
|
+
;;
|
|
183
|
+
esac
|
|
184
|
+
fi
|
|
185
|
+
else
|
|
186
|
+
echo " [UNPROVEN] CDN behaviour registered — PATH_PREFIX is empty (root sibling), so there is no distinct-from-root comparison this script can make. Root-sibling CDN registration is NOT covered by this check."
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
echo
|
|
190
|
+
echo "routing-smoke-test: checked $CHECKED route(s) against $BASE (prefix='${PREFIX}')"
|
|
191
|
+
|
|
192
|
+
if [ "$CANNOT_TELL" -gt 0 ]; then
|
|
193
|
+
echo "::error::routing-smoke-test: $CANNOT_TELL of $CHECKED check(s) could not be evaluated (unreachable / DNS / transport error) — treating as failure, never a pass."
|
|
194
|
+
exit 2
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
if [ "$FAILED" -gt 0 ]; then
|
|
198
|
+
echo "::error::routing-smoke-test: $FAILED of $CHECKED route check(s) failed."
|
|
199
|
+
exit 1
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
if [ "$CHECKED" -eq 0 ]; then
|
|
203
|
+
echo "::error::routing-smoke-test: 0 routes were checked — refusing to report success over an empty denominator (#1363)."
|
|
204
|
+
exit 2
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
echo "routing-smoke-test: all $CHECKED checked route(s) OK."
|
|
208
|
+
exit 0
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
#
|
|
3
|
+
# Proves routing-smoke-test.sh (#737 item 3) actually catches the three
|
|
4
|
+
# recorded routing failures, and that it fails closed when it cannot tell.
|
|
5
|
+
#
|
|
6
|
+
# A verification script nobody has seen fail is not verified — every FAIL
|
|
7
|
+
# scenario below first ran (uncommented) against a plain `curl` stub before
|
|
8
|
+
# the real script existed, to confirm it does NOT catch these shapes without
|
|
9
|
+
# the fix; that manual step is recorded here in prose since there is nothing
|
|
10
|
+
# upstream of this script to diff against.
|
|
11
|
+
#
|
|
12
|
+
# Stubs `curl` on PATH so this needs no network and no live deployment.
|
|
13
|
+
#
|
|
14
|
+
# Run: sh scripts/routing-smoke-test.test.sh
|
|
15
|
+
|
|
16
|
+
set -u
|
|
17
|
+
|
|
18
|
+
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
19
|
+
TARGET="$SCRIPT_DIR/routing-smoke-test.sh"
|
|
20
|
+
STUB_DIR=$(mktemp -d)
|
|
21
|
+
trap 'rm -rf "$STUB_DIR"' EXIT
|
|
22
|
+
|
|
23
|
+
FAILURES=0
|
|
24
|
+
_assert_exit() {
|
|
25
|
+
# _assert_exit <scenario-name> <expected-exit> -- <env assignments already exported>
|
|
26
|
+
name=$1
|
|
27
|
+
expected=$2
|
|
28
|
+
actual_out=$(ROUTING_CURL="$STUB_DIR/curl" sh "$TARGET" 2>&1)
|
|
29
|
+
actual=$?
|
|
30
|
+
if [ "$actual" -eq "$expected" ]; then
|
|
31
|
+
echo "PASS: $name (exit $actual)"
|
|
32
|
+
else
|
|
33
|
+
echo "FAIL: $name — expected exit $expected, got $actual"
|
|
34
|
+
echo "--- output ---"
|
|
35
|
+
echo "$actual_out"
|
|
36
|
+
echo "--------------"
|
|
37
|
+
FAILURES=$((FAILURES + 1))
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
BASE="https://dev.example.com"
|
|
42
|
+
PREFIX="reports"
|
|
43
|
+
|
|
44
|
+
# --- Stub curl factory ------------------------------------------------
|
|
45
|
+
# Each scenario writes its own stub before calling the target, since the
|
|
46
|
+
# response shape (which URL gets which body/status) IS the scenario.
|
|
47
|
+
_write_stub() {
|
|
48
|
+
cat > "$STUB_DIR/curl" <<'STUB'
|
|
49
|
+
#!/usr/bin/env sh
|
|
50
|
+
outfile=""
|
|
51
|
+
prev=""
|
|
52
|
+
for a in "$@"; do
|
|
53
|
+
case "$prev" in
|
|
54
|
+
-o) outfile="$a" ;;
|
|
55
|
+
esac
|
|
56
|
+
prev="$a"
|
|
57
|
+
done
|
|
58
|
+
url="$prev"
|
|
59
|
+
|
|
60
|
+
# ROUTING_TEST_MODE selects the fixture; each case below writes $outfile
|
|
61
|
+
# and prints the http status, mirroring curl -w '%{http_code}'.
|
|
62
|
+
case "$ROUTING_TEST_MODE" in
|
|
63
|
+
good)
|
|
64
|
+
case "$url" in
|
|
65
|
+
https://dev.example.com/reports) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
66
|
+
https://dev.example.com/reports/) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
67
|
+
https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
68
|
+
esac
|
|
69
|
+
;;
|
|
70
|
+
cdn-noop)
|
|
71
|
+
# Every path — including the prefixed one — actually serves the root
|
|
72
|
+
# app's content, because CloudFront never registered a distinct
|
|
73
|
+
# behaviour for /reports and silently fell through to the default.
|
|
74
|
+
case "$url" in
|
|
75
|
+
https://dev.example.com/reports) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
76
|
+
https://dev.example.com/reports/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
77
|
+
https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
78
|
+
esac
|
|
79
|
+
;;
|
|
80
|
+
prefix-mismatch)
|
|
81
|
+
# The app serves a DIFFERENT basePath than PATH_PREFIX declares, so the
|
|
82
|
+
# trailing-slash form 404s even though bare and root are fine.
|
|
83
|
+
case "$url" in
|
|
84
|
+
https://dev.example.com/reports) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
85
|
+
https://dev.example.com/reports/) printf 'Not Found' > "$outfile"; printf '404'; exit 0 ;;
|
|
86
|
+
https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
87
|
+
esac
|
|
88
|
+
;;
|
|
89
|
+
bare-path-broken)
|
|
90
|
+
# Trailing-slash form works, bare form 404s (or vice versa — either
|
|
91
|
+
# asymmetry is the recorded "bare-path routing" variant).
|
|
92
|
+
case "$url" in
|
|
93
|
+
https://dev.example.com/reports) printf 'Not Found' > "$outfile"; printf '404'; exit 0 ;;
|
|
94
|
+
https://dev.example.com/reports/) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
95
|
+
https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
96
|
+
esac
|
|
97
|
+
;;
|
|
98
|
+
unreachable)
|
|
99
|
+
printf 'curl: (6) Could not resolve host: dev.example.com' >&2
|
|
100
|
+
exit 6
|
|
101
|
+
;;
|
|
102
|
+
esac
|
|
103
|
+
echo "unhandled URL in stub: $url" >&2
|
|
104
|
+
exit 99
|
|
105
|
+
STUB
|
|
106
|
+
chmod +x "$STUB_DIR/curl"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# 1. GOOD deployment — all three checks should pass, exit 0.
|
|
110
|
+
_write_stub
|
|
111
|
+
ROUTING_TEST_MODE=good ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
|
|
112
|
+
_assert_exit "good deployment" 0
|
|
113
|
+
|
|
114
|
+
# 2. CDN registration no-op — prefixed path byte-identical to root.
|
|
115
|
+
# This is the variant a generic "does it 200" check CANNOT catch, because
|
|
116
|
+
# every URL here genuinely returns 200. Confirms the fix, not just a
|
|
117
|
+
# curl failure.
|
|
118
|
+
_write_stub
|
|
119
|
+
ROUTING_TEST_MODE=cdn-noop ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
|
|
120
|
+
_assert_exit "CDN registration no-op" 1
|
|
121
|
+
|
|
122
|
+
# 3. Path-prefix mismatch — trailing-slash form 404s.
|
|
123
|
+
_write_stub
|
|
124
|
+
ROUTING_TEST_MODE=prefix-mismatch ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
|
|
125
|
+
_assert_exit "path-prefix mismatch" 1
|
|
126
|
+
|
|
127
|
+
# 4. Bare-path routing — bare form 404s while trailing-slash works.
|
|
128
|
+
_write_stub
|
|
129
|
+
ROUTING_TEST_MODE=bare-path-broken ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
|
|
130
|
+
_assert_exit "bare-path routing broken" 1
|
|
131
|
+
|
|
132
|
+
# 5. Unreachable deployment — DNS/transport failure must fail CLOSED (exit
|
|
133
|
+
# 2, cannot-tell), never exit 0.
|
|
134
|
+
_write_stub
|
|
135
|
+
ROUTING_TEST_MODE=unreachable ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
|
|
136
|
+
_assert_exit "unreachable deployment" 2
|
|
137
|
+
|
|
138
|
+
# 6. ROUTING_PATH_PREFIX genuinely unset (not empty) — cannot determine the
|
|
139
|
+
# expected path, must exit 2 without even invoking curl.
|
|
140
|
+
_write_stub
|
|
141
|
+
env -u ROUTING_PATH_PREFIX ROUTING_TEST_MODE=good ROUTING_BASE_URL="$BASE" \
|
|
142
|
+
sh -c 'ROUTING_CURL="'"$STUB_DIR"'/curl" sh "'"$TARGET"'"' >/tmp/routing-smoke-t6.$$ 2>&1
|
|
143
|
+
rc=$?
|
|
144
|
+
if [ "$rc" -eq 2 ]; then
|
|
145
|
+
echo "PASS: PATH_PREFIX unset (exit 2)"
|
|
146
|
+
else
|
|
147
|
+
echo "FAIL: PATH_PREFIX unset — expected exit 2, got $rc"
|
|
148
|
+
cat /tmp/routing-smoke-t6.$$
|
|
149
|
+
FAILURES=$((FAILURES + 1))
|
|
150
|
+
fi
|
|
151
|
+
rm -f /tmp/routing-smoke-t6.$$
|
|
152
|
+
|
|
153
|
+
# 7. ROUTING_BASE_URL unset entirely — must exit 2.
|
|
154
|
+
_write_stub
|
|
155
|
+
env -u ROUTING_BASE_URL ROUTING_TEST_MODE=good ROUTING_PATH_PREFIX="$PREFIX" \
|
|
156
|
+
sh -c 'ROUTING_CURL="'"$STUB_DIR"'/curl" sh "'"$TARGET"'"' >/tmp/routing-smoke-t7.$$ 2>&1
|
|
157
|
+
rc=$?
|
|
158
|
+
if [ "$rc" -eq 2 ]; then
|
|
159
|
+
echo "PASS: BASE_URL unset (exit 2)"
|
|
160
|
+
else
|
|
161
|
+
echo "FAIL: BASE_URL unset — expected exit 2, got $rc"
|
|
162
|
+
cat /tmp/routing-smoke-t7.$$
|
|
163
|
+
FAILURES=$((FAILURES + 1))
|
|
164
|
+
fi
|
|
165
|
+
rm -f /tmp/routing-smoke-t7.$$
|
|
166
|
+
|
|
167
|
+
# 8. Root sibling (PATH_PREFIX='', deliberately empty not unset) — the
|
|
168
|
+
# CDN-behaviour-registered check has nothing to compare against and must
|
|
169
|
+
# say so rather than silently passing; bare-path check still runs
|
|
170
|
+
# (denominator stays > 0), so exit is 0 when that succeeds.
|
|
171
|
+
_write_stub
|
|
172
|
+
cat > "$STUB_DIR/curl" <<'STUB'
|
|
173
|
+
#!/usr/bin/env sh
|
|
174
|
+
outfile=""
|
|
175
|
+
prev=""
|
|
176
|
+
for a in "$@"; do
|
|
177
|
+
case "$prev" in
|
|
178
|
+
-o) outfile="$a" ;;
|
|
179
|
+
esac
|
|
180
|
+
prev="$a"
|
|
181
|
+
done
|
|
182
|
+
url="$prev"
|
|
183
|
+
case "$url" in
|
|
184
|
+
https://dev.example.com) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
185
|
+
esac
|
|
186
|
+
echo "unhandled URL in stub: $url" >&2
|
|
187
|
+
exit 99
|
|
188
|
+
STUB
|
|
189
|
+
chmod +x "$STUB_DIR/curl"
|
|
190
|
+
out=$(ROUTING_CURL="$STUB_DIR/curl" ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="" sh "$TARGET" 2>&1)
|
|
191
|
+
rc=$?
|
|
192
|
+
if [ "$rc" -eq 0 ] && echo "$out" | grep -q "UNPROVEN"; then
|
|
193
|
+
echo "PASS: root sibling (exit 0, unproven CDN check reported)"
|
|
194
|
+
else
|
|
195
|
+
echo "FAIL: root sibling — expected exit 0 with UNPROVEN noted, got exit $rc"
|
|
196
|
+
echo "$out"
|
|
197
|
+
FAILURES=$((FAILURES + 1))
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
echo
|
|
201
|
+
if [ "$FAILURES" -eq 0 ]; then
|
|
202
|
+
echo "routing-smoke-test.test.sh: all checks passed."
|
|
203
|
+
exit 0
|
|
204
|
+
else
|
|
205
|
+
echo "routing-smoke-test.test.sh: $FAILURES check(s) failed."
|
|
206
|
+
exit 1
|
|
207
|
+
fi
|
|
@@ -216,6 +216,14 @@ jobs:
|
|
|
216
216
|
# services/api, which is the environment `uv sync` installed and so the
|
|
217
217
|
# one to audit.
|
|
218
218
|
run: sh ../../scripts/py-dependency-audit.sh
|
|
219
|
+
- name: Routing smoke test — self-test
|
|
220
|
+
if: ${{ !cancelled() }}
|
|
221
|
+
# Not exercising this repo's own deployment (CI has none to reach) —
|
|
222
|
+
# proves scripts/routing-smoke-test.sh, the deploy.yml post-deploy
|
|
223
|
+
# routing check (#737), still catches the three recorded CDN routing
|
|
224
|
+
# failures against a stubbed curl. A verification script with no
|
|
225
|
+
# caller is the class tracked in #1413; this is that caller.
|
|
226
|
+
run: sh ../../scripts/routing-smoke-test.test.sh
|
|
219
227
|
|
|
220
228
|
security-secrets:
|
|
221
229
|
name: Secret Scan
|
|
@@ -308,6 +308,23 @@ jobs:
|
|
|
308
308
|
echo "::error::Health check failed after deploy — the Lambda uploaded but isn't serving requests. Check CloudWatch logs for ${{ vars.LAMBDA_FUNCTION_NAME }}."
|
|
309
309
|
exit 1
|
|
310
310
|
fi
|
|
311
|
+
- name: Smoke test the CDN routing (dev.<domain>/<path>)
|
|
312
|
+
# The Lambda check above proves the API boots. It says nothing about
|
|
313
|
+
# the URL a real user hits — `${{ vars.CORE_PORTAL_URL }}/${{
|
|
314
|
+
# vars.PATH_PREFIX }}` — and one sibling went green here three times
|
|
315
|
+
# while that route was broken three different ways: a CDN behaviour
|
|
316
|
+
# that never actually got registered (silently falling through to the
|
|
317
|
+
# PARENT's default behaviour instead of this sibling), a path-prefix
|
|
318
|
+
# mismatch between the app's basePath and what CloudFront forwards,
|
|
319
|
+
# and the bare path (no trailing slash) not resolving. See
|
|
320
|
+
# scripts/routing-smoke-test.sh for how each is checked and why a
|
|
321
|
+
# generic "does it 200" probe would have passed on every one of them.
|
|
322
|
+
# Exits 0/1/2 (2 = cannot tell, never a pass); ROUTING_CURL is unset
|
|
323
|
+
# here so it uses the real `curl` — only the script's own tests stub it.
|
|
324
|
+
env:
|
|
325
|
+
ROUTING_BASE_URL: ${{ vars.CORE_PORTAL_URL }}
|
|
326
|
+
ROUTING_PATH_PREFIX: ${{ vars.PATH_PREFIX }}
|
|
327
|
+
run: sh scripts/routing-smoke-test.sh
|
|
311
328
|
|
|
312
329
|
# A Deploy run that deploys NOTHING must not report success (#1065).
|
|
313
330
|
#
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
#
|
|
3
|
+
# Does the sibling actually route, not just deploy?
|
|
4
|
+
#
|
|
5
|
+
# ## Why this exists (#737 item 3)
|
|
6
|
+
#
|
|
7
|
+
# The Lambda smoke test added by #162 (`deploy.yml`'s "Smoke test the deployed
|
|
8
|
+
# Lambda" step) proves the Lambda boots and answers a direct API Gateway
|
|
9
|
+
# request. It says nothing about the CDN path a real user hits:
|
|
10
|
+
# `dev.<domain>/<path>`. One sibling went fully green three separate times
|
|
11
|
+
# while that path was broken in three different ways:
|
|
12
|
+
#
|
|
13
|
+
# 1. CDN registration no-op — the path never got a CloudFront behaviour,
|
|
14
|
+
# so the request silently fell through to
|
|
15
|
+
# the PARENT's default behaviour (the root
|
|
16
|
+
# app) instead of 404ing.
|
|
17
|
+
# 2. Path-prefix mismatch — the app's basePath didn't match the prefix
|
|
18
|
+
# CloudFront forwards, so the prefixed path
|
|
19
|
+
# 404'd even though the app was live.
|
|
20
|
+
# 3. Bare-path routing — the prefix resolved with a trailing slash
|
|
21
|
+
# but not without one (or vice versa).
|
|
22
|
+
#
|
|
23
|
+
# A generic "does the origin respond" check passes in all three cases —
|
|
24
|
+
# variant 1 gets a real 200 (from the WRONG origin), and variants 2/3 differ
|
|
25
|
+
# only in exactly which shape of the URL is broken. So this checks three
|
|
26
|
+
# specific routes, not one generic one, and prints how many it checked and
|
|
27
|
+
# which — "routing OK" over a denominator of zero is the #1363 class.
|
|
28
|
+
#
|
|
29
|
+
# ## Fail-closed
|
|
30
|
+
#
|
|
31
|
+
# 0 = every route checked resolved correctly.
|
|
32
|
+
# 1 = a route was checked and did NOT resolve correctly (a real routing bug).
|
|
33
|
+
# 2 = could not tell — DNS didn't resolve, the deployment was unreachable, or
|
|
34
|
+
# the expected path could not be determined. 2 is NEVER a pass; callers
|
|
35
|
+
# (deploy.yml) must treat it as a failure exactly like 1.
|
|
36
|
+
#
|
|
37
|
+
# ## Usage
|
|
38
|
+
#
|
|
39
|
+
# ROUTING_BASE_URL=https://dev.example.com \
|
|
40
|
+
# ROUTING_PATH_PREFIX=reports \
|
|
41
|
+
# sh scripts/routing-smoke-test.sh
|
|
42
|
+
#
|
|
43
|
+
# ROUTING_BASE_URL — the parent CDN origin the sibling is served through
|
|
44
|
+
# (CORE_PORTAL_URL in deploy.yml). Required.
|
|
45
|
+
# ROUTING_PATH_PREFIX — the sibling's routed path segment (PATH_PREFIX in
|
|
46
|
+
# deploy.yml). Required to be SET, but may be the empty
|
|
47
|
+
# string — the root application sibling (issue #306)
|
|
48
|
+
# legitimately has one. Unset (vs set-empty) is treated
|
|
49
|
+
# as "cannot determine the expected path" and exits 2,
|
|
50
|
+
# because those are different facts: one is a known
|
|
51
|
+
# root sibling, the other is a workflow that forgot to
|
|
52
|
+
# pass the variable.
|
|
53
|
+
# ROUTING_CURL — override the curl binary/wrapper (tests use this to
|
|
54
|
+
# point at a stub).
|
|
55
|
+
#
|
|
56
|
+
# Run this file's own tests: sh scripts/routing-smoke-test.test.sh
|
|
57
|
+
|
|
58
|
+
set -u
|
|
59
|
+
|
|
60
|
+
CURL=${ROUTING_CURL:-curl}
|
|
61
|
+
CHECKED=0
|
|
62
|
+
FAILED=0
|
|
63
|
+
CANNOT_TELL=0
|
|
64
|
+
|
|
65
|
+
_report() {
|
|
66
|
+
# _report <status> <label> <detail>
|
|
67
|
+
status=$1
|
|
68
|
+
label=$2
|
|
69
|
+
detail=$3
|
|
70
|
+
case "$status" in
|
|
71
|
+
ok) echo " [OK] $label — $detail" ;;
|
|
72
|
+
fail) echo " [FAIL] $label — $detail"; FAILED=$((FAILED + 1)) ;;
|
|
73
|
+
cannot-tell) echo " [????] $label — $detail"; CANNOT_TELL=$((CANNOT_TELL + 1)) ;;
|
|
74
|
+
esac
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# _fetch <url> -> prints "<http_status>\n<body_sha256>" or "" on transport
|
|
78
|
+
# failure (DNS, connection refused, timeout — curl exit != 0).
|
|
79
|
+
_fetch() {
|
|
80
|
+
url=$1
|
|
81
|
+
out=$("$CURL" -sS --max-time 15 --retry 2 --retry-delay 2 \
|
|
82
|
+
-o /tmp/routing-smoke-body.$$ -w '%{http_code}' "$url" 2>/tmp/routing-smoke-err.$$)
|
|
83
|
+
rc=$?
|
|
84
|
+
if [ "$rc" -ne 0 ]; then
|
|
85
|
+
err=$(cat /tmp/routing-smoke-err.$$ 2>/dev/null)
|
|
86
|
+
rm -f /tmp/routing-smoke-body.$$ /tmp/routing-smoke-err.$$
|
|
87
|
+
echo "TRANSPORT_ERROR: $err"
|
|
88
|
+
return 1
|
|
89
|
+
fi
|
|
90
|
+
body_hash=$(cksum /tmp/routing-smoke-body.$$ 2>/dev/null | awk '{print $1"-"$2}')
|
|
91
|
+
rm -f /tmp/routing-smoke-body.$$ /tmp/routing-smoke-err.$$
|
|
92
|
+
echo "$out $body_hash"
|
|
93
|
+
return 0
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if [ -z "${ROUTING_BASE_URL+x}" ] || [ -z "$ROUTING_BASE_URL" ]; then
|
|
97
|
+
echo "::error::routing-smoke-test: ROUTING_BASE_URL is not set — cannot determine which deployment to check." >&2
|
|
98
|
+
exit 2
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
if [ -z "${ROUTING_PATH_PREFIX+x}" ]; then
|
|
102
|
+
echo "::error::routing-smoke-test: ROUTING_PATH_PREFIX is unset (not even empty) — cannot determine the expected path. If this is genuinely the root sibling, pass ROUTING_PATH_PREFIX='' explicitly." >&2
|
|
103
|
+
exit 2
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
BASE=${ROUTING_BASE_URL%/}
|
|
107
|
+
PREFIX=$ROUTING_PATH_PREFIX
|
|
108
|
+
|
|
109
|
+
echo "routing-smoke-test: base=$BASE prefix='${PREFIX}'"
|
|
110
|
+
|
|
111
|
+
# --- Route A: bare path, no trailing slash --------------------------------
|
|
112
|
+
# Directly the "bare-path routing" recorded variant.
|
|
113
|
+
if [ -n "$PREFIX" ]; then
|
|
114
|
+
BARE_URL="$BASE/$PREFIX"
|
|
115
|
+
else
|
|
116
|
+
BARE_URL="$BASE"
|
|
117
|
+
fi
|
|
118
|
+
CHECKED=$((CHECKED + 1))
|
|
119
|
+
result=$(_fetch "$BARE_URL")
|
|
120
|
+
if [ $? -ne 0 ]; then
|
|
121
|
+
_report cannot-tell "bare path ($BARE_URL)" "$result"
|
|
122
|
+
else
|
|
123
|
+
code=${result%% *}
|
|
124
|
+
case "$code" in
|
|
125
|
+
2*) _report ok "bare path ($BARE_URL)" "HTTP $code" ;;
|
|
126
|
+
*) _report fail "bare path ($BARE_URL)" "HTTP $code — expected 2xx" ;;
|
|
127
|
+
esac
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# --- Route B: trailing slash, path prefix serving -------------------------
|
|
131
|
+
# Directly the "path-prefix mismatch" recorded variant: the app's basePath
|
|
132
|
+
# not matching what CloudFront forwards shows up as this route 404ing even
|
|
133
|
+
# though the app is live and the bare path (or root) is fine.
|
|
134
|
+
if [ -n "$PREFIX" ]; then
|
|
135
|
+
PREFIXED_URL="$BASE/$PREFIX/"
|
|
136
|
+
CHECKED=$((CHECKED + 1))
|
|
137
|
+
result=$(_fetch "$PREFIXED_URL")
|
|
138
|
+
if [ $? -ne 0 ]; then
|
|
139
|
+
_report cannot-tell "path prefix ($PREFIXED_URL)" "$result"
|
|
140
|
+
else
|
|
141
|
+
code=${result%% *}
|
|
142
|
+
case "$code" in
|
|
143
|
+
2*) _report ok "path prefix ($PREFIXED_URL)" "HTTP $code" ;;
|
|
144
|
+
*) _report fail "path prefix ($PREFIXED_URL)" "HTTP $code — expected 2xx" ;;
|
|
145
|
+
esac
|
|
146
|
+
fi
|
|
147
|
+
else
|
|
148
|
+
echo " [SKIP] path prefix — PATH_PREFIX is empty (root sibling, issue #306); trailing-slash form is identical to the bare-path check above, by design."
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# --- Route C: CDN behaviour actually registered ---------------------------
|
|
152
|
+
# The CDN-no-op variant: if no distinct CloudFront behaviour was ever
|
|
153
|
+
# registered for this path, the request falls through to the PARENT's
|
|
154
|
+
# default behaviour and returns a real 200 — from the WRONG origin. A bare
|
|
155
|
+
# status check cannot see this; it must compare against a known-different
|
|
156
|
+
# response. Only meaningful when PREFIX is non-empty (the root sibling IS
|
|
157
|
+
# the default behaviour, so there is nothing to differ from — that case is
|
|
158
|
+
# recorded as unproven below, not silently skipped as a pass).
|
|
159
|
+
if [ -n "$PREFIX" ]; then
|
|
160
|
+
ROOT_URL="$BASE/"
|
|
161
|
+
CHECKED=$((CHECKED + 1))
|
|
162
|
+
prefixed_result=$(_fetch "$BASE/$PREFIX/")
|
|
163
|
+
prefixed_rc=$?
|
|
164
|
+
root_result=$(_fetch "$ROOT_URL")
|
|
165
|
+
root_rc=$?
|
|
166
|
+
if [ "$prefixed_rc" -ne 0 ] || [ "$root_rc" -ne 0 ]; then
|
|
167
|
+
_report cannot-tell "CDN behaviour registered (vs $ROOT_URL)" "could not fetch one or both paths for comparison"
|
|
168
|
+
else
|
|
169
|
+
prefixed_hash=${prefixed_result#* }
|
|
170
|
+
root_hash=${root_result#* }
|
|
171
|
+
prefixed_code=${prefixed_result%% *}
|
|
172
|
+
case "$prefixed_code" in
|
|
173
|
+
2*)
|
|
174
|
+
if [ "$prefixed_hash" = "$root_hash" ]; then
|
|
175
|
+
_report fail "CDN behaviour registered" "response at $BASE/$PREFIX/ is byte-identical to $ROOT_URL — CloudFront is serving the PARENT's default behaviour, not this sibling (registration no-op)"
|
|
176
|
+
else
|
|
177
|
+
_report ok "CDN behaviour registered" "response at $BASE/$PREFIX/ differs from $ROOT_URL"
|
|
178
|
+
fi
|
|
179
|
+
;;
|
|
180
|
+
*)
|
|
181
|
+
_report fail "CDN behaviour registered" "prefixed path did not return 2xx (HTTP $prefixed_code), cannot compare bodies"
|
|
182
|
+
;;
|
|
183
|
+
esac
|
|
184
|
+
fi
|
|
185
|
+
else
|
|
186
|
+
echo " [UNPROVEN] CDN behaviour registered — PATH_PREFIX is empty (root sibling), so there is no distinct-from-root comparison this script can make. Root-sibling CDN registration is NOT covered by this check."
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
echo
|
|
190
|
+
echo "routing-smoke-test: checked $CHECKED route(s) against $BASE (prefix='${PREFIX}')"
|
|
191
|
+
|
|
192
|
+
if [ "$CANNOT_TELL" -gt 0 ]; then
|
|
193
|
+
echo "::error::routing-smoke-test: $CANNOT_TELL of $CHECKED check(s) could not be evaluated (unreachable / DNS / transport error) — treating as failure, never a pass."
|
|
194
|
+
exit 2
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
if [ "$FAILED" -gt 0 ]; then
|
|
198
|
+
echo "::error::routing-smoke-test: $FAILED of $CHECKED route check(s) failed."
|
|
199
|
+
exit 1
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
if [ "$CHECKED" -eq 0 ]; then
|
|
203
|
+
echo "::error::routing-smoke-test: 0 routes were checked — refusing to report success over an empty denominator (#1363)."
|
|
204
|
+
exit 2
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
echo "routing-smoke-test: all $CHECKED checked route(s) OK."
|
|
208
|
+
exit 0
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
#
|
|
3
|
+
# Proves routing-smoke-test.sh (#737 item 3) actually catches the three
|
|
4
|
+
# recorded routing failures, and that it fails closed when it cannot tell.
|
|
5
|
+
#
|
|
6
|
+
# A verification script nobody has seen fail is not verified — every FAIL
|
|
7
|
+
# scenario below first ran (uncommented) against a plain `curl` stub before
|
|
8
|
+
# the real script existed, to confirm it does NOT catch these shapes without
|
|
9
|
+
# the fix; that manual step is recorded here in prose since there is nothing
|
|
10
|
+
# upstream of this script to diff against.
|
|
11
|
+
#
|
|
12
|
+
# Stubs `curl` on PATH so this needs no network and no live deployment.
|
|
13
|
+
#
|
|
14
|
+
# Run: sh scripts/routing-smoke-test.test.sh
|
|
15
|
+
|
|
16
|
+
set -u
|
|
17
|
+
|
|
18
|
+
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
19
|
+
TARGET="$SCRIPT_DIR/routing-smoke-test.sh"
|
|
20
|
+
STUB_DIR=$(mktemp -d)
|
|
21
|
+
trap 'rm -rf "$STUB_DIR"' EXIT
|
|
22
|
+
|
|
23
|
+
FAILURES=0
|
|
24
|
+
_assert_exit() {
|
|
25
|
+
# _assert_exit <scenario-name> <expected-exit> -- <env assignments already exported>
|
|
26
|
+
name=$1
|
|
27
|
+
expected=$2
|
|
28
|
+
actual_out=$(ROUTING_CURL="$STUB_DIR/curl" sh "$TARGET" 2>&1)
|
|
29
|
+
actual=$?
|
|
30
|
+
if [ "$actual" -eq "$expected" ]; then
|
|
31
|
+
echo "PASS: $name (exit $actual)"
|
|
32
|
+
else
|
|
33
|
+
echo "FAIL: $name — expected exit $expected, got $actual"
|
|
34
|
+
echo "--- output ---"
|
|
35
|
+
echo "$actual_out"
|
|
36
|
+
echo "--------------"
|
|
37
|
+
FAILURES=$((FAILURES + 1))
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
BASE="https://dev.example.com"
|
|
42
|
+
PREFIX="reports"
|
|
43
|
+
|
|
44
|
+
# --- Stub curl factory ------------------------------------------------
|
|
45
|
+
# Each scenario writes its own stub before calling the target, since the
|
|
46
|
+
# response shape (which URL gets which body/status) IS the scenario.
|
|
47
|
+
_write_stub() {
|
|
48
|
+
cat > "$STUB_DIR/curl" <<'STUB'
|
|
49
|
+
#!/usr/bin/env sh
|
|
50
|
+
outfile=""
|
|
51
|
+
prev=""
|
|
52
|
+
for a in "$@"; do
|
|
53
|
+
case "$prev" in
|
|
54
|
+
-o) outfile="$a" ;;
|
|
55
|
+
esac
|
|
56
|
+
prev="$a"
|
|
57
|
+
done
|
|
58
|
+
url="$prev"
|
|
59
|
+
|
|
60
|
+
# ROUTING_TEST_MODE selects the fixture; each case below writes $outfile
|
|
61
|
+
# and prints the http status, mirroring curl -w '%{http_code}'.
|
|
62
|
+
case "$ROUTING_TEST_MODE" in
|
|
63
|
+
good)
|
|
64
|
+
case "$url" in
|
|
65
|
+
https://dev.example.com/reports) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
66
|
+
https://dev.example.com/reports/) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
67
|
+
https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
68
|
+
esac
|
|
69
|
+
;;
|
|
70
|
+
cdn-noop)
|
|
71
|
+
# Every path — including the prefixed one — actually serves the root
|
|
72
|
+
# app's content, because CloudFront never registered a distinct
|
|
73
|
+
# behaviour for /reports and silently fell through to the default.
|
|
74
|
+
case "$url" in
|
|
75
|
+
https://dev.example.com/reports) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
76
|
+
https://dev.example.com/reports/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
77
|
+
https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
78
|
+
esac
|
|
79
|
+
;;
|
|
80
|
+
prefix-mismatch)
|
|
81
|
+
# The app serves a DIFFERENT basePath than PATH_PREFIX declares, so the
|
|
82
|
+
# trailing-slash form 404s even though bare and root are fine.
|
|
83
|
+
case "$url" in
|
|
84
|
+
https://dev.example.com/reports) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
85
|
+
https://dev.example.com/reports/) printf 'Not Found' > "$outfile"; printf '404'; exit 0 ;;
|
|
86
|
+
https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
87
|
+
esac
|
|
88
|
+
;;
|
|
89
|
+
bare-path-broken)
|
|
90
|
+
# Trailing-slash form works, bare form 404s (or vice versa — either
|
|
91
|
+
# asymmetry is the recorded "bare-path routing" variant).
|
|
92
|
+
case "$url" in
|
|
93
|
+
https://dev.example.com/reports) printf 'Not Found' > "$outfile"; printf '404'; exit 0 ;;
|
|
94
|
+
https://dev.example.com/reports/) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
95
|
+
https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
96
|
+
esac
|
|
97
|
+
;;
|
|
98
|
+
unreachable)
|
|
99
|
+
printf 'curl: (6) Could not resolve host: dev.example.com' >&2
|
|
100
|
+
exit 6
|
|
101
|
+
;;
|
|
102
|
+
esac
|
|
103
|
+
echo "unhandled URL in stub: $url" >&2
|
|
104
|
+
exit 99
|
|
105
|
+
STUB
|
|
106
|
+
chmod +x "$STUB_DIR/curl"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# 1. GOOD deployment — all three checks should pass, exit 0.
|
|
110
|
+
_write_stub
|
|
111
|
+
ROUTING_TEST_MODE=good ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
|
|
112
|
+
_assert_exit "good deployment" 0
|
|
113
|
+
|
|
114
|
+
# 2. CDN registration no-op — prefixed path byte-identical to root.
|
|
115
|
+
# This is the variant a generic "does it 200" check CANNOT catch, because
|
|
116
|
+
# every URL here genuinely returns 200. Confirms the fix, not just a
|
|
117
|
+
# curl failure.
|
|
118
|
+
_write_stub
|
|
119
|
+
ROUTING_TEST_MODE=cdn-noop ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
|
|
120
|
+
_assert_exit "CDN registration no-op" 1
|
|
121
|
+
|
|
122
|
+
# 3. Path-prefix mismatch — trailing-slash form 404s.
|
|
123
|
+
_write_stub
|
|
124
|
+
ROUTING_TEST_MODE=prefix-mismatch ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
|
|
125
|
+
_assert_exit "path-prefix mismatch" 1
|
|
126
|
+
|
|
127
|
+
# 4. Bare-path routing — bare form 404s while trailing-slash works.
|
|
128
|
+
_write_stub
|
|
129
|
+
ROUTING_TEST_MODE=bare-path-broken ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
|
|
130
|
+
_assert_exit "bare-path routing broken" 1
|
|
131
|
+
|
|
132
|
+
# 5. Unreachable deployment — DNS/transport failure must fail CLOSED (exit
|
|
133
|
+
# 2, cannot-tell), never exit 0.
|
|
134
|
+
_write_stub
|
|
135
|
+
ROUTING_TEST_MODE=unreachable ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
|
|
136
|
+
_assert_exit "unreachable deployment" 2
|
|
137
|
+
|
|
138
|
+
# 6. ROUTING_PATH_PREFIX genuinely unset (not empty) — cannot determine the
|
|
139
|
+
# expected path, must exit 2 without even invoking curl.
|
|
140
|
+
_write_stub
|
|
141
|
+
env -u ROUTING_PATH_PREFIX ROUTING_TEST_MODE=good ROUTING_BASE_URL="$BASE" \
|
|
142
|
+
sh -c 'ROUTING_CURL="'"$STUB_DIR"'/curl" sh "'"$TARGET"'"' >/tmp/routing-smoke-t6.$$ 2>&1
|
|
143
|
+
rc=$?
|
|
144
|
+
if [ "$rc" -eq 2 ]; then
|
|
145
|
+
echo "PASS: PATH_PREFIX unset (exit 2)"
|
|
146
|
+
else
|
|
147
|
+
echo "FAIL: PATH_PREFIX unset — expected exit 2, got $rc"
|
|
148
|
+
cat /tmp/routing-smoke-t6.$$
|
|
149
|
+
FAILURES=$((FAILURES + 1))
|
|
150
|
+
fi
|
|
151
|
+
rm -f /tmp/routing-smoke-t6.$$
|
|
152
|
+
|
|
153
|
+
# 7. ROUTING_BASE_URL unset entirely — must exit 2.
|
|
154
|
+
_write_stub
|
|
155
|
+
env -u ROUTING_BASE_URL ROUTING_TEST_MODE=good ROUTING_PATH_PREFIX="$PREFIX" \
|
|
156
|
+
sh -c 'ROUTING_CURL="'"$STUB_DIR"'/curl" sh "'"$TARGET"'"' >/tmp/routing-smoke-t7.$$ 2>&1
|
|
157
|
+
rc=$?
|
|
158
|
+
if [ "$rc" -eq 2 ]; then
|
|
159
|
+
echo "PASS: BASE_URL unset (exit 2)"
|
|
160
|
+
else
|
|
161
|
+
echo "FAIL: BASE_URL unset — expected exit 2, got $rc"
|
|
162
|
+
cat /tmp/routing-smoke-t7.$$
|
|
163
|
+
FAILURES=$((FAILURES + 1))
|
|
164
|
+
fi
|
|
165
|
+
rm -f /tmp/routing-smoke-t7.$$
|
|
166
|
+
|
|
167
|
+
# 8. Root sibling (PATH_PREFIX='', deliberately empty not unset) — the
|
|
168
|
+
# CDN-behaviour-registered check has nothing to compare against and must
|
|
169
|
+
# say so rather than silently passing; bare-path check still runs
|
|
170
|
+
# (denominator stays > 0), so exit is 0 when that succeeds.
|
|
171
|
+
_write_stub
|
|
172
|
+
cat > "$STUB_DIR/curl" <<'STUB'
|
|
173
|
+
#!/usr/bin/env sh
|
|
174
|
+
outfile=""
|
|
175
|
+
prev=""
|
|
176
|
+
for a in "$@"; do
|
|
177
|
+
case "$prev" in
|
|
178
|
+
-o) outfile="$a" ;;
|
|
179
|
+
esac
|
|
180
|
+
prev="$a"
|
|
181
|
+
done
|
|
182
|
+
url="$prev"
|
|
183
|
+
case "$url" in
|
|
184
|
+
https://dev.example.com) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
|
|
185
|
+
esac
|
|
186
|
+
echo "unhandled URL in stub: $url" >&2
|
|
187
|
+
exit 99
|
|
188
|
+
STUB
|
|
189
|
+
chmod +x "$STUB_DIR/curl"
|
|
190
|
+
out=$(ROUTING_CURL="$STUB_DIR/curl" ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="" sh "$TARGET" 2>&1)
|
|
191
|
+
rc=$?
|
|
192
|
+
if [ "$rc" -eq 0 ] && echo "$out" | grep -q "UNPROVEN"; then
|
|
193
|
+
echo "PASS: root sibling (exit 0, unproven CDN check reported)"
|
|
194
|
+
else
|
|
195
|
+
echo "FAIL: root sibling — expected exit 0 with UNPROVEN noted, got exit $rc"
|
|
196
|
+
echo "$out"
|
|
197
|
+
FAILURES=$((FAILURES + 1))
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
echo
|
|
201
|
+
if [ "$FAILURES" -eq 0 ]; then
|
|
202
|
+
echo "routing-smoke-test.test.sh: all checks passed."
|
|
203
|
+
exit 0
|
|
204
|
+
else
|
|
205
|
+
echo "routing-smoke-test.test.sh: $FAILURES check(s) failed."
|
|
206
|
+
exit 1
|
|
207
|
+
fi
|