Files
clean-architecture-backend-…/infra/graphql/smoke/graphql-smoke.sh
T

76 lines
3.4 KiB
Bash
Executable File

#!/bin/sh
# The GraphQL transport, exercised as a request rather than as a bean inventory.
#
# auth-smoke proves the realm issues a usable token and that the application answers its public
# health path. It never sends an authenticated request, and never touches /graphql at all — so the
# JWT decoder, the security filter chain and the GraphQL execution path were each covered by their
# own tests and by nothing that put them in one line together.
#
# Three requests, in this order, because each is only meaningful given the one before:
#
# 1. an unauthenticated query, which must be refused — otherwise steps 2 and 3 prove nothing about
# authentication, they just prove the endpoint answers;
# 2. a malformed token, which must be refused without a server error — a 500 here means the
# decoder threw where it should have rejected;
# 3. the real token, which must return the schema's liveness field — the request path, the policy
# instrumentation and the resolver, in one call.
set -eu
SECRET_FILE="/run/secrets/keycloak-graphql-smoke-client-secret"
GRAPHQL_PATH="${GRAPHQL_PATH:-/graphql}"
QUERY='{"query":"{ _health }"}'
fail() { echo "graphql-smoke: $1" >&2; exit 1; }
post() {
# $1 = Authorization header value, or empty for none. Prints the status code; body to /tmp/gql.json.
if [ -n "$1" ]; then
curl -s -o /tmp/gql.json -w '%{http_code}' -X POST "${APP_BASE_URL}${GRAPHQL_PATH}" \
-H "Authorization: $1" -H 'Content-Type: application/json' -d "${QUERY}"
else
curl -s -o /tmp/gql.json -w '%{http_code}' -X POST "${APP_BASE_URL}${GRAPHQL_PATH}" \
-H 'Content-Type: application/json' -d "${QUERY}"
fi
}
# 1. no credential at all
ANON_STATUS="$(post '')"
case "${ANON_STATUS}" in
401|403) : ;;
200) fail "an unauthenticated GraphQL query was answered (${ANON_STATUS}); /graphql is not guarded" ;;
*) fail "an unauthenticated GraphQL query answered ${ANON_STATUS}: $(cat /tmp/gql.json)" ;;
esac
# 2. a credential that is not a token
BAD_STATUS="$(post 'Bearer not-a-real-token')"
[ "${BAD_STATUS}" != "500" ] \
|| fail "a malformed token produced a server error rather than a refusal"
case "${BAD_STATUS}" in
401|403) : ;;
*) fail "a malformed token answered ${BAD_STATUS}, which is neither a refusal nor a server error" ;;
esac
# 3. the real thing
[ -r "${SECRET_FILE}" ] || fail "the client secret was not mounted"
TOKEN_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"
ACCESS_TOKEN="$(echo "${TOKEN_RESPONSE}" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')"
[ -n "${ACCESS_TOKEN}" ] || fail "the token response carried no access_token"
OK_STATUS="$(post "Bearer ${ACCESS_TOKEN}")"
[ "${OK_STATUS}" = "200" ] \
|| fail "an authenticated GraphQL query answered ${OK_STATUS}: $(cat /tmp/gql.json)"
# The body, not just the status. A 200 carrying a GraphQL `errors` array is how a refused or failed
# execution looks over HTTP, so a status-only check would pass on an unresolved field.
grep -q '"_health"' /tmp/gql.json \
|| fail "the response carried no _health field: $(cat /tmp/gql.json)"
if grep -q '"errors"' /tmp/gql.json; then
fail "the query returned GraphQL errors: $(cat /tmp/gql.json)"
fi
echo "graphql-smoke: /graphql refused anonymous and malformed credentials and answered the authenticated query"