Migrate io-tests from bash to pytest

Authored-by: monacoremo <monacoremo>
This commit is contained in:
Remo Rechkemmer
2020-12-13 16:19:26 +01:00
committed by GitHub
parent 0c25f12825
commit 71fa87937b
8 changed files with 419 additions and 434 deletions
+1
View File
@@ -17,3 +17,4 @@ result*
dist-newstyle
postgrest.hp
postgrest.prof
__pycache__
+5 -2
View File
@@ -87,6 +87,8 @@ jobs:
update: true
packages:
- postgresql-12
- python3
- python3-pip
cache:
yarn: true
timeout: 1000
@@ -107,17 +109,18 @@ jobs:
fi
travis_wait stack --no-terminal setup
travis_wait stack --no-terminal install hpc
pip3 install pytest pyyaml requests requests-unixsocket pyjwt
script: |
travis_wait 50 stack --no-terminal build --fast -j1 --coverage
travis_wait 50 stack --no-terminal build --fast -j1 --coverage --test --no-run-tests
test/with_tmp_db stack --no-terminal test --coverage
test/with_tmp_db stack --no-terminal exec test/io-tests.sh
test/with_tmp_db stack --no-terminal exec -- pytest -v test/io-tests
after_script: |
export _HPC_DIR=$(stack path --local-hpc-root)
export _MIX_DIR=$(stack path --dist-dir)
export _PKG_NAME=$(stack exec -- ghc-pkg field postgrest key --simple-output)
# merge the results from `stack test` and the io tests and exclude Paths_postgrest
stack --no-terminal exec hpc -- sum --union --exclude=Paths_postgrest --output=/tmp/all.tix $_HPC_DIR/combined/all/all.tix test/io-tests/postgrest.tix
stack --no-terminal exec hpc -- sum --union --exclude=Paths_postgrest --output=/tmp/all.tix $_HPC_DIR/combined/all/all.tix postgrest.tix
# fix a bug in stack-hpc-coveralls
mv $_MIX_DIR/hpc/Main.mix $_MIX_DIR/hpc/$_PKG_NAME/Main.mix
mv $_MIX_DIR/hpc/UnixSocket.mix $_MIX_DIR/hpc/$_PKG_NAME/UnixSocket.mix
+5 -1
View File
@@ -1,4 +1,5 @@
{ buildEnv
{ black
, buildEnv
, checkedShellScript
, git
, hlint
@@ -19,6 +20,9 @@ let
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' . "$rootdir" \
| xargs ${stylish-haskell}/bin/stylish-haskell -i
# Format Python files
${black}/bin/black "$rootdir" 2> /dev/null
'';
# Script to check whether any uncommited changes result from postgrest-style
+14 -3
View File
@@ -15,6 +15,7 @@
, postgrestStatic
, postgrestProfiled
, procps
, python3
, runtimeShell
}:
let
@@ -74,19 +75,29 @@ let
checkedShellScript "postgrest-test-spec-all"
(lib.concatStringsSep "\n" testRunners);
ioTestPython =
python3.withPackages (ps: [
ps.pytest
ps.requests
ps.requests-unixsocket
ps.pyjwt
ps.pyyaml
]);
testIO =
name: postgresql:
checkedShellScript
name
''
env="$(cat ${postgrest.env})"
export PATH="$env/bin:${curl}/bin:${procps}/bin:${diffutils}/bin:$PATH"
export PATH="$env/bin:$PATH"
rootdir="$(${git}/bin/git rev-parse --show-toplevel)"
cd "$rootdir"
${cabal-install}/bin/cabal v2-build ${devCabalOptions}
${cabal-install}/bin/cabal v2-exec ${withTmpDb postgresql} "$rootdir"/test/io-tests.sh
${cabal-install}/bin/cabal v2-exec ${withTmpDb postgresql} \
${ioTestPython}/bin/pytest -- -v "$rootdir"/test/io-tests "$@"
'';
testMemory =
@@ -117,7 +128,7 @@ buildEnv
] ++ testSpecVersions;
}
# The memory tests have large dependencies (a profiled build of PostgREST)
# and are run less often than the spec tests, so we don't include them in
# and are run less often than the spec tests, so we don't include them in
# the default test environment. We make them available through a separate attribute:
// {
memoryTests =
-427
View File
@@ -1,427 +0,0 @@
#!/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 4915265535: for private or temporary use
# Colors
NC='\033[0m' # no color
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
# TAP utilities
currentTest=1
failedTests=0
bailOut(){ echo "Bail out! $1"; exit 1; }
result(){ echo -e "$1 $currentTest $2${NC}"; currentTest=$(( $currentTest + 1 )); }
todo(){ result "${YELLOW}ok" "# TODO: $*"; }
skip(){ result "${YELLOW}ok" "# SKIP: $*"; }
ok(){ result "${GREEN}ok" "- $1"; }
ko(){ result "${RED}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 --color "$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 --color "$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-schemas
replaceConfigValue "db-schemas" "test, v1" "$configFile"
# reload
kill -s SIGUSR2 $pgrPID
kill -s SIGUSR1 $pgrPID
httpStatus="$(v1SchemaParentsStatus)"
if test "$httpStatus" -eq 200
then
ok "db-schemas config reloaded with SIGUSR2"
else
ko "db-schemas 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
+1 -1
View File
@@ -3,5 +3,5 @@ db-schemas = "test"
db-anon-role = "postgrest_test_anonymous"
db-pool = 1
server-host = "127.0.0.1"
server-unix-socket = "/tmp/postgrest.sock"
server-unix-socket = "$(POSTGREST_TEST_SOCKET)"
jwt-secret = "reallyreallyreallyreallyverysafe"
+35
View File
@@ -0,0 +1,35 @@
roleclaims:
- key: '.postgrest.a_role'
data:
postgrest:
a_role: postgrest_test_author
expected_status: 200
- key: '.customObject.manyRoles[1]'
data:
customObject:
manyRoles:
- other
- postgrest_test_author
expected_status: 200
- key: '."https://www.example.com/roles"[0].value'
data:
'https://www.example.com/roles':
- value: postgrest_test_author
expected_status: 200
- key: '.myDomain[3]'
data:
myDomain:
- other
- postgrest_test_author
expected_status: 401
- key: '.myRole'
data:
role: postgrest_test_author
expected_status: 401
invalidroleclaimkeys:
- 'role.other'
- '.role##'
- '.my_role;;domain'
- '.#$$%&$%/'
- ''
- '1234'
+358
View File
@@ -0,0 +1,358 @@
"Unit tests for Input/Ouput of PostgREST seen as a black box."
import contextlib
import dataclasses
from datetime import datetime
import pathlib
import subprocess
from operator import attrgetter
import os
import signal
import time
import urllib.parse
import jwt
import pytest
import requests
import requests_unixsocket
import yaml
BASEDIR = pathlib.Path(os.path.realpath(__file__)).parent
CONFIGSDIR = BASEDIR / "configs"
FIXTURES = yaml.load((BASEDIR / "fixtures.yaml").read_text(), Loader=yaml.Loader)
BASEURL = "http://127.0.0.1:49421"
SECRET = "reallyreallyreallyreallyverysafe"
class PostgrestTimedOut(Exception):
"Connecting to PostgREST endpoint timed out."
class PostgrestError(Exception):
"Postgrest exited with a non-zero return code."
class PostgrestSession(requests_unixsocket.Session):
"HTTP client session directed at a PostgREST endpoint."
def __init__(self, baseurl, *args, **kwargs):
super(PostgrestSession, self).__init__(*args, **kwargs)
self.baseurl = baseurl
def request(self, method, url, *args, **kwargs):
fullurl = urllib.parse.urljoin(self.baseurl, url)
return super(PostgrestSession, self).request(method, fullurl, *args, **kwargs)
@dataclasses.dataclass
class PostgrestProcess:
"Running PostgREST process and its corresponding endpoint."
process: object
session: object
@pytest.fixture
def dburi():
"Postgres database connection URI."
return os.getenv("POSTGREST_TEST_CONNECTION").encode("utf-8")
def dumpconfig(configpath, moreenv=None, stdin=None):
"Dump the config as parsed by PostgREST."
env = {**os.environ, **(moreenv or {})}
command = ["postgrest", "--dump-config", configpath]
process = subprocess.Popen(
command, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE
)
process.stdin.write(stdin or b"")
result = process.communicate()[0]
process.kill()
process.wait()
if process.returncode != 0:
raise PostgrestError()
return result.decode("utf-8")
@contextlib.contextmanager
def run(configpath, stdin=None, moreenv=None, socket=None):
"Run PostgREST and yield an endpoint that is ready for connections."
env = {**os.environ, **(moreenv or {})}
if socket:
baseurl = "http+unix://" + urllib.parse.quote_plus(str(socket))
else:
baseurl = BASEURL
command = ["postgrest", configpath]
process = subprocess.Popen(command, stdin=subprocess.PIPE, env=env)
try:
process.stdin.write(stdin or b"")
process.stdin.close()
wait_until_ready(baseurl)
yield PostgrestProcess(process=process, session=PostgrestSession(baseurl))
finally:
process.kill()
process.wait()
def wait_until_ready(url):
"Wait for the given HTTP endpoint to return a status of 200."
session = requests_unixsocket.Session()
for _ in range(10):
try:
response = session.get(url, timeout=0.1)
if response.status_code == 200:
return
except requests.ConnectionError:
pass
time.sleep(0.1)
raise PostgrestTimedOut()
def authheader(token):
"Bearer token HTTP authorization header."
return {"Authorization": f"Bearer {token}"}
def jwtauthheader(claim, secret):
"Authorization header with signed JWT."
return authheader(jwt.encode(claim, secret).decode("utf-8"))
@pytest.mark.parametrize(
"expectedconfig", (CONFIGSDIR / "expected").iterdir(), ids=attrgetter("name")
)
def test_expected_config(expectedconfig):
"""
Configs as dumped by PostgREST should match an expected output.
Used to test default values, config aliases and environment variables. The
expected output for each file in 'configs', if available, is found in the
'configs/expected' directory.
"""
expected = expectedconfig.read_text()
assert dumpconfig(CONFIGSDIR / expectedconfig.name) == expected
@pytest.mark.parametrize(
"config",
[conf for conf in CONFIGSDIR.iterdir() if conf.suffix == ".config"],
ids=attrgetter("name"),
)
def test_stable_config(tmp_path, config):
"""
A dumped, re-read and re-dumped config should match the dumped config.
Note: only dump vs. re-dump must be equal, as the original config file might
be different because of default values, whitespace, and quoting.
"""
# Set environment variables that some of the configs expect. Using a
# complex ROLE_CLAIM_KEY to make sure quoting works.
env = {
"ROLE_CLAIM_KEY": '."https://www.example.com/roles"[0].value',
"POSTGREST_TEST_SOCKET": "/tmp/postgrest.sock",
}
# Some configs expect input from stdin, at least on base64.
stdin = b"Y29ubmVjdGlvbl9zdHJpbmc="
dumped = dumpconfig(config, moreenv=env, stdin=stdin)
tmpconfigpath = tmp_path / "config"
tmpconfigpath.write_text(dumped)
redumped = dumpconfig(tmpconfigpath, moreenv=env)
assert dumped == redumped
def test_socket_connection(tmp_path):
"Connections via unix domain sockets should work."
socket = tmp_path / "postgrest.sock"
env = {
"POSTGREST_TEST_SOCKET": str(socket),
}
with run(CONFIGSDIR / "unix-socket.config", socket=socket, moreenv=env):
pass
@pytest.mark.parametrize(
"secretpath",
[path for path in (BASEDIR / "secrets").iterdir() if path.suffix != ".jwt"],
ids=attrgetter("name"),
)
def test_read_secret_from_file(secretpath):
"Authorization should succeed when the secret is read from a file."
if secretpath.suffix == ".b64":
configfile = CONFIGSDIR / "base64-secret-from-file.config"
else:
configfile = CONFIGSDIR / "secret-from-file.config"
secret = secretpath.read_bytes()
headers = authheader(secretpath.with_suffix(".jwt").read_text())
with run(configfile, stdin=secret) as postgrest:
response = postgrest.session.get("/authors_only", headers=headers)
assert response.status_code == 200
def test_read_dburi_from_file_without_eol(dburi):
"Reading the dburi from a file with a single line should work."
with run(CONFIGSDIR / "dburi-from-file.config", stdin=dburi):
pass
def test_read_dburi_from_file_with_eol(dburi):
"Reading the dburi from a file containing a newline should work."
with run(CONFIGSDIR / "dburi-from-file.config", stdin=dburi + b"\n"):
pass
@pytest.mark.parametrize(
"roleclaim", FIXTURES["roleclaims"], ids=lambda claim: claim["key"]
)
def test_role_claim_key(roleclaim):
"Authorization should depend on a correct role-claim-key and JWT claim."
env = {"ROLE_CLAIM_KEY": roleclaim["key"]}
headers = jwtauthheader(roleclaim["data"], SECRET)
with run(CONFIGSDIR / "role-claim-key.config", moreenv=env) as postgrest:
response = postgrest.session.get("/authors_only", headers=headers)
assert response.status_code == roleclaim["expected_status"]
@pytest.mark.parametrize("invalidroleclaimkey", FIXTURES["invalidroleclaimkeys"])
def test_invalid_role_claim_key(invalidroleclaimkey):
"Given an invalid role-claim-key, Postgrest should exit with a non-zero exit code."
env = {"ROLE_CLAIM_KEY": invalidroleclaimkey}
with pytest.raises(PostgrestError):
dump = dumpconfig(CONFIGSDIR / "role-claim-key.config", moreenv=env)
for line in dump.split("\n"):
if line.startswith("jwt-role-claim-key"):
print(line)
def test_iat_claim():
"""
A claim with an 'iat' (issued at) attribute should be successful.
The PostgREST time cache leads to issues here, see:
https://github.com/PostgREST/postgrest/issues/1139
"""
claim = {"role": "postgrest_test_author", "iat": datetime.utcnow()}
headers = jwtauthheader(claim, SECRET)
with run(CONFIGSDIR / "simple.config") as postgrest:
for _ in range(10):
response = postgrest.session.get("/authors_only", headers=headers)
assert response.status_code == 200
time.sleep(0.5)
def test_app_settings():
"""
App settings should not reset when the db pool times out.
See: https://github.com/PostgREST/postgrest/issues/1141
"""
with run(CONFIGSDIR / "app-settings.config") as postgrest:
# Wait for the db pool to time out, set to 1s in config
time.sleep(2)
uri = "/rpc/get_guc_value?name=app.settings.external_api_secret"
response = postgrest.session.get(uri)
assert response.status_code == 200
assert response.text == '"0123456789abcdef"'
def test_app_settings_reload(tmp_path):
"App settings should be reloaded when PostgREST is sent SIGUSR2."
config = (CONFIGSDIR / "sigusr2-settings.config").read_text()
configfile = tmp_path / "test.config"
configfile.write_text(config)
uri = "/rpc/get_guc_value?name=app.settings.name_var"
with run(configfile) as postgrest:
response = postgrest.session.get(uri)
assert response.status_code == 200
assert response.text == '"John"'
# change setting
configfile.write_text(config.replace("John", "Jane"))
# reload
postgrest.process.send_signal(signal.SIGUSR2)
time.sleep(0.1)
response = postgrest.session.get(uri)
assert response.status_code == 200
assert response.text == '"Jane"'
def test_jwt_secret_reload(tmp_path):
"JWT secret should be reloaded when PostgREST is sent SIGUSR2."
config = (CONFIGSDIR / "sigusr2-settings.config").read_text()
configfile = tmp_path / "test.config"
configfile.write_text(config)
headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET)
with run(configfile) as postgrest:
response = postgrest.session.get("/authors_only", headers=headers)
assert response.status_code == 401
# change setting
configfile.write_text(config.replace("invalid" * 5, SECRET))
# reload config
postgrest.process.send_signal(signal.SIGUSR2)
time.sleep(0.1)
response = postgrest.session.get("/authors_only", headers=headers)
assert response.status_code == 200
def test_db_schema_reload(tmp_path):
"DB schema should be reloaded when PostgREST is sent SIGUSR2."
config = (CONFIGSDIR / "sigusr2-settings.config").read_text()
configfile = tmp_path / "test.config"
configfile.write_text(config)
headers = {"Accept-Profile": "v1"}
with run(configfile) as postgrest:
response = postgrest.session.get("/parents", headers=headers)
assert response.status_code == 404
# change setting
configfile.write_text(
config.replace('db-schemas = "test"', 'db-schemas = "test, v1"')
)
# reload config
postgrest.process.send_signal(signal.SIGUSR2)
# reload schema cache to verify that the config reload actually happened
postgrest.process.send_signal(signal.SIGUSR1)
time.sleep(0.1)
response = postgrest.session.get("/parents", headers=headers)
assert response.status_code == 200