This is most useful for automated tests for upcoming configuration features. Can also be used for debugging.
422 lines
12 KiB
Bash
Executable File
422 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Run unit tests for Input/Ouput of PostgREST seen as a black box
|
||
# with test output in Test Anything Protocol format.
|
||
#
|
||
# These tests expect that `postgrest` is on the PATH, as well as `curl`
|
||
#
|
||
# References:
|
||
# [1] Test Anything Protocol
|
||
# https://testanything.org/
|
||
#
|
||
# [2] TAP Specification
|
||
# https://testanything.org/tap-specification.html
|
||
#
|
||
# [3] List of TCP and UDP port numbers
|
||
# https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers
|
||
#
|
||
set -eu
|
||
|
||
export POSTGREST_TEST_CONNECTION=${POSTGREST_TEST_CONNECTION:-"postgres:///postgrest_test"}
|
||
|
||
cd "$(dirname "$0")"
|
||
cd io-tests
|
||
|
||
cleanup() {
|
||
# clean up trap to avoid bash segmentation fault
|
||
trap - sigint sigterm exit
|
||
|
||
# kill without output
|
||
ps=$(pgrep -g0 | sed -e "1,/$$/d")
|
||
kill $ps 2> /dev/null
|
||
wait $ps 2> /dev/null
|
||
}
|
||
|
||
trap cleanup sigint sigterm exit
|
||
|
||
# Port for Test PostgREST Server (must match config)
|
||
pgrPort=49421 # in range 49152–65535: for private or temporary use
|
||
|
||
# TAP utilities
|
||
currentTest=1
|
||
failedTests=0
|
||
bailOut(){ echo "Bail out! $1"; exit 1; }
|
||
result(){ echo "$1 $currentTest $2"; currentTest=$(( $currentTest + 1 )); }
|
||
todo(){ result 'ok' "# TODO: $*"; }
|
||
skip(){ result 'ok' "# SKIP: $*"; }
|
||
ok(){ result 'ok' "- $1"; }
|
||
ko(){ result 'not ok' "- $1"; failedTests=$(( $failedTests + 1 )); }
|
||
comment(){ echo "# $1"; }
|
||
|
||
########################
|
||
# SYNCHRONOUS IO TESTS #
|
||
########################
|
||
|
||
dumpedConfigMatchesExpectation(){
|
||
# This test compares the dumped config vs. the corresponding file in ./configs/expected.
|
||
# To be used to test default values, config aliases and environment variables.
|
||
dump="$(mktemp)"
|
||
tap(){
|
||
if test $1 -eq 0; then
|
||
ok "dump of config file $2 does match expectation"
|
||
else
|
||
ko "dump of config file $2 does not match expectation"
|
||
fi
|
||
rm -f "$dump"
|
||
}
|
||
trap 'tap $? $1; trap - RETURN; return 0' ERR RETURN
|
||
postgrest --dump-config "$1" > "$dump"
|
||
diff "$dump" "$2"
|
||
}
|
||
|
||
dumpedConfigIsValid(){
|
||
# This test compares the dumped config vs. the dumped-reread-redumped config.
|
||
# Re-reading the dumped config tests the validity of the config format.
|
||
# Re-dumping this config should yield no difference to the first dump, showing
|
||
# that the semantics have not changed by dumping.
|
||
# Note: only dump vs redump must be equal, the original config file can be different,
|
||
# because of default values, whitespace, and quoting
|
||
dump="$(mktemp)"
|
||
redump="$(mktemp)"
|
||
tap(){
|
||
if test $1 -eq 0; then
|
||
ok "dump of config file $2 is valid"
|
||
else
|
||
ko "dump of config file $2 is invalid"
|
||
fi
|
||
rm -f "$dump" "$redump"
|
||
}
|
||
trap 'tap $? $1; trap - RETURN; return 0' ERR RETURN
|
||
postgrest --dump-config "$1" > "$dump"
|
||
postgrest --dump-config "$dump" > "$redump"
|
||
diff "$dump" "$redump"
|
||
}
|
||
|
||
####################
|
||
# BACKGROUND TESTS #
|
||
####################
|
||
|
||
# Utilities to start/stop test PostgREST server running in the background
|
||
pgrStart(){
|
||
# stderr is not piped to /dev/null to catch errors on startup.
|
||
# to keep $! reference the correct pid, stderr is piped to a subshell and
|
||
# then filtered for FatalError. Those are part of the tests and expected.
|
||
postgrest $1 >/dev/null 2> >(grep -v 'FatalError' 1>&2) & pgrPID="$!";
|
||
}
|
||
pgrStartRead(){ postgrest $1 <$2 >/dev/null & pgrPID="$!"; }
|
||
pgrStartStdin(){ postgrest $1 >/dev/null <<< "$2" & pgrPID="$!"; }
|
||
pgrStarted(){ kill -0 "$pgrPID" 2>/dev/null; }
|
||
pgrStop(){ kill "$pgrPID" 2>/dev/null; pgrPID=""; sleep 0.1; }
|
||
|
||
# Utilities to send HTTP requests to the PostgREST server
|
||
rootStatus(){
|
||
curl -s -o /dev/null -w '%{http_code}' "http://localhost:$pgrPort/"
|
||
}
|
||
|
||
authorsStatus(){
|
||
curl -s -o /dev/null -w '%{http_code}' \
|
||
-H "Authorization: Bearer $1" \
|
||
"http://localhost:$pgrPort/authors_only"
|
||
}
|
||
|
||
v1SchemaParentsStatus(){
|
||
curl -s -o /dev/null -w '%{http_code}' \
|
||
-H "Accept-Profile: v1" \
|
||
"http://localhost:$pgrPort/parents"
|
||
}
|
||
|
||
# Unit Test Templates
|
||
readSecretFromFile(){
|
||
case "$1" in
|
||
*.b64)
|
||
pgrConfig="base64-secret-from-file.config";;
|
||
*)
|
||
pgrConfig="secret-from-file.config";;
|
||
esac
|
||
pgrStartRead "./configs/$pgrConfig" "./secrets/$1"
|
||
while pgrStarted && test "$( rootStatus )" -ne 200
|
||
do
|
||
# wait for the server to start
|
||
sleep 0.1
|
||
done
|
||
if pgrStarted
|
||
then
|
||
authorsJwt="./secrets/${1%.*}.jwt"
|
||
httpStatus="$( authorsStatus $(cat "$authorsJwt") )"
|
||
if test "$httpStatus" -eq 200
|
||
then
|
||
ok "authentication with $2 secret read from a file"
|
||
else
|
||
ko "authentication with $2 secret read from a file: $httpStatus"
|
||
fi
|
||
else
|
||
ko "failed to read $2 secret from a file"
|
||
fi
|
||
pgrStop
|
||
}
|
||
|
||
readDbUriFromStdin(){
|
||
pgrConfig="dburi-from-file.config"
|
||
pgrStartStdin "./configs/$pgrConfig" "$1"
|
||
while pgrStarted && test "$( rootStatus )" -ne 200
|
||
do
|
||
# wait for the server to start
|
||
sleep 0.1
|
||
done
|
||
if pgrStarted
|
||
then
|
||
ok "connection with $2 dburi read from stdin / a file"
|
||
else
|
||
ko "connection with $2 dburi read from stdin / a file"
|
||
fi
|
||
pgrStop
|
||
}
|
||
|
||
reqWithRoleClaimKey(){
|
||
export ROLE_CLAIM_KEY=$1
|
||
pgrStart "./configs/role-claim-key.config"
|
||
while pgrStarted && test "$( rootStatus )" -ne 200
|
||
do
|
||
# wait for the server to start
|
||
sleep 0.1
|
||
done
|
||
authorsJwt=$(psql -qtAX "$POSTGREST_TEST_CONNECTION" -c "select jwt.sign('$2', 'reallyreallyreallyreallyverysafe');")
|
||
httpStatus="$( authorsStatus "$authorsJwt" )"
|
||
if test "$httpStatus" -eq $3
|
||
then
|
||
ok "request with \"$1\" role-claim-key for $2 jwt: $httpStatus"
|
||
else
|
||
ko "request with \"$1\" role-claim-key for $2 jwt: $httpStatus"
|
||
fi
|
||
pgrStop
|
||
}
|
||
|
||
invalidRoleClaimKey(){
|
||
export ROLE_CLAIM_KEY=$1
|
||
pgrStart "./configs/role-claim-key.config"
|
||
while pgrStarted && test "$( rootStatus )" -ne 200
|
||
do
|
||
# wait for the server to start
|
||
sleep 0.1
|
||
done
|
||
if pgrStarted
|
||
then
|
||
ko "invalid jspath \"$1\": accepted"
|
||
pgrStop
|
||
else
|
||
ok "invalid jspath \"$1\": rejected"
|
||
fi
|
||
}
|
||
|
||
# ensure iat claim is successful in the presence of pgrst time cache, see https://github.com/PostgREST/postgrest/issues/1139
|
||
ensureIatClaimWorks(){
|
||
pgrStart "./configs/simple.config"
|
||
while pgrStarted && test "$( rootStatus )" -ne 200
|
||
do
|
||
# wait for the server to start
|
||
sleep 0.1
|
||
done
|
||
for i in {1..10}; do \
|
||
iatJwt=$(psql -qtAX "$POSTGREST_TEST_CONNECTION" -c "select jwt.sign(row_to_json(r), 'reallyreallyreallyreallyverysafe') from ( select 'postgrest_test_author' as role, extract(epoch from now()) as iat) r")
|
||
httpStatus="$( authorsStatus $iatJwt )"
|
||
if test "$httpStatus" -ne 200
|
||
then
|
||
ko "iat claim rejected: $httpStatus"
|
||
return
|
||
fi
|
||
sleep .5;\
|
||
done
|
||
ok "iat claim accepted"
|
||
pgrStop
|
||
}
|
||
|
||
# ensure app settings don't reset on pool timeout, see https://github.com/PostgREST/postgrest/issues/1141
|
||
# pool timeout set to 1s to shorten runtime
|
||
ensureAppSettings(){
|
||
pgrStart "./configs/app-settings.config"
|
||
while pgrStarted && test "$( rootStatus )" -ne 200
|
||
do
|
||
# wait for the server to start
|
||
sleep 0.1
|
||
done
|
||
sleep 2
|
||
response=$(curl -s "http://localhost:$pgrPort/rpc/get_guc_value?name=app.settings.external_api_secret")
|
||
if test "$response" = "\"0123456789abcdef\""
|
||
then
|
||
ok "GET /rpc/get_guc_value: $response"
|
||
else
|
||
ko "GET /rpc/get_guc_value: $response"
|
||
fi
|
||
pgrStop
|
||
}
|
||
|
||
checkAppSettingsReload(){
|
||
configFile=$(mktemp)
|
||
trap "rm -f $configFile" ERR RETURN
|
||
cat "./configs/sigusr2-settings.config" > "$configFile"
|
||
pgrStart "$configFile"
|
||
while pgrStarted && test "$( rootStatus )" -ne 200
|
||
do
|
||
# wait for the server to start
|
||
sleep 0.1
|
||
done
|
||
# change setting
|
||
replaceConfigValue "app.settings.name_var" "Jane" "$configFile"
|
||
# reload
|
||
kill -s SIGUSR2 $pgrPID
|
||
response=$(curl -s "http://localhost:$pgrPort/rpc/get_guc_value?name=app.settings.name_var")
|
||
if test "$response" = "\"Jane\""
|
||
then
|
||
ok "app.settings.name_var config reloaded with SIGUSR2"
|
||
else
|
||
ko "app.settings.name_var config not reloaded with SIGUSR2. Got: $response"
|
||
fi
|
||
pgrStop
|
||
}
|
||
|
||
checkJwtSecretReload(){
|
||
configFile=$(mktemp)
|
||
trap "rm -f $configFile" ERR RETURN
|
||
cat "./configs/sigusr2-settings.config" > "$configFile"
|
||
pgrStart "$configFile"
|
||
while pgrStarted && test "$( rootStatus )" -ne 200
|
||
do
|
||
# wait for the server to start
|
||
sleep 0.1
|
||
done
|
||
secret="reallyreallyreallyreallyverysafe"
|
||
# change setting
|
||
replaceConfigValue "jwt-secret" "$secret" "$configFile"
|
||
# reload
|
||
kill -s SIGUSR2 $pgrPID
|
||
payload='{"role":"postgrest_test_author"}'
|
||
authorsJwt=$(psql -qtAX "$POSTGREST_TEST_CONNECTION" -c "select jwt.sign('$payload', '$secret');")
|
||
httpStatus="$( authorsStatus "$authorsJwt" )"
|
||
if test "$httpStatus" -eq 200
|
||
then
|
||
ok "jwt-secret config reloaded with SIGUSR2"
|
||
else
|
||
ko "jwt-secret config not reloaded with SIGUSR2. Got: $httpStatus"
|
||
fi
|
||
pgrStop
|
||
}
|
||
|
||
checkDbSchemaReload(){
|
||
configFile=$(mktemp)
|
||
trap "rm -f $configFile" ERR RETURN
|
||
cat "./configs/sigusr2-settings.config" > "$configFile"
|
||
pgrStart "$configFile"
|
||
while pgrStarted && test "$( rootStatus )" -ne 200
|
||
do
|
||
# wait for the server to start
|
||
sleep 0.1
|
||
done
|
||
# add v1 schema to db-schema
|
||
replaceConfigValue "db-schema" "test, v1" "$configFile"
|
||
# reload
|
||
kill -s SIGUSR2 $pgrPID
|
||
kill -s SIGUSR1 $pgrPID
|
||
httpStatus="$(v1SchemaParentsStatus)"
|
||
if test "$httpStatus" -eq 200
|
||
then
|
||
ok "db-schema config reloaded with SIGUSR2"
|
||
else
|
||
ko "db-schema config not reloaded with SIGUSR2. Got: $httpStatus"
|
||
fi
|
||
pgrStop
|
||
}
|
||
|
||
replaceConfigValue(){
|
||
sed -i "s/.*$1.*/$1 = \"$2\"/g" $3
|
||
}
|
||
|
||
getSocketStatus() {
|
||
curl -sL -w "%{http_code}\\n" -o /dev/null --unix-socket /tmp/postgrest.sock http://localhost/
|
||
}
|
||
|
||
socketConnection(){
|
||
pgrStart "./configs/unix-socket.config"
|
||
while pgrStarted && test "$( getSocketStatus )" -ne 200
|
||
do
|
||
# wait for the server to start
|
||
sleep 0.1
|
||
done
|
||
if test $( getSocketStatus ) -eq 200
|
||
then
|
||
ok "Succesfully connected through unix socket"
|
||
else
|
||
ko "Failed to connect through unix socket"
|
||
fi
|
||
pgrStop
|
||
}
|
||
|
||
# PRE: curl must be available
|
||
test -n "$(command -v curl)" || bailOut 'curl is not available'
|
||
|
||
# PRE: postgres must be running
|
||
psql -l "$POSTGREST_TEST_CONNECTION" 1>/dev/null 2>/dev/null || bailOut 'postgres is not running'
|
||
|
||
echo "Running IO tests.."
|
||
|
||
# run dumpConfigIsValid with as many inputs as possible
|
||
for cfg in configs/*.config
|
||
do
|
||
# ROLE_CLAIM_KEY is only used in one of the config files
|
||
# using a complex example here, to make sure the quoting works
|
||
ROLE_CLAIM_KEY='."https://www.example.com/roles"[0].value' \
|
||
dumpedConfigIsValid "$cfg" \
|
||
<<< "Y29ubmVjdGlvbl9zdHJpbmc=" # /dev/stdin is read by some config files, one of them expects Base64
|
||
done
|
||
|
||
# run dumpConfigMatchesExpectation with all expectations
|
||
for exp in configs/expected/*.config
|
||
do
|
||
cfg="$(sed -e 's|expected/||' <(echo $exp))"
|
||
dumpedConfigMatchesExpectation "$cfg" "$exp"
|
||
done
|
||
|
||
socketConnection
|
||
|
||
readSecretFromFile word.noeol 'simple (no EOL)'
|
||
readSecretFromFile word.txt 'simple'
|
||
readSecretFromFile ascii.noeol 'ASCII (no EOL)'
|
||
readSecretFromFile ascii.txt 'ASCII'
|
||
readSecretFromFile utf8.noeol 'UTF-8 (no EOL)'
|
||
readSecretFromFile utf8.txt 'UTF-8'
|
||
readSecretFromFile binary.noeol 'binary'
|
||
readSecretFromFile binary.eol 'binary (+EOL)'
|
||
|
||
readSecretFromFile word.b64 'Base64 (simple)'
|
||
readSecretFromFile ascii.b64 'Base64 (ASCII)'
|
||
readSecretFromFile utf8.b64 'Base64 (UTF-8)'
|
||
readSecretFromFile binary.b64 'Base64 (binary)'
|
||
|
||
eol=$'\x0a'
|
||
|
||
readDbUriFromStdin "$POSTGREST_TEST_CONNECTION" "(no EOL)"
|
||
readDbUriFromStdin "$POSTGREST_TEST_CONNECTION$eol" "(EOL)"
|
||
|
||
reqWithRoleClaimKey '.postgrest.a_role' '{"postgrest":{"a_role":"postgrest_test_author"}}' 200
|
||
reqWithRoleClaimKey '.customObject.manyRoles[1]' '{"customObject":{"manyRoles": ["other", "postgrest_test_author"]}}' 200
|
||
reqWithRoleClaimKey '."https://www.example.com/roles"[0].value' '{"https://www.example.com/roles":[{"value":"postgrest_test_author"}]}' 200
|
||
reqWithRoleClaimKey '.myDomain[3]' '{"myDomain":["other","postgrest_test_author"]}' 401
|
||
reqWithRoleClaimKey '.myRole' '{"role":"postgrest_test_author"}' 401
|
||
|
||
invalidRoleClaimKey 'role.other'
|
||
invalidRoleClaimKey '.role##'
|
||
invalidRoleClaimKey '.my_role;;domain'
|
||
invalidRoleClaimKey '.#$%&$%/'
|
||
invalidRoleClaimKey ''
|
||
invalidRoleClaimKey 1234
|
||
|
||
ensureIatClaimWorks
|
||
ensureAppSettings
|
||
|
||
checkAppSettingsReload
|
||
checkJwtSecretReload
|
||
checkDbSchemaReload
|
||
# TODO: SIGUSR2 tests for other config options
|
||
|
||
trap - sigint sigterm exit
|
||
|
||
exit $failedTests
|