| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
|
| 18 |
|
| 19 |
|
| 20 |
|
| 21 |
|
| 22 |
|
| 23 |
|
| 24 |
|
| 25 |
|
| 26 |
|
| 27 |
|
| 28 |
|
| 29 |
|
| 30 |
|
| 31 |
|
| 32 |
|
| 33 |
|
| 34 |
|
| 35 |
set -uo pipefail |
| 36 |
|
| 37 |
ENV_FILE="${MT_HARNESS_ENV:-$HOME/.config/mnw/mt-harness.env}" |
| 38 |
COMMUNITY="${MT_HARNESS_COMMUNITY:-rust}" |
| 39 |
CATEGORY="${MT_HARNESS_CATEGORY:-general}" |
| 40 |
|
| 41 |
if [ ! -r "$ENV_FILE" ]; then |
| 42 |
echo "FAIL: no credentials at $ENV_FILE (see multithreaded/deploy/README.md)" >&2 |
| 43 |
exit 1 |
| 44 |
fi |
| 45 |
|
| 46 |
. "$ENV_FILE" |
| 47 |
|
| 48 |
: "${MT_HARNESS_URL:?MT_HARNESS_URL not set in $ENV_FILE}" |
| 49 |
: "${MT_HARNESS_PASSWORD:?MT_HARNESS_PASSWORD not set in $ENV_FILE}" |
| 50 |
MT="${MT_HARNESS_URL%/}" |
| 51 |
|
| 52 |
FAILURES=0 |
| 53 |
fail() { echo "FAIL: $*" >&2; FAILURES=$((FAILURES + 1)); } |
| 54 |
ok() { echo " ok: $*"; } |
| 55 |
step() { echo; echo "== $*"; } |
| 56 |
|
| 57 |
WORK="$(mktemp -d)" |
| 58 |
trap 'rm -rf "$WORK"' EXIT |
| 59 |
|
| 60 |
|
| 61 |
|
| 62 |
|
| 63 |
|
| 64 |
|
| 65 |
|
| 66 |
|
| 67 |
hidden_field() { |
| 68 |
grep -o "name=\"$2\"[^>]*value=\"[^\"]*\"" "$1" | head -1 | |
| 69 |
sed -E 's/.*value="([^"]*)".*/\1/' |
| 70 |
} |
| 71 |
|
| 72 |
|
| 73 |
csrf_token() { |
| 74 |
curl -sS -b "$1" -c "$1" "$MT$2" | |
| 75 |
grep -o 'name="csrf-token" content="[0-9a-f]*"' | head -1 | |
| 76 |
sed -E 's/.*content="([0-9a-f]*)".*/\1/' |
| 77 |
} |
| 78 |
|
| 79 |
|
| 80 |
|
| 81 |
|
| 82 |
mt_post() { |
| 83 |
local jar="$1" token="$2" path="$3" |
| 84 |
shift 3 |
| 85 |
curl -sS -b "$jar" -c "$jar" -o /dev/null -w '%{http_code} %{redirect_url}' \ |
| 86 |
-H "X-CSRF-Token: $token" -X POST "$@" "$MT$path" |
| 87 |
} |
| 88 |
|
| 89 |
|
| 90 |
|
| 91 |
|
| 92 |
|
| 93 |
|
| 94 |
|
| 95 |
login() { |
| 96 |
local jar="$1" user="$2" |
| 97 |
local page="$WORK/authorize-$user.html" |
| 98 |
|
| 99 |
local authorize_url |
| 100 |
authorize_url=$(curl -sS -b "$jar" -c "$jar" -L -o "$page" -w '%{url_effective}' \ |
| 101 |
"$MT/auth/login") |
| 102 |
case "$authorize_url" in |
| 103 |
*"/oauth/authorize"*) ;; |
| 104 |
*) fail "login($user): /auth/login did not reach an authorize page (landed on $authorize_url)" |
| 105 |
return 1 ;; |
| 106 |
esac |
| 107 |
|
| 108 |
|
| 109 |
|
| 110 |
|
| 111 |
local mnw_base="${authorize_url%%/oauth/authorize*}" |
| 112 |
local csrf client_id redirect_uri state challenge method scope |
| 113 |
csrf=$(hidden_field "$page" _csrf) |
| 114 |
client_id=$(hidden_field "$page" client_id) |
| 115 |
redirect_uri=$(hidden_field "$page" redirect_uri) |
| 116 |
state=$(hidden_field "$page" state) |
| 117 |
challenge=$(hidden_field "$page" code_challenge) |
| 118 |
method=$(hidden_field "$page" code_challenge_method) |
| 119 |
scope=$(hidden_field "$page" scope) |
| 120 |
|
| 121 |
if [ -z "$client_id" ] || [ -z "$challenge" ]; then |
| 122 |
fail "login($user): authorize page carried no client_id/code_challenge — is the harness client seeded on $mnw_base?" |
| 123 |
return 1 |
| 124 |
fi |
| 125 |
|
| 126 |
local callback |
| 127 |
callback=$(curl -sS -b "$jar" -c "$jar" -o "$WORK/consent-$user.html" -w '%{redirect_url}' \ |
| 128 |
-X POST \ |
| 129 |
--data-urlencode "login=$user" \ |
| 130 |
--data-urlencode "password=$MT_HARNESS_PASSWORD" \ |
| 131 |
--data-urlencode "_csrf=$csrf" \ |
| 132 |
--data-urlencode "client_id=$client_id" \ |
| 133 |
--data-urlencode "redirect_uri=$redirect_uri" \ |
| 134 |
--data-urlencode "state=$state" \ |
| 135 |
--data-urlencode "code_challenge=$challenge" \ |
| 136 |
--data-urlencode "code_challenge_method=$method" \ |
| 137 |
--data-urlencode "scope=$scope" \ |
| 138 |
"$mnw_base/oauth/authorize") |
| 139 |
|
| 140 |
case "$callback" in |
| 141 |
*"/auth/callback?"*) ;; |
| 142 |
*) fail "login($user): authorize did not answer a callback redirect (got '${callback:-no redirect}')" |
| 143 |
return 1 ;; |
| 144 |
esac |
| 145 |
|
| 146 |
curl -sS -b "$jar" -c "$jar" -L -o /dev/null "$callback" |
| 147 |
|
| 148 |
|
| 149 |
|
| 150 |
if curl -sS -b "$jar" -c "$jar" "$MT/account" | grep -q "$user"; then |
| 151 |
ok "logged in as $user" |
| 152 |
return 0 |
| 153 |
fi |
| 154 |
fail "login($user): no session after the callback" |
| 155 |
return 1 |
| 156 |
} |
| 157 |
|
| 158 |
echo "harness walk against $MT (community: $COMMUNITY/$CATEGORY)" |
| 159 |
|
| 160 |
FAN_JAR="$WORK/fan.jar" |
| 161 |
OWNER_JAR="$WORK/owner.jar" |
| 162 |
|
| 163 |
step "reachability" |
| 164 |
health=$(curl -sS -o /dev/null -w '%{http_code}' "$MT/api/health" || echo 000) |
| 165 |
if [ "$health" = "200" ]; then |
| 166 |
ok "/api/health 200" |
| 167 |
else |
| 168 |
fail "/api/health returned $health — is the tailnet proxy up and mt running?" |
| 169 |
echo; echo "harness walk: $FAILURES failure(s)"; exit 1 |
| 170 |
fi |
| 171 |
|
| 172 |
step "login" |
| 173 |
login "$FAN_JAR" harness_fan || { echo; echo "harness walk: $FAILURES failure(s)"; exit 1; } |
| 174 |
login "$OWNER_JAR" harness_owner || { echo; echo "harness walk: $FAILURES failure(s)"; exit 1; } |
| 175 |
|
| 176 |
|
| 177 |
step "thread create (harness_fan)" |
| 178 |
STAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)" |
| 179 |
TITLE="Harness walk $STAMP" |
| 180 |
token=$(csrf_token "$FAN_JAR" "/p/$COMMUNITY/$CATEGORY/new") |
| 181 |
read -r code location < <(mt_post "$FAN_JAR" "$token" "/p/$COMMUNITY/$CATEGORY/new" \ |
| 182 |
--data-urlencode "title=$TITLE" \ |
| 183 |
--data-urlencode "body=Written by deploy/harness-walk.sh at $STAMP. Safe to delete.") |
| 184 |
|
| 185 |
THREAD_ID=$(echo "$location" | sed -nE "s#.*/p/$COMMUNITY/$CATEGORY/([0-9a-f-]{36}).*#\1#p") |
| 186 |
if [ -n "$THREAD_ID" ]; then |
| 187 |
ok "thread $THREAD_ID created (HTTP $code)" |
| 188 |
else |
| 189 |
fail "thread create answered $code, redirect '$location'" |
| 190 |
echo; echo "harness walk: $FAILURES failure(s)"; exit 1 |
| 191 |
fi |
| 192 |
|
| 193 |
|
| 194 |
step "reply (harness_fan)" |
| 195 |
THREAD_PATH="/p/$COMMUNITY/$CATEGORY/$THREAD_ID" |
| 196 |
token=$(csrf_token "$FAN_JAR" "$THREAD_PATH") |
| 197 |
read -r code location < <(mt_post "$FAN_JAR" "$token" "$THREAD_PATH/reply" \ |
| 198 |
--data-urlencode "body=Reply from the harness walk at $STAMP.") |
| 199 |
case "$code" in |
| 200 |
30*) ok "reply posted (HTTP $code)" ;; |
| 201 |
*) fail "reply answered $code" ;; |
| 202 |
esac |
| 203 |
|
| 204 |
|
| 205 |
|
| 206 |
curl -sS -b "$FAN_JAR" -c "$FAN_JAR" -o "$WORK/thread.html" "$MT$THREAD_PATH" |
| 207 |
POST_ID=$(grep -o 'data-post-id="[0-9a-f-]\{36\}"' "$WORK/thread.html" | |
| 208 |
sed -E 's/.*"([0-9a-f-]*)".*/\1/' | sed -n 2p) |
| 209 |
if [ -n "$POST_ID" ]; then |
| 210 |
ok "reply rendered as post $POST_ID" |
| 211 |
else |
| 212 |
fail "the thread page shows no second post — the reply did not land" |
| 213 |
echo; echo "harness walk: $FAILURES failure(s)"; exit 1 |
| 214 |
fi |
| 215 |
|
| 216 |
|
| 217 |
step "flag (harness_owner flags harness_fan's reply)" |
| 218 |
token=$(csrf_token "$OWNER_JAR" "$THREAD_PATH") |
| 219 |
read -r code location < <(mt_post "$OWNER_JAR" "$token" "$THREAD_PATH/posts/$POST_ID/flag" \ |
| 220 |
--data-urlencode "reason=off_topic" \ |
| 221 |
--data-urlencode "detail=Filed by deploy/harness-walk.sh at $STAMP.") |
| 222 |
case "$code" in |
| 223 |
30*) ok "flag accepted (HTTP $code)" ;; |
| 224 |
*) fail "flag answered $code" ;; |
| 225 |
esac |
| 226 |
|
| 227 |
|
| 228 |
|
| 229 |
|
| 230 |
|
| 231 |
|
| 232 |
step "moderation (harness_owner removes the flagged post)" |
| 233 |
curl -sS -b "$OWNER_JAR" -c "$OWNER_JAR" -o "$WORK/moderation.html" "$MT/p/$COMMUNITY/moderation" |
| 234 |
FLAG_ID=$(grep -o "moderation/flags/[0-9a-f-]\{36\}/remove" "$WORK/moderation.html" | head -1 | |
| 235 |
sed -E 's#.*/flags/([0-9a-f-]*)/remove#\1#') |
| 236 |
if [ -n "$FLAG_ID" ]; then |
| 237 |
ok "flag $FLAG_ID is in the moderation queue" |
| 238 |
else |
| 239 |
fail "the moderation queue shows no flag — either the flag did not land or the Owner grant did not apply" |
| 240 |
echo; echo "harness walk: $FAILURES failure(s)"; exit 1 |
| 241 |
fi |
| 242 |
|
| 243 |
token=$(csrf_token "$OWNER_JAR" "/p/$COMMUNITY/moderation") |
| 244 |
read -r code location < <(mt_post "$OWNER_JAR" "$token" "/p/$COMMUNITY/moderation/flags/$FLAG_ID/remove") |
| 245 |
case "$code" in |
| 246 |
30*) ok "post removed through the flag (HTTP $code)" ;; |
| 247 |
*) fail "flag removal answered $code" ;; |
| 248 |
esac |
| 249 |
|
| 250 |
|
| 251 |
|
| 252 |
|
| 253 |
|
| 254 |
step "read-back" |
| 255 |
curl -sS -b "$OWNER_JAR" -c "$OWNER_JAR" -o "$WORK/thread-after.html" "$MT$THREAD_PATH" |
| 256 |
grep -q "Harness walk $STAMP" "$WORK/thread-after.html" && |
| 257 |
ok "thread title renders" || fail "the thread page does not show the title we wrote" |
| 258 |
grep -q "post-removed" "$WORK/thread-after.html" && |
| 259 |
ok "the removed post renders as removed" || fail "the flagged post does not render as removed" |
| 260 |
|
| 261 |
curl -sS -b "$OWNER_JAR" -c "$OWNER_JAR" -o "$WORK/modlog.html" "$MT/p/$COMMUNITY/moderation/log" |
| 262 |
grep -qi "harness_owner" "$WORK/modlog.html" && |
| 263 |
ok "the moderation log records the actor" || fail "the moderation log does not name harness_owner" |
| 264 |
|
| 265 |
echo |
| 266 |
if [ "$FAILURES" -eq 0 ]; then |
| 267 |
echo "harness walk: all steps passed — a browser lens can log in and write here" |
| 268 |
exit 0 |
| 269 |
fi |
| 270 |
echo "harness walk: $FAILURES failure(s)" |
| 271 |
exit 1 |
| 272 |
|