110 lines
5.9 KiB
Bash
Executable File
110 lines
5.9 KiB
Bash
Executable File
#!/bin/sh
|
|
# Notification lane client. One script, three phases, because the handoff lane needs the accept and
|
|
# the verify to be the same client talking about the same request id.
|
|
#
|
|
# ingest accept a request while the platform is INGEST_ONLY, and record its id
|
|
# serving accept and expect delivery in the same run
|
|
# handoff-verify re-check a request accepted in an earlier phase, after a SERVING restart
|
|
#
|
|
# The evidence a handoff needs is that the id from phase one is delivered exactly once after the
|
|
# restart, on the route frozen at accept — not that some message arrived.
|
|
#
|
|
# Three things this client does that it did not have to before NTF-INT-008, and each is a fact about
|
|
# the platform rather than about the test:
|
|
#
|
|
# 1. it authenticates. Submission and template publication are ordinary non-public paths, so they
|
|
# sit behind the same JWT the rest of the API does. The token is obtained by client credentials
|
|
# against the same issuer URL the application validates against — a token from a different URL
|
|
# is not the same token.
|
|
# 2. it publishes a template first. A submission pins a template id and version and the platform
|
|
# refuses one it cannot resolve, so "send a notification" is two calls, not one.
|
|
# 3. it addresses a recipient by value. The platform stores contact points encrypted and references
|
|
# them by id; the accept endpoint registers or reuses one, so the address never reaches a plan.
|
|
set -eu
|
|
|
|
STATE_FILE="/opt/notification-smoke-state/request-id"
|
|
SECRET_FILE="/run/secrets/keycloak-graphql-smoke-client-secret"
|
|
BASE_PATH="${APP_BASE_PATH:-/api}"
|
|
TEMPLATE_ID="smoke"
|
|
RECIPIENT="smoke@example.test"
|
|
|
|
fail() { echo "notification-smoke: $1" >&2; exit 1; }
|
|
|
|
token() {
|
|
[ -r "${SECRET_FILE}" ] || fail "the client secret was not mounted"
|
|
RESPONSE="$(curl -sf -X POST "${KEYCLOAK_ISSUER}/protocol/openid-connect/token" \
|
|
-d grant_type=client_credentials \
|
|
-d "client_id=${KEYCLOAK_CLIENT_ID}" \
|
|
--data-urlencode "client_secret=$(cat "${SECRET_FILE}")")" \
|
|
|| fail "client-credentials token request failed"
|
|
echo "${RESPONSE}" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p'
|
|
}
|
|
|
|
case "${NOTIFICATION_SMOKE_PHASE}" in
|
|
ingest|serving)
|
|
ACCESS_TOKEN="$(token)"
|
|
[ -n "${ACCESS_TOKEN}" ] || fail "the token response carried no access_token"
|
|
|
|
# 1. the template. Republishing the same version is the same immutable content, so a lane that
|
|
# reruns against a surviving volume is not a different lane; a 409 here means the platform
|
|
# holds a version with this id and different content, which is a real failure.
|
|
PUBLISH_STATUS="$(curl -s -o /tmp/publish.json -w '%{http_code}' \
|
|
-X POST "${APP_BASE_URL}${BASE_PATH}/notification-templates" \
|
|
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
-H 'Content-Type: application/json' \
|
|
-d "{\"templateId\":\"${TEMPLATE_ID}\",\"version\":1,\"channel\":\"EMAIL\",\"locale\":\"en\",
|
|
\"slots\":{\"SUBJECT\":\"lane smoke\",\"TEXT_BODY\":\"lane smoke body\"}}")"
|
|
case "${PUBLISH_STATUS}" in
|
|
201|409) : ;;
|
|
401|403) fail "template publication was refused (${PUBLISH_STATUS}); the token lacks notification-template:publish" ;;
|
|
*) fail "template publication answered ${PUBLISH_STATUS}: $(cat /tmp/publish.json)" ;;
|
|
esac
|
|
|
|
# 2. the submission.
|
|
ACCEPT_STATUS="$(curl -s -o /tmp/accept.json -w '%{http_code}' \
|
|
-X POST "${APP_BASE_URL}${BASE_PATH}/notifications" \
|
|
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
-H 'Content-Type: application/json' \
|
|
-d "{\"recipientRef\":\"lane-smoke-recipient\",\"channel\":\"EMAIL\",
|
|
\"address\":\"${RECIPIENT}\",\"template\":\"${TEMPLATE_ID}\",\"templateVersion\":1,
|
|
\"locale\":\"en\",\"variables\":{},\"category\":\"transactional\"}")"
|
|
[ "${ACCEPT_STATUS}" = "202" ] \
|
|
|| fail "accept answered ${ACCEPT_STATUS}: $(cat /tmp/accept.json)"
|
|
|
|
REQUEST_ID="$(sed -n 's/.*"requestId":"\([^"]*\)".*/\1/p' /tmp/accept.json)"
|
|
[ -n "${REQUEST_ID}" ] || fail "the accept response carried no requestId"
|
|
# Not an `&&` chain: a failed mkdir in one is exempt from `set -e`, so the id went unrecorded
|
|
# and the ingest phase still reported success — leaving the handoff phase to fail later about a
|
|
# state file "the phases did not share", which describes the symptom and not the cause.
|
|
mkdir -p "$(dirname "${STATE_FILE}")" || fail "the state directory is not writable"
|
|
echo "${REQUEST_ID}" > "${STATE_FILE}" || fail "the request id could not be recorded"
|
|
echo "notification-smoke: accepted ${REQUEST_ID} in ${NOTIFICATION_SMOKE_PHASE}"
|
|
;;
|
|
handoff-verify)
|
|
[ -r "${STATE_FILE}" ] || fail "no request id from the ingest phase; the phases did not share state"
|
|
REQUEST_ID="$(cat "${STATE_FILE}")"
|
|
;;
|
|
*)
|
|
fail "unknown phase ${NOTIFICATION_SMOKE_PHASE}"
|
|
;;
|
|
esac
|
|
|
|
if [ "${NOTIFICATION_SMOKE_PHASE}" = "ingest" ]; then
|
|
# INGEST_ONLY accepts durably and sends nothing. A message here means a worker ran that should not
|
|
# have.
|
|
COUNT="$(curl -sf "${MAILPIT_BASE_URL}/api/v1/messages?limit=200" 2>/dev/null \
|
|
| grep -o '"ID"' | wc -l | tr -d ' ')" || COUNT=0
|
|
[ "${COUNT}" = "0" ] || fail "INGEST_ONLY delivered ${COUNT} message(s); no worker should have run"
|
|
echo "notification-smoke: ingest stored the request and sent nothing"
|
|
exit 0
|
|
fi
|
|
|
|
# serving and handoff-verify: exactly one delivery, and still exactly one after another poll window.
|
|
sleep 10
|
|
first="$(curl -sf "${MAILPIT_BASE_URL}/api/v1/search?query=smoke%40example.test" | grep -o '"ID"' | wc -l | tr -d ' ')"
|
|
[ "${first}" = "1" ] || fail "expected exactly one delivery, saw ${first}"
|
|
sleep 15
|
|
second="$(curl -sf "${MAILPIT_BASE_URL}/api/v1/search?query=smoke%40example.test" | grep -o '"ID"' | wc -l | tr -d ' ')"
|
|
[ "${second}" = "1" ] || fail "a second dispatch window produced ${second} deliveries; at-most-once is broken"
|
|
echo "notification-smoke: ${REQUEST_ID} delivered exactly once and stayed that way"
|