From 02a88474f0c7edbc6fb28e0676bc97a4b38936fb Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 6 Dec 2020 17:35:06 +0100 Subject: [PATCH 01/44] minimal scaffolding for new io tests --- nix/tests.nix | 23 +++++++++++++++++- .../test_io.cpython-38-pytest-5.4.3.pyc | Bin 0 -> 502 bytes test/io-tests/test_io.py | 4 +++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 test/io-tests/__pycache__/test_io.cpython-38-pytest-5.4.3.pyc create mode 100644 test/io-tests/test_io.py diff --git a/nix/tests.nix b/nix/tests.nix index 767c815c7a..fc9ccc7157 100644 --- a/nix/tests.nix +++ b/nix/tests.nix @@ -15,6 +15,7 @@ , postgrestStatic , postgrestProfiled , procps +, python3 , runtimeShell }: let @@ -89,6 +90,25 @@ let ${cabal-install}/bin/cabal v2-exec ${withTmpDb postgresql} "$rootdir"/test/io-tests.sh ''; + ioTestPython = + python3.withPackages (ps: [ ps.pytest ]); + + # Provisional name until all io-tests are migrated + testIONew = + name: postgresql: + checkedShellScript + name + '' + env="$(cat ${postgrest.env})" + export PATH="$env/bin:${curl}/bin:${procps}/bin:${diffutils}/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} ${ioTestPython}/bin/py.test "$rootdir"/test/io-tests + ''; + testMemory = name: postgresql: checkedShellScript @@ -114,10 +134,11 @@ buildEnv (testSpec "postgrest-test-spec" postgresql).bin testSpecAllVersions.bin (testIO "postgrest-test-io" postgresql).bin + (testIONew "postgrest-test-io-new" postgresql).bin ] ++ 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 = diff --git a/test/io-tests/__pycache__/test_io.cpython-38-pytest-5.4.3.pyc b/test/io-tests/__pycache__/test_io.cpython-38-pytest-5.4.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c763eee26e3363413dd79039ae04f895e8623ab4 GIT binary patch literal 502 zcmY*V%}T>S5S~raCav0og5X8GD9yp9dhjGt5wAs1FS&%+Y&DW}!|WEVv={Y3>{0Ml ze1W}s@)bNen`qI6`G#+PzuC)`mFAoK9&G}^2id=85$lo39fAZBmO;o$Z{@ZH400!| zF-WY$eul7~uygR8PpXCfz~JvQb82KKWU@<;uo+B2woRvk7dC}COW8Win6il5Q#NN$ z?2&fOXs?;tzj$cHQgT#V+wEeq=gZJG&e6?YF;QHqC`R3gWIW`^3)HRSvN{!`Q$&G! zEgA~UV-@lHB8x|{qKt2aOVk}zgS!=}%JyFY>!kGSAh;1Z50K{~I74yE2TBHokm?#~ zX<#-7sn|7MmMls|ri|&P^Xeb qIy=bkFjZVP79s8a#L#?hIkMFI2V6>M?=Tn1IO9j95Qjn@iTDOHw1fu$ literal 0 HcmV?d00001 diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py new file mode 100644 index 0000000000..18756cd93f --- /dev/null +++ b/test/io-tests/test_io.py @@ -0,0 +1,4 @@ + + +def test_a(): + assert True From 8a49e5b9f8c2226f375c15982df5fecb10ded088 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 6 Dec 2020 18:09:45 +0100 Subject: [PATCH 02/44] tests for expected configs --- .gitignore | 1 + nix/tests.nix | 5 ++-- .../test_io.cpython-38-pytest-5.4.3.pyc | Bin 502 -> 0 bytes test/io-tests/test_io.py | 28 ++++++++++++++++-- 4 files changed, 30 insertions(+), 4 deletions(-) delete mode 100644 test/io-tests/__pycache__/test_io.cpython-38-pytest-5.4.3.pyc diff --git a/.gitignore b/.gitignore index 620f7c30a1..fd5e22ee22 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ result* dist-newstyle postgrest.hp postgrest.prof +__pycache__ diff --git a/nix/tests.nix b/nix/tests.nix index fc9ccc7157..05ac37e13b 100644 --- a/nix/tests.nix +++ b/nix/tests.nix @@ -91,7 +91,7 @@ let ''; ioTestPython = - python3.withPackages (ps: [ ps.pytest ]); + python3.withPackages (ps: [ ps.pytest ps.requests ]); # Provisional name until all io-tests are migrated testIONew = @@ -106,7 +106,8 @@ let cd "$rootdir" ${cabal-install}/bin/cabal v2-build ${devCabalOptions} - ${cabal-install}/bin/cabal v2-exec ${withTmpDb postgresql} ${ioTestPython}/bin/py.test "$rootdir"/test/io-tests + ${cabal-install}/bin/cabal v2-exec ${withTmpDb postgresql} \ + ${ioTestPython}/bin/pytest "$rootdir"/test/io-tests ''; testMemory = diff --git a/test/io-tests/__pycache__/test_io.cpython-38-pytest-5.4.3.pyc b/test/io-tests/__pycache__/test_io.cpython-38-pytest-5.4.3.pyc deleted file mode 100644 index c763eee26e3363413dd79039ae04f895e8623ab4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 502 zcmY*V%}T>S5S~raCav0og5X8GD9yp9dhjGt5wAs1FS&%+Y&DW}!|WEVv={Y3>{0Ml ze1W}s@)bNen`qI6`G#+PzuC)`mFAoK9&G}^2id=85$lo39fAZBmO;o$Z{@ZH400!| zF-WY$eul7~uygR8PpXCfz~JvQb82KKWU@<;uo+B2woRvk7dC}COW8Win6il5Q#NN$ z?2&fOXs?;tzj$cHQgT#V+wEeq=gZJG&e6?YF;QHqC`R3gWIW`^3)HRSvN{!`Q$&G! zEgA~UV-@lHB8x|{qKt2aOVk}zgS!=}%JyFY>!kGSAh;1Z50K{~I74yE2TBHokm?#~ zX<#-7sn|7MmMls|ri|&P^Xeb qIy=bkFjZVP79s8a#L#?hIkMFI2V6>M?=Tn1IO9j95Qjn@iTDOHw1fu$ diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 18756cd93f..4c96bc0bad 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -1,4 +1,28 @@ +'Tests for inputs and outputs of PostgREST.' +import subprocess +import os -def test_a(): - assert True +import requests + + +basedir = os.path.dirname(os.path.realpath(__file__)) +expectedconfigs = os.listdir(os.path.join(basedir, 'configs/expected')) + + +def dumpconfig(configpath): + 'Dump a config as parsed by PostgREST.' + + command = ['postgrest', '--dump-config', configpath] + return subprocess.run(command, capture_output=True, check=True).stdout.decode('utf-8') + + +def test_configs(): + 'PostgREST should parse configs as expected.' + + for config in expectedconfigs: + expectedpath = os.path.join(basedir, 'configs', 'expected', config) + with open(expectedpath) as expectedfile: + expected = expectedfile.read() + + assert dumpconfig(os.path.join(basedir, 'configs', config)) == expected From 40b2860ff1fde40364107b316aa8d21d5ad2f24b Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 6 Dec 2020 19:36:21 +0100 Subject: [PATCH 03/44] refactor with pathlib and parametrized fixtures --- test/io-tests/test_io.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 4c96bc0bad..596de8fb9b 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -1,13 +1,21 @@ 'Tests for inputs and outputs of PostgREST.' +import pathlib import subprocess import os +import pytest import requests -basedir = os.path.dirname(os.path.realpath(__file__)) -expectedconfigs = os.listdir(os.path.join(basedir, 'configs/expected')) +basedir = pathlib.Path(os.path.realpath(__file__)).parent +expectedconfigs = list((basedir / 'configs' / 'expected').iterdir()) + + +@pytest.fixture(params=expectedconfigs) +def expectedconfig(request): + 'Fixture for all expected configs' + return request.param def dumpconfig(configpath): @@ -17,12 +25,9 @@ def dumpconfig(configpath): return subprocess.run(command, capture_output=True, check=True).stdout.decode('utf-8') -def test_configs(): +def test_expected_config(expectedconfig): 'PostgREST should parse configs as expected.' - for config in expectedconfigs: - expectedpath = os.path.join(basedir, 'configs', 'expected', config) - with open(expectedpath) as expectedfile: - expected = expectedfile.read() + expected = (basedir / 'configs' / 'expected' / expectedconfig).read_text() - assert dumpconfig(os.path.join(basedir, 'configs', config)) == expected + assert dumpconfig(basedir / 'configs' / expectedconfig) == expected From d2df4c867efd633501d66024051862b9b2e3eca7 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 6 Dec 2020 20:20:48 +0100 Subject: [PATCH 04/44] add tests on re-dumping config --- test/io-tests/test_io.py | 51 ++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 596de8fb9b..394589d872 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -2,6 +2,7 @@ import pathlib import subprocess +import tempfile import os import pytest @@ -9,25 +10,61 @@ basedir = pathlib.Path(os.path.realpath(__file__)).parent +configs = [path for path in (basedir / 'configs').iterdir() if path.is_file()] expectedconfigs = list((basedir / 'configs' / 'expected').iterdir()) -@pytest.fixture(params=expectedconfigs) +@pytest.fixture(params=configs, ids=[conf.name for conf in configs]) +def configpath(request): + 'Fixture for all config paths.' + return basedir / 'configs' / request.param + + +@pytest.fixture(params=expectedconfigs, ids=[conf.name for conf in expectedconfigs]) def expectedconfig(request): - 'Fixture for all expected configs' + 'Fixture for all expected configs.' return request.param -def dumpconfig(configpath): - 'Dump a config as parsed by PostgREST.' +def dumpconfig(configpath, moreenv=None): + 'Dump the config as parsed by PostgREST.' + + env = os.environ + if moreenv: + env = {**env, **moreenv} command = ['postgrest', '--dump-config', configpath] - return subprocess.run(command, capture_output=True, check=True).stdout.decode('utf-8') + result = subprocess.run(command, env=env, capture_output=True, check=True) + return result.stdout.decode('utf-8') def test_expected_config(expectedconfig): - 'PostgREST should parse configs as expected.' + ''' + Configs as dumped by PostgREST should match an expected output. - expected = (basedir / 'configs' / 'expected' / expectedconfig).read_text() + 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 = (basedir / 'configs' / 'expected' / expectedconfig).read_text() assert dumpconfig(basedir / 'configs' / expectedconfig) == expected + + +def test_stable_config(configpath): + ''' + 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. + + ''' + env = {'ROLE_CLAIM_KEY': '."https://www.example.com/roles"[0].value'} + dumped = dumpconfig(configpath, moreenv=env) + + with tempfile.TemporaryDirectory() as tmpdir: + tmpconfigpath = pathlib.Path(tmpdir, 'config') + tmpconfigpath.write_text(dumped) + redumped = dumpconfig(tmpconfigpath, moreenv=env) + + assert dumped == redumped From e4a89b64549ebfc55b81b70c474fcf10c8141bd8 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 6 Dec 2020 21:24:03 +0100 Subject: [PATCH 05/44] add read secret from file tests --- test/io-tests/test_io.py | 61 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 394589d872..07b4c0615a 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -1,17 +1,22 @@ 'Tests for inputs and outputs of PostgREST.' +import contextlib import pathlib import subprocess import tempfile import os +import time import pytest import requests +BASEURL = 'http://127.0.0.1:49421' + basedir = pathlib.Path(os.path.realpath(__file__)).parent configs = [path for path in (basedir / 'configs').iterdir() if path.is_file()] expectedconfigs = list((basedir / 'configs' / 'expected').iterdir()) +secrets = [path for path in (basedir / 'secrets').iterdir() if path.suffix != '.jwt'] @pytest.fixture(params=configs, ids=[conf.name for conf in configs]) @@ -26,9 +31,14 @@ def expectedconfig(request): return request.param +@pytest.fixture(params=secrets, ids=[secret.name for secret in secrets]) +def secretpath(request): + 'Fixture for all secrets.' + return request.param + + def dumpconfig(configpath, moreenv=None): 'Dump the config as parsed by PostgREST.' - env = os.environ if moreenv: env = {**env, **moreenv} @@ -38,6 +48,39 @@ def dumpconfig(configpath, moreenv=None): return result.stdout.decode('utf-8') +@contextlib.contextmanager +def run(configpath, stdin=None): + 'Run PostgREST.' + command = ['postgrest', configpath] + process = subprocess.Popen(command, stdin=subprocess.PIPE) + + try: + if stdin: + process.stdin.write(stdin) + process.stdin.close() + + waitfor200(BASEURL) + yield BASEURL + finally: + process.kill() + process.wait() + + +def waitfor200(url): + for i in range(10): + try: + response = requests.get(url, timeout=0.1) + + if response.status_code == 200: + return + except requests.ConnectionError: + pass + + time.sleep(.1) + + raise Exception('Waiting for PostgREST ready timed out') + + def test_expected_config(expectedconfig): ''' Configs as dumped by PostgREST should match an expected output. @@ -68,3 +111,19 @@ def test_stable_config(configpath): redumped = dumpconfig(tmpconfigpath, moreenv=env) assert dumped == redumped + + +def test_read_secret_from_file(secretpath): + if secretpath.suffix == '.b64': + configfile = basedir / 'configs' / 'base64-secret-from-file.config' + else: + configfile = basedir / 'configs' / 'secret-from-file.config' + + secret = secretpath.read_bytes() + + jwt = secretpath.with_suffix('.jwt').read_text() + headers = {'Authorization': f'Bearer {jwt}'} + + with run(configfile, stdin=secret) as url: + response = requests.get(f'{url}/authors_only', headers=headers) + assert response.status_code == 200 From cd38b5a298ce36989e231057fb07a409a6f1d19c Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 6 Dec 2020 21:32:50 +0100 Subject: [PATCH 06/44] plug new tests into CI --- .circleci/config.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f09cb966cb..eb3ae183a8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -223,7 +223,9 @@ jobs: when: always - run: name: Run io tests - command: postgrest-test-io + command: | + postgrest-test-io + postgrest-test-io-new when: always - run: name: Run memory tests From b3bf6e0bcb40dd53db0ce963e2f1ed0e788aaa7a Mon Sep 17 00:00:00 2001 From: Remo Rechkemmer <59358383+monacoremo@users.noreply.github.com> Date: Sun, 6 Dec 2020 22:11:27 +0100 Subject: [PATCH 07/44] More green! --- nix/tests.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/tests.nix b/nix/tests.nix index 05ac37e13b..0dcb9efc84 100644 --- a/nix/tests.nix +++ b/nix/tests.nix @@ -107,7 +107,7 @@ let ${cabal-install}/bin/cabal v2-build ${devCabalOptions} ${cabal-install}/bin/cabal v2-exec ${withTmpDb postgresql} \ - ${ioTestPython}/bin/pytest "$rootdir"/test/io-tests + ${ioTestPython}/bin/pytest -v "$rootdir"/test/io-tests ''; testMemory = From c9ef548fe49f98e11b26fdba662cb4b5b12d9f5e Mon Sep 17 00:00:00 2001 From: Remo Rechkemmer <59358383+monacoremo@users.noreply.github.com> Date: Sun, 6 Dec 2020 22:20:43 +0100 Subject: [PATCH 08/44] Not enough green --- nix/tests.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/tests.nix b/nix/tests.nix index 0dcb9efc84..5912c239da 100644 --- a/nix/tests.nix +++ b/nix/tests.nix @@ -107,7 +107,7 @@ let ${cabal-install}/bin/cabal v2-build ${devCabalOptions} ${cabal-install}/bin/cabal v2-exec ${withTmpDb postgresql} \ - ${ioTestPython}/bin/pytest -v "$rootdir"/test/io-tests + ${ioTestPython}/bin/pytest -vv "$rootdir"/test/io-tests ''; testMemory = From 06346de949474bf6abf1cad14d1b768d9cd9ed3a Mon Sep 17 00:00:00 2001 From: Remo Rechkemmer <59358383+monacoremo@users.noreply.github.com> Date: Sun, 6 Dec 2020 23:11:27 +0100 Subject: [PATCH 09/44] Update tests.nix --- nix/tests.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/tests.nix b/nix/tests.nix index 5912c239da..3cfc5a234c 100644 --- a/nix/tests.nix +++ b/nix/tests.nix @@ -100,14 +100,14 @@ let 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} \ - ${ioTestPython}/bin/pytest -vv "$rootdir"/test/io-tests + ${ioTestPython}/bin/pytest -- -v "$rootdir"/test/io-tests ''; testMemory = From e33f61ade66f7c8a43e7533c1da47f970c70179e Mon Sep 17 00:00:00 2001 From: monacoremo Date: Fri, 11 Dec 2020 21:19:40 +0100 Subject: [PATCH 10/44] read dburi from file --- test/io-tests/test_io.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 07b4c0615a..1c5c2ada20 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -17,6 +17,8 @@ configs = [path for path in (basedir / 'configs').iterdir() if path.is_file()] expectedconfigs = list((basedir / 'configs' / 'expected').iterdir()) secrets = [path for path in (basedir / 'secrets').iterdir() if path.suffix != '.jwt'] +dburi = os.getenv('POSTGREST_TEST_CONNECTION') +dburifromfileconfig = basedir / 'configs' / 'dburi-from-file.config' @pytest.fixture(params=configs, ids=[conf.name for conf in configs]) @@ -127,3 +129,15 @@ def test_read_secret_from_file(secretpath): with run(configfile, stdin=secret) as url: response = requests.get(f'{url}/authors_only', headers=headers) assert response.status_code == 200 + + +def test_read_dburi_from_file_withouteol(): + with run(dburifromfileconfig, stdin=dburi.encode('utf-8')) as url: + response = requests.get(f'{url}/') + assert response.status_code == 200 + + +def test_read_dburi_from_file_witheol(): + with run(dburifromfileconfig, stdin=dburi.encode('utf-8') + b'\n') as url: + response = requests.get(f'{url}/') + assert response.status_code == 200 From 0948d584b22f7ef1e4d1c0f9e36ca6ea77545d1e Mon Sep 17 00:00:00 2001 From: monacoremo Date: Fri, 11 Dec 2020 21:45:59 +0100 Subject: [PATCH 11/44] test role claim key --- test/io-tests/test_io.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 1c5c2ada20..bdfd13fee3 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -19,6 +19,20 @@ secrets = [path for path in (basedir / 'secrets').iterdir() if path.suffix != '.jwt'] dburi = os.getenv('POSTGREST_TEST_CONNECTION') dburifromfileconfig = basedir / 'configs' / 'dburi-from-file.config' +roleclaimkeyconfig = basedir / 'configs' / 'role-claim-key.config' + +roleclaimkeys = [ + 'role.other', + '.role##', + '.my_role;;domain', + '.#$$%&$%/', + '', + '1234' + ] + + +class TimeOutException(Exception): + pass @pytest.fixture(params=configs, ids=[conf.name for conf in configs]) @@ -39,6 +53,12 @@ def secretpath(request): return request.param +@pytest.fixture(params=roleclaimkeys) +def roleclaimkey(request): + 'Fixture for all secrets.' + return request.param + + def dumpconfig(configpath, moreenv=None): 'Dump the config as parsed by PostgREST.' env = os.environ @@ -51,8 +71,12 @@ def dumpconfig(configpath, moreenv=None): @contextlib.contextmanager -def run(configpath, stdin=None): +def run(configpath, stdin=None, moreenv=None): 'Run PostgREST.' + env = os.environ + if moreenv: + env = {**env, **moreenv} + command = ['postgrest', configpath] process = subprocess.Popen(command, stdin=subprocess.PIPE) @@ -80,7 +104,7 @@ def waitfor200(url): time.sleep(.1) - raise Exception('Waiting for PostgREST ready timed out') + raise TimeOutException('Waiting for PostgREST ready timed out') def test_expected_config(expectedconfig): @@ -141,3 +165,11 @@ def test_read_dburi_from_file_witheol(): with run(dburifromfileconfig, stdin=dburi.encode('utf-8') + b'\n') as url: response = requests.get(f'{url}/') assert response.status_code == 200 + + +def test_role_claim_key(roleclaimkey): + env = {'ROLE_CLAIM_KEY': roleclaimkey} + + with pytest.raises(TimeOutException): + with run(roleclaimkeyconfig, moreenv=env): + assert False From 1d5164ff21538d87609c6d2a654b4f6cc06268f9 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Fri, 11 Dec 2020 21:56:59 +0100 Subject: [PATCH 12/44] invalid role claim keys --- test/io-tests/test_io.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index bdfd13fee3..00f720c038 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -21,7 +21,7 @@ dburifromfileconfig = basedir / 'configs' / 'dburi-from-file.config' roleclaimkeyconfig = basedir / 'configs' / 'role-claim-key.config' -roleclaimkeys = [ +invalidroleclaimkeys = [ 'role.other', '.role##', '.my_role;;domain', @@ -53,8 +53,8 @@ def secretpath(request): return request.param -@pytest.fixture(params=roleclaimkeys) -def roleclaimkey(request): +@pytest.fixture(params=invalidroleclaimkeys) +def invalidroleclaimkey(request): 'Fixture for all secrets.' return request.param @@ -93,7 +93,7 @@ def run(configpath, stdin=None, moreenv=None): def waitfor200(url): - for i in range(10): + for i in range(2): try: response = requests.get(url, timeout=0.1) @@ -167,8 +167,8 @@ def test_read_dburi_from_file_witheol(): assert response.status_code == 200 -def test_role_claim_key(roleclaimkey): - env = {'ROLE_CLAIM_KEY': roleclaimkey} +def test_invalid_role_claim_key(invalidroleclaimkey): + env = {'ROLE_CLAIM_KEY': invalidroleclaimkey} with pytest.raises(TimeOutException): with run(roleclaimkeyconfig, moreenv=env): From 303f99ced275cd90e1d505149df0eacd41bb96ca Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sat, 12 Dec 2020 11:47:55 +0100 Subject: [PATCH 13/44] add role claim key tests --- nix/tests.nix | 2 +- test/io-tests/test_io.py | 153 +++++++++++++++++++++++++-------------- 2 files changed, 100 insertions(+), 55 deletions(-) diff --git a/nix/tests.nix b/nix/tests.nix index 3cfc5a234c..88e665bd1f 100644 --- a/nix/tests.nix +++ b/nix/tests.nix @@ -91,7 +91,7 @@ let ''; ioTestPython = - python3.withPackages (ps: [ ps.pytest ps.requests ]); + python3.withPackages (ps: [ ps.pytest ps.requests ps.pyjwt ]); # Provisional name until all io-tests are migrated testIONew = diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 00f720c038..9ef47c758e 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -1,6 +1,7 @@ -'Tests for inputs and outputs of PostgREST.' +"Tests for inputs and outputs of PostgREST." import contextlib +import dataclasses import pathlib import subprocess import tempfile @@ -9,26 +10,55 @@ import pytest import requests +import jwt -BASEURL = 'http://127.0.0.1:49421' - +BASEURL = "http://127.0.0.1:49421" +secret = "reallyreallyreallyreallyverysafe" basedir = pathlib.Path(os.path.realpath(__file__)).parent -configs = [path for path in (basedir / 'configs').iterdir() if path.is_file()] -expectedconfigs = list((basedir / 'configs' / 'expected').iterdir()) -secrets = [path for path in (basedir / 'secrets').iterdir() if path.suffix != '.jwt'] -dburi = os.getenv('POSTGREST_TEST_CONNECTION') -dburifromfileconfig = basedir / 'configs' / 'dburi-from-file.config' -roleclaimkeyconfig = basedir / 'configs' / 'role-claim-key.config' +configs = [path for path in (basedir / "configs").iterdir() if path.is_file()] +expectedconfigs = list((basedir / "configs" / "expected").iterdir()) +secrets = [path for path in (basedir / "secrets").iterdir() if path.suffix != ".jwt"] +dburi = os.getenv("POSTGREST_TEST_CONNECTION").encode("utf-8") +dburifromfileconfig = basedir / "configs" / "dburi-from-file.config" +roleclaimkeyconfig = basedir / "configs" / "role-claim-key.config" + + +@dataclasses.dataclass +class RoleClaimCase: + key: str + data: dict + expected_status: int + + +roleclaimcases = [ + RoleClaimCase( + ".postgrest.a_role", {"postgrest": {"a_role": "postgrest_test_author"}}, 200 + ), + RoleClaimCase( + ".customObject.manyRoles[1]", + {"customObject": {"manyRoles": ["other", "postgrest_test_author"]}}, + 200, + ), + RoleClaimCase( + '."https://www.example.com/roles"[0].value', + {"https://www.example.com/roles": [{"value": "postgrest_test_author"}]}, + 200, + ), + RoleClaimCase( + ".myDomain[3]", {"myDomain": ["other", "postgrest_test_author"]}, 401 + ), + RoleClaimCase(".myRole", {"role": "postgrest_test_author"}, 401), +] invalidroleclaimkeys = [ - 'role.other', - '.role##', - '.my_role;;domain', - '.#$$%&$%/', - '', - '1234' - ] + "role.other", + ".role##", + ".my_role;;domain", + ".#$$%&$%/", + "", + "1234", +] class TimeOutException(Exception): @@ -37,48 +67,54 @@ class TimeOutException(Exception): @pytest.fixture(params=configs, ids=[conf.name for conf in configs]) def configpath(request): - 'Fixture for all config paths.' - return basedir / 'configs' / request.param + "Fixture for all config paths." + return basedir / "configs" / request.param @pytest.fixture(params=expectedconfigs, ids=[conf.name for conf in expectedconfigs]) def expectedconfig(request): - 'Fixture for all expected configs.' + "Fixture for all expected configs." return request.param @pytest.fixture(params=secrets, ids=[secret.name for secret in secrets]) def secretpath(request): - 'Fixture for all secrets.' + "Fixture for all secrets." + return request.param + + +@pytest.fixture(params=roleclaimcases, ids=[case.key for case in roleclaimcases]) +def roleclaimcase(request): + "Fixture for role claim test cases." return request.param @pytest.fixture(params=invalidroleclaimkeys) def invalidroleclaimkey(request): - 'Fixture for all secrets.' + "Fixture for all invalid role claim keys." return request.param def dumpconfig(configpath, moreenv=None): - 'Dump the config as parsed by PostgREST.' + "Dump the config as parsed by PostgREST." env = os.environ if moreenv: env = {**env, **moreenv} - command = ['postgrest', '--dump-config', configpath] + command = ["postgrest", "--dump-config", configpath] result = subprocess.run(command, env=env, capture_output=True, check=True) - return result.stdout.decode('utf-8') + return result.stdout.decode("utf-8") @contextlib.contextmanager def run(configpath, stdin=None, moreenv=None): - 'Run PostgREST.' + "Run PostgREST." env = os.environ if moreenv: env = {**env, **moreenv} - command = ['postgrest', configpath] - process = subprocess.Popen(command, stdin=subprocess.PIPE) + command = ["postgrest", configpath] + process = subprocess.Popen(command, stdin=subprocess.PIPE, env=env) try: if stdin: @@ -93,7 +129,7 @@ def run(configpath, stdin=None, moreenv=None): def waitfor200(url): - for i in range(2): + for i in range(10): try: response = requests.get(url, timeout=0.1) @@ -102,37 +138,37 @@ def waitfor200(url): except requests.ConnectionError: pass - time.sleep(.1) + time.sleep(0.1) - raise TimeOutException('Waiting for PostgREST ready timed out') + raise TimeOutException() 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 = (basedir / 'configs' / 'expected' / expectedconfig).read_text() - assert dumpconfig(basedir / 'configs' / expectedconfig) == expected + """ + expected = (basedir / "configs" / "expected" / expectedconfig).read_text() + assert dumpconfig(basedir / "configs" / expectedconfig) == expected def test_stable_config(configpath): - ''' + """ 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. - ''' - env = {'ROLE_CLAIM_KEY': '."https://www.example.com/roles"[0].value'} + """ + env = {"ROLE_CLAIM_KEY": '."https://www.example.com/roles"[0].value'} dumped = dumpconfig(configpath, moreenv=env) with tempfile.TemporaryDirectory() as tmpdir: - tmpconfigpath = pathlib.Path(tmpdir, 'config') + tmpconfigpath = pathlib.Path(tmpdir, "config") tmpconfigpath.write_text(dumped) redumped = dumpconfig(tmpconfigpath, moreenv=env) @@ -140,36 +176,45 @@ def test_stable_config(configpath): def test_read_secret_from_file(secretpath): - if secretpath.suffix == '.b64': - configfile = basedir / 'configs' / 'base64-secret-from-file.config' + if secretpath.suffix == ".b64": + configfile = basedir / "configs" / "base64-secret-from-file.config" else: - configfile = basedir / 'configs' / 'secret-from-file.config' + configfile = basedir / "configs" / "secret-from-file.config" secret = secretpath.read_bytes() - jwt = secretpath.with_suffix('.jwt').read_text() - headers = {'Authorization': f'Bearer {jwt}'} + jwt = secretpath.with_suffix(".jwt").read_text() + headers = {"Authorization": f"Bearer {jwt}"} with run(configfile, stdin=secret) as url: - response = requests.get(f'{url}/authors_only', headers=headers) + response = requests.get(f"{url}/authors_only", headers=headers) assert response.status_code == 200 -def test_read_dburi_from_file_withouteol(): - with run(dburifromfileconfig, stdin=dburi.encode('utf-8')) as url: - response = requests.get(f'{url}/') +def test_read_dburi_from_file_without_eol(): + with run(dburifromfileconfig, stdin=dburi) as url: + response = requests.get(f"{url}/") assert response.status_code == 200 -def test_read_dburi_from_file_witheol(): - with run(dburifromfileconfig, stdin=dburi.encode('utf-8') + b'\n') as url: - response = requests.get(f'{url}/') +def test_read_dburi_from_file_with_eol(): + with run(dburifromfileconfig, stdin=dburi + b"\n") as url: + response = requests.get(f"{url}/") assert response.status_code == 200 +def test_role_claim_key(roleclaimcase): + env = {"ROLE_CLAIM_KEY": roleclaimcase.key} + token = jwt.encode(roleclaimcase.data, secret).decode("utf-8") + headers = {"Authorization": f"Bearer {token}"} + + with run(roleclaimkeyconfig, moreenv=env) as url: + response = requests.get(f"{url}/authors_only", headers=headers) + assert response.status_code == roleclaimcase.expected_status + + def test_invalid_role_claim_key(invalidroleclaimkey): - env = {'ROLE_CLAIM_KEY': invalidroleclaimkey} + env = {"ROLE_CLAIM_KEY": invalidroleclaimkey} - with pytest.raises(TimeOutException): - with run(roleclaimkeyconfig, moreenv=env): - assert False + with pytest.raises(subprocess.CalledProcessError): + dumpconfig(roleclaimkeyconfig, moreenv=env) From 7e5ca4324ecd91e03f8612ced0ec12641f71fffe Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sat, 12 Dec 2020 11:50:40 +0100 Subject: [PATCH 14/44] add styling for Python files --- nix/style.nix | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nix/style.nix b/nix/style.nix index 9f2a7165e6..33fc658524 100644 --- a/nix/style.nix +++ b/nix/style.nix @@ -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 From e4e29cf6c4c1fb2f74af43bfa2860757612dde91 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sat, 12 Dec 2020 15:28:19 +0100 Subject: [PATCH 15/44] add iat claim test --- test/io-tests/test_io.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 9ef47c758e..c464c8749b 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -2,6 +2,7 @@ import contextlib import dataclasses +from datetime import datetime import pathlib import subprocess import tempfile @@ -218,3 +219,16 @@ def test_invalid_role_claim_key(invalidroleclaimkey): with pytest.raises(subprocess.CalledProcessError): dumpconfig(roleclaimkeyconfig, moreenv=env) + + +def test_iat_claim(): + claim = {"role": "postgrest_test_author", "iat": datetime.utcnow()} + token = jwt.encode(claim, secret).decode("utf-8") + headers = {"Authorization": f"Bearer {token}"} + + with run(basedir / "configs" / "simple.config") as url: + for _ in range(10): + response = requests.get(f"{url}/authors_only", headers=headers) + assert response.status_code == 200 + + time.sleep(.5) From 367a5047087445f7b95fa8a207b5d161a2c9a08d Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sat, 12 Dec 2020 16:52:14 +0100 Subject: [PATCH 16/44] test app settings --- nix/tests.nix | 2 +- test/io-tests/test_io.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/nix/tests.nix b/nix/tests.nix index 88e665bd1f..90340c6654 100644 --- a/nix/tests.nix +++ b/nix/tests.nix @@ -107,7 +107,7 @@ let ${cabal-install}/bin/cabal v2-build ${devCabalOptions} ${cabal-install}/bin/cabal v2-exec ${withTmpDb postgresql} \ - ${ioTestPython}/bin/pytest -- -v "$rootdir"/test/io-tests + ${ioTestPython}/bin/pytest -- -v "$rootdir"/test/io-tests "$@" ''; testMemory = diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index c464c8749b..195f2bfd73 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -232,3 +232,10 @@ def test_iat_claim(): assert response.status_code == 200 time.sleep(.5) + +def test_app_settings(): + with run(basedir / "configs" / "app-settings.config") as baseurl: + url = f"{baseurl}/rpc/get_guc_value?name=app.settings.external_api_secret" + response = requests.get(url) + assert response.status_code == 200 + assert response.text == '"0123456789abcdef"' From b08c0845d1e78bcbbae42edcd5fd6c644ffc24fb Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sat, 12 Dec 2020 16:53:08 +0100 Subject: [PATCH 17/44] style --- test/io-tests/test_io.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 195f2bfd73..da42352b71 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -231,7 +231,8 @@ def test_iat_claim(): response = requests.get(f"{url}/authors_only", headers=headers) assert response.status_code == 200 - time.sleep(.5) + time.sleep(0.5) + def test_app_settings(): with run(basedir / "configs" / "app-settings.config") as baseurl: From 6f48c62d32fffe5304dce4c100ff2a199a2039d1 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sat, 12 Dec 2020 17:17:07 +0100 Subject: [PATCH 18/44] add app settings reload test --- test/io-tests/test_io.py | 66 +++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index da42352b71..032bb58a3b 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -7,6 +7,7 @@ import subprocess import tempfile import os +import signal import time import pytest @@ -25,6 +26,12 @@ roleclaimkeyconfig = basedir / "configs" / "role-claim-key.config" +@dataclasses.dataclass +class PostgrestProcess: + baseurl: str + process: object + + @dataclasses.dataclass class RoleClaimCase: key: str @@ -123,7 +130,7 @@ def run(configpath, stdin=None, moreenv=None): process.stdin.close() waitfor200(BASEURL) - yield BASEURL + yield PostgrestProcess(baseurl=BASEURL, process=process) finally: process.kill() process.wait() @@ -157,7 +164,7 @@ def test_expected_config(expectedconfig): assert dumpconfig(basedir / "configs" / expectedconfig) == expected -def test_stable_config(configpath): +def test_stable_config(tmp_path, configpath): """ A dumped, re-read and re-dumped config should match the dumped config. @@ -168,10 +175,9 @@ def test_stable_config(configpath): env = {"ROLE_CLAIM_KEY": '."https://www.example.com/roles"[0].value'} dumped = dumpconfig(configpath, moreenv=env) - with tempfile.TemporaryDirectory() as tmpdir: - tmpconfigpath = pathlib.Path(tmpdir, "config") - tmpconfigpath.write_text(dumped) - redumped = dumpconfig(tmpconfigpath, moreenv=env) + tmpconfigpath = tmp_path / "config" + tmpconfigpath.write_text(dumped) + redumped = dumpconfig(tmpconfigpath, moreenv=env) assert dumped == redumped @@ -187,20 +193,20 @@ def test_read_secret_from_file(secretpath): jwt = secretpath.with_suffix(".jwt").read_text() headers = {"Authorization": f"Bearer {jwt}"} - with run(configfile, stdin=secret) as url: - response = requests.get(f"{url}/authors_only", headers=headers) + with run(configfile, stdin=secret) as process: + response = requests.get(f"{process.baseurl}/authors_only", headers=headers) assert response.status_code == 200 def test_read_dburi_from_file_without_eol(): - with run(dburifromfileconfig, stdin=dburi) as url: - response = requests.get(f"{url}/") + with run(dburifromfileconfig, stdin=dburi) as process: + response = requests.get(f"{process.baseurl}/") assert response.status_code == 200 def test_read_dburi_from_file_with_eol(): - with run(dburifromfileconfig, stdin=dburi + b"\n") as url: - response = requests.get(f"{url}/") + with run(dburifromfileconfig, stdin=dburi + b"\n") as process: + response = requests.get(f"{process.baseurl}/") assert response.status_code == 200 @@ -209,8 +215,8 @@ def test_role_claim_key(roleclaimcase): token = jwt.encode(roleclaimcase.data, secret).decode("utf-8") headers = {"Authorization": f"Bearer {token}"} - with run(roleclaimkeyconfig, moreenv=env) as url: - response = requests.get(f"{url}/authors_only", headers=headers) + with run(roleclaimkeyconfig, moreenv=env) as process: + response = requests.get(f"{process.baseurl}/authors_only", headers=headers) assert response.status_code == roleclaimcase.expected_status @@ -226,17 +232,41 @@ def test_iat_claim(): token = jwt.encode(claim, secret).decode("utf-8") headers = {"Authorization": f"Bearer {token}"} - with run(basedir / "configs" / "simple.config") as url: + with run(basedir / "configs" / "simple.config") as process: for _ in range(10): - response = requests.get(f"{url}/authors_only", headers=headers) + response = requests.get(f"{process.baseurl}/authors_only", headers=headers) assert response.status_code == 200 time.sleep(0.5) def test_app_settings(): - with run(basedir / "configs" / "app-settings.config") as baseurl: - url = f"{baseurl}/rpc/get_guc_value?name=app.settings.external_api_secret" + with run(basedir / "configs" / "app-settings.config") as process: + url = ( + f"{process.baseurl}/rpc/get_guc_value?name=app.settings.external_api_secret" + ) response = requests.get(url) assert response.status_code == 200 assert response.text == '"0123456789abcdef"' + + +def test_app_settings_reload(tmp_path): + config = (basedir / "configs" / "sigusr2-settings.config").read_text() + configfile = tmp_path / "test.config" + configfile.write_text(config) + + with run(configfile) as process: + url = f"{process.baseurl}/rpc/get_guc_value?name=app.settings.name_var" + + response = requests.get(url) + assert response.status_code == 200 + assert response.text == '"John"' + + # change setting + configfile.write_text(config.replace("John", "Jane")) + # reload + process.process.send_signal(signal.SIGUSR2) + + response = requests.get(url) + assert response.status_code == 200 + assert response.text == '"Jane"' From 605ee1205dbe92533626f4d61ebd774197f03ef8 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sat, 12 Dec 2020 17:26:49 +0100 Subject: [PATCH 19/44] add jwt secret reload test --- test/io-tests/test_io.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 032bb58a3b..34655ced53 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -270,3 +270,27 @@ def test_app_settings_reload(tmp_path): response = requests.get(url) assert response.status_code == 200 assert response.text == '"Jane"' + + +def test_jwt_secret_reload(tmp_path): + config = (basedir / "configs" / "sigusr2-settings.config").read_text() + configfile = tmp_path / "test.config" + configfile.write_text(config) + + claim = {"role": "postgrest_test_author"} + token = jwt.encode(claim, secret).decode("utf-8") + headers = {"Authorization": f"Bearer {token}"} + + with run(configfile) as process: + url = f"{process.baseurl}/authors_only" + + response = requests.get(url, headers=headers) + assert response.status_code == 401 + + # change setting + configfile.write_text(config.replace("invalid" * 5, secret)) + # reload + process.process.send_signal(signal.SIGUSR2) + + response = requests.get(url, headers=headers) + assert response.status_code == 200 From 488035674b64791dda527d8d263fed4f238bbf4a Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sat, 12 Dec 2020 17:45:11 +0100 Subject: [PATCH 20/44] add db schema reload test --- test/io-tests/test_io.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 34655ced53..b194a9778e 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -294,3 +294,28 @@ def test_jwt_secret_reload(tmp_path): response = requests.get(url, headers=headers) assert response.status_code == 200 + + +def test_db_schema_reload(tmp_path): + config = (basedir / "configs" / "sigusr2-settings.config").read_text() + configfile = tmp_path / "test.config" + configfile.write_text(config) + + headers = {"Accept-Profile": "v1"} + + with run(configfile) as process: + url = f"{process.baseurl}/parents" + + response = requests.get(url, headers=headers) + assert response.status_code == 404 + + # change setting + configfile.write_text( + config.replace('db-schema = "test"', 'db-schema = "test, v1"') + ) + # reload + process.process.send_signal(signal.SIGUSR2) + process.process.send_signal(signal.SIGUSR1) + + response = requests.get(url, headers=headers) + assert response.status_code == 200 From d4d449277f4e464a2ae0fbf3f9b6ff9db957ffbc Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sat, 12 Dec 2020 18:04:03 +0100 Subject: [PATCH 21/44] prepare for unix domain sockets --- nix/tests.nix | 2 +- test/io-tests/test_io.py | 56 +++++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/nix/tests.nix b/nix/tests.nix index 90340c6654..35857bb26a 100644 --- a/nix/tests.nix +++ b/nix/tests.nix @@ -91,7 +91,7 @@ let ''; ioTestPython = - python3.withPackages (ps: [ ps.pytest ps.requests ps.pyjwt ]); + python3.withPackages (ps: [ ps.pytest ps.requests ps.requests-unixsocket ps.pyjwt ]); # Provisional name until all io-tests are migrated testIONew = diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index b194a9778e..d8fde77638 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -10,9 +10,11 @@ import signal import time +import jwt import pytest import requests -import jwt +import requests_unixsocket + BASEURL = "http://127.0.0.1:49421" @@ -73,6 +75,12 @@ class TimeOutException(Exception): pass +@pytest.fixture +def session(): + "Session for http requests." + return requests_unixsocket.Session() + + @pytest.fixture(params=configs, ids=[conf.name for conf in configs]) def configpath(request): "Fixture for all config paths." @@ -137,9 +145,11 @@ def run(configpath, stdin=None, moreenv=None): def waitfor200(url): + session = requests_unixsocket.Session() + for i in range(10): try: - response = requests.get(url, timeout=0.1) + response = session.get(url, timeout=0.1) if response.status_code == 200: return @@ -182,7 +192,7 @@ def test_stable_config(tmp_path, configpath): assert dumped == redumped -def test_read_secret_from_file(secretpath): +def test_read_secret_from_file(session, secretpath): if secretpath.suffix == ".b64": configfile = basedir / "configs" / "base64-secret-from-file.config" else: @@ -194,29 +204,29 @@ def test_read_secret_from_file(secretpath): headers = {"Authorization": f"Bearer {jwt}"} with run(configfile, stdin=secret) as process: - response = requests.get(f"{process.baseurl}/authors_only", headers=headers) + response = session.get(f"{process.baseurl}/authors_only", headers=headers) assert response.status_code == 200 -def test_read_dburi_from_file_without_eol(): +def test_read_dburi_from_file_without_eol(session): with run(dburifromfileconfig, stdin=dburi) as process: - response = requests.get(f"{process.baseurl}/") + response = session.get(f"{process.baseurl}/") assert response.status_code == 200 -def test_read_dburi_from_file_with_eol(): +def test_read_dburi_from_file_with_eol(session): with run(dburifromfileconfig, stdin=dburi + b"\n") as process: - response = requests.get(f"{process.baseurl}/") + response = session.get(f"{process.baseurl}/") assert response.status_code == 200 -def test_role_claim_key(roleclaimcase): +def test_role_claim_key(session, roleclaimcase): env = {"ROLE_CLAIM_KEY": roleclaimcase.key} token = jwt.encode(roleclaimcase.data, secret).decode("utf-8") headers = {"Authorization": f"Bearer {token}"} with run(roleclaimkeyconfig, moreenv=env) as process: - response = requests.get(f"{process.baseurl}/authors_only", headers=headers) + response = session.get(f"{process.baseurl}/authors_only", headers=headers) assert response.status_code == roleclaimcase.expected_status @@ -227,30 +237,30 @@ def test_invalid_role_claim_key(invalidroleclaimkey): dumpconfig(roleclaimkeyconfig, moreenv=env) -def test_iat_claim(): +def test_iat_claim(session): claim = {"role": "postgrest_test_author", "iat": datetime.utcnow()} token = jwt.encode(claim, secret).decode("utf-8") headers = {"Authorization": f"Bearer {token}"} with run(basedir / "configs" / "simple.config") as process: for _ in range(10): - response = requests.get(f"{process.baseurl}/authors_only", headers=headers) + response = session.get(f"{process.baseurl}/authors_only", headers=headers) assert response.status_code == 200 time.sleep(0.5) -def test_app_settings(): +def test_app_settings(session): with run(basedir / "configs" / "app-settings.config") as process: url = ( f"{process.baseurl}/rpc/get_guc_value?name=app.settings.external_api_secret" ) - response = requests.get(url) + response = session.get(url) assert response.status_code == 200 assert response.text == '"0123456789abcdef"' -def test_app_settings_reload(tmp_path): +def test_app_settings_reload(session, tmp_path): config = (basedir / "configs" / "sigusr2-settings.config").read_text() configfile = tmp_path / "test.config" configfile.write_text(config) @@ -258,7 +268,7 @@ def test_app_settings_reload(tmp_path): with run(configfile) as process: url = f"{process.baseurl}/rpc/get_guc_value?name=app.settings.name_var" - response = requests.get(url) + response = session.get(url) assert response.status_code == 200 assert response.text == '"John"' @@ -267,12 +277,12 @@ def test_app_settings_reload(tmp_path): # reload process.process.send_signal(signal.SIGUSR2) - response = requests.get(url) + response = session.get(url) assert response.status_code == 200 assert response.text == '"Jane"' -def test_jwt_secret_reload(tmp_path): +def test_jwt_secret_reload(session, tmp_path): config = (basedir / "configs" / "sigusr2-settings.config").read_text() configfile = tmp_path / "test.config" configfile.write_text(config) @@ -284,7 +294,7 @@ def test_jwt_secret_reload(tmp_path): with run(configfile) as process: url = f"{process.baseurl}/authors_only" - response = requests.get(url, headers=headers) + response = session.get(url, headers=headers) assert response.status_code == 401 # change setting @@ -292,11 +302,11 @@ def test_jwt_secret_reload(tmp_path): # reload process.process.send_signal(signal.SIGUSR2) - response = requests.get(url, headers=headers) + response = session.get(url, headers=headers) assert response.status_code == 200 -def test_db_schema_reload(tmp_path): +def test_db_schema_reload(session, tmp_path): config = (basedir / "configs" / "sigusr2-settings.config").read_text() configfile = tmp_path / "test.config" configfile.write_text(config) @@ -306,7 +316,7 @@ def test_db_schema_reload(tmp_path): with run(configfile) as process: url = f"{process.baseurl}/parents" - response = requests.get(url, headers=headers) + response = session.get(url, headers=headers) assert response.status_code == 404 # change setting @@ -317,5 +327,5 @@ def test_db_schema_reload(tmp_path): process.process.send_signal(signal.SIGUSR2) process.process.send_signal(signal.SIGUSR1) - response = requests.get(url, headers=headers) + response = session.get(url, headers=headers) assert response.status_code == 200 From 5c5cafa0e3bcf162aef490cb445c541177025bcd Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 10:00:20 +0100 Subject: [PATCH 22/44] parametrize unix socket --- test/io-tests.sh | 2 ++ test/io-tests/configs/unix-socket.config | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/io-tests.sh b/test/io-tests.sh index 5ff60430e0..277606ef34 100755 --- a/test/io-tests.sh +++ b/test/io-tests.sh @@ -17,6 +17,8 @@ set -eu export POSTGREST_TEST_CONNECTION=${POSTGREST_TEST_CONNECTION:-"postgres:///postgrest_test"} +export POSTGREST_TEST_SOCKET="/tmp/postgrest.sock" + cd "$(dirname "$0")" cd io-tests diff --git a/test/io-tests/configs/unix-socket.config b/test/io-tests/configs/unix-socket.config index 48dfb981d9..4d3c4ac10d 100644 --- a/test/io-tests/configs/unix-socket.config +++ b/test/io-tests/configs/unix-socket.config @@ -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" From faa79e30a560bd2038ce048c2f115a2a4e3f279a Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 10:14:21 +0100 Subject: [PATCH 23/44] add connect through socket test --- test/io-tests/test_io.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index d8fde77638..24d02a0d82 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -9,6 +9,7 @@ import os import signal import time +import urllib.parse import jwt import pytest @@ -113,9 +114,7 @@ def invalidroleclaimkey(request): def dumpconfig(configpath, moreenv=None): "Dump the config as parsed by PostgREST." - env = os.environ - if moreenv: - env = {**env, **moreenv} + env = {**os.environ, **(moreenv or {})} command = ["postgrest", "--dump-config", configpath] result = subprocess.run(command, env=env, capture_output=True, check=True) @@ -123,11 +122,14 @@ def dumpconfig(configpath, moreenv=None): @contextlib.contextmanager -def run(configpath, stdin=None, moreenv=None): +def run(configpath, stdin=None, moreenv=None, socket=None): "Run PostgREST." - env = os.environ - if moreenv: - env = {**env, **moreenv} + 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) @@ -137,8 +139,8 @@ def run(configpath, stdin=None, moreenv=None): process.stdin.write(stdin) process.stdin.close() - waitfor200(BASEURL) - yield PostgrestProcess(baseurl=BASEURL, process=process) + waitfor200(baseurl) + yield PostgrestProcess(baseurl=baseurl, process=process) finally: process.kill() process.wait() @@ -182,7 +184,10 @@ def test_stable_config(tmp_path, configpath): be different because of default values, whitespace, and quoting. """ - env = {"ROLE_CLAIM_KEY": '."https://www.example.com/roles"[0].value'} + env = { + "ROLE_CLAIM_KEY": '."https://www.example.com/roles"[0].value', + "POSTGREST_TEST_SOCKET": "/tmp/postgrest.sock", + } dumped = dumpconfig(configpath, moreenv=env) tmpconfigpath = tmp_path / "config" @@ -192,6 +197,16 @@ def test_stable_config(tmp_path, configpath): assert dumped == redumped +def test_socket_connection(session, tmp_path): + socket = tmp_path / "postgrest.sock" + env = { + "POSTGREST_TEST_SOCKET": str(socket), + } + + with run(basedir / "configs" / "unix-socket.config", socket=socket, moreenv=env): + pass + + def test_read_secret_from_file(session, secretpath): if secretpath.suffix == ".b64": configfile = basedir / "configs" / "base64-secret-from-file.config" From b49292fa46927a83c76fc8f3d8bc8401ba95032f Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 10:48:53 +0100 Subject: [PATCH 24/44] separate data from test code --- nix/tests.nix | 8 ++++- test/io-tests/fixtures.yaml | 23 ++++++++++++++ test/io-tests/test_io.py | 63 ++++++------------------------------- 3 files changed, 40 insertions(+), 54 deletions(-) create mode 100644 test/io-tests/fixtures.yaml diff --git a/nix/tests.nix b/nix/tests.nix index 35857bb26a..1ed727cbd0 100644 --- a/nix/tests.nix +++ b/nix/tests.nix @@ -91,7 +91,13 @@ let ''; ioTestPython = - python3.withPackages (ps: [ ps.pytest ps.requests ps.requests-unixsocket ps.pyjwt ]); + python3.withPackages (ps: [ + ps.pytest + ps.requests + ps.requests-unixsocket + ps.pyjwt + ps.pyyaml + ]); # Provisional name until all io-tests are migrated testIONew = diff --git a/test/io-tests/fixtures.yaml b/test/io-tests/fixtures.yaml new file mode 100644 index 0000000000..b796a09a8e --- /dev/null +++ b/test/io-tests/fixtures.yaml @@ -0,0 +1,23 @@ +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" diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 24d02a0d82..08d7687d2b 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -15,6 +15,7 @@ import pytest import requests import requests_unixsocket +import yaml BASEURL = "http://127.0.0.1:49421" @@ -27,6 +28,7 @@ dburi = os.getenv("POSTGREST_TEST_CONNECTION").encode("utf-8") dburifromfileconfig = basedir / "configs" / "dburi-from-file.config" roleclaimkeyconfig = basedir / "configs" / "role-claim-key.config" +fixtures = yaml.load((basedir / "fixtures.yaml").read_text(), Loader=yaml.Loader) @dataclasses.dataclass @@ -35,43 +37,6 @@ class PostgrestProcess: process: object -@dataclasses.dataclass -class RoleClaimCase: - key: str - data: dict - expected_status: int - - -roleclaimcases = [ - RoleClaimCase( - ".postgrest.a_role", {"postgrest": {"a_role": "postgrest_test_author"}}, 200 - ), - RoleClaimCase( - ".customObject.manyRoles[1]", - {"customObject": {"manyRoles": ["other", "postgrest_test_author"]}}, - 200, - ), - RoleClaimCase( - '."https://www.example.com/roles"[0].value', - {"https://www.example.com/roles": [{"value": "postgrest_test_author"}]}, - 200, - ), - RoleClaimCase( - ".myDomain[3]", {"myDomain": ["other", "postgrest_test_author"]}, 401 - ), - RoleClaimCase(".myRole", {"role": "postgrest_test_author"}, 401), -] - -invalidroleclaimkeys = [ - "role.other", - ".role##", - ".my_role;;domain", - ".#$$%&$%/", - "", - "1234", -] - - class TimeOutException(Exception): pass @@ -100,18 +65,6 @@ def secretpath(request): return request.param -@pytest.fixture(params=roleclaimcases, ids=[case.key for case in roleclaimcases]) -def roleclaimcase(request): - "Fixture for role claim test cases." - return request.param - - -@pytest.fixture(params=invalidroleclaimkeys) -def invalidroleclaimkey(request): - "Fixture for all invalid role claim keys." - return request.param - - def dumpconfig(configpath, moreenv=None): "Dump the config as parsed by PostgREST." env = {**os.environ, **(moreenv or {})} @@ -235,16 +188,20 @@ def test_read_dburi_from_file_with_eol(session): assert response.status_code == 200 -def test_role_claim_key(session, roleclaimcase): - env = {"ROLE_CLAIM_KEY": roleclaimcase.key} - token = jwt.encode(roleclaimcase.data, secret).decode("utf-8") +@pytest.mark.parametrize( + "roleclaim", fixtures["roleclaims"], ids=lambda claim: claim["key"] +) +def test_role_claim_key(session, roleclaim): + env = {"ROLE_CLAIM_KEY": roleclaim["key"]} + token = jwt.encode(roleclaim["data"], secret).decode("utf-8") headers = {"Authorization": f"Bearer {token}"} with run(roleclaimkeyconfig, moreenv=env) as process: response = session.get(f"{process.baseurl}/authors_only", headers=headers) - assert response.status_code == roleclaimcase.expected_status + assert response.status_code == roleclaim["expected_status"] +@pytest.mark.parametrize("invalidroleclaimkey", fixtures["invalidroleclaimkeys"]) def test_invalid_role_claim_key(invalidroleclaimkey): env = {"ROLE_CLAIM_KEY": invalidroleclaimkey} From c642a7e4bfd24371f1ab1f431d7da9ab2c2a1019 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 10:54:37 +0100 Subject: [PATCH 25/44] configsdir --- test/io-tests/test_io.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 08d7687d2b..f442fa5f26 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -22,12 +22,13 @@ secret = "reallyreallyreallyreallyverysafe" basedir = pathlib.Path(os.path.realpath(__file__)).parent -configs = [path for path in (basedir / "configs").iterdir() if path.is_file()] -expectedconfigs = list((basedir / "configs" / "expected").iterdir()) +configsdir = basedir / "configs" +configs = [path for path in configsdir.iterdir() if path.is_file()] +expectedconfigs = list((configsdir / "expected").iterdir()) secrets = [path for path in (basedir / "secrets").iterdir() if path.suffix != ".jwt"] dburi = os.getenv("POSTGREST_TEST_CONNECTION").encode("utf-8") -dburifromfileconfig = basedir / "configs" / "dburi-from-file.config" -roleclaimkeyconfig = basedir / "configs" / "role-claim-key.config" +dburifromfileconfig = configsdir / "dburi-from-file.config" +roleclaimkeyconfig = configsdir / "role-claim-key.config" fixtures = yaml.load((basedir / "fixtures.yaml").read_text(), Loader=yaml.Loader) @@ -50,7 +51,7 @@ def session(): @pytest.fixture(params=configs, ids=[conf.name for conf in configs]) def configpath(request): "Fixture for all config paths." - return basedir / "configs" / request.param + return configsdir / request.param @pytest.fixture(params=expectedconfigs, ids=[conf.name for conf in expectedconfigs]) @@ -125,8 +126,8 @@ def test_expected_config(expectedconfig): 'configs/expected' directory. """ - expected = (basedir / "configs" / "expected" / expectedconfig).read_text() - assert dumpconfig(basedir / "configs" / expectedconfig) == expected + expected = (configsdir / "expected" / expectedconfig).read_text() + assert dumpconfig(configsdir / expectedconfig) == expected def test_stable_config(tmp_path, configpath): @@ -156,15 +157,15 @@ def test_socket_connection(session, tmp_path): "POSTGREST_TEST_SOCKET": str(socket), } - with run(basedir / "configs" / "unix-socket.config", socket=socket, moreenv=env): + with run(configsdir / "unix-socket.config", socket=socket, moreenv=env): pass def test_read_secret_from_file(session, secretpath): if secretpath.suffix == ".b64": - configfile = basedir / "configs" / "base64-secret-from-file.config" + configfile = configsdir / "base64-secret-from-file.config" else: - configfile = basedir / "configs" / "secret-from-file.config" + configfile = configsdir / "secret-from-file.config" secret = secretpath.read_bytes() @@ -214,7 +215,7 @@ def test_iat_claim(session): token = jwt.encode(claim, secret).decode("utf-8") headers = {"Authorization": f"Bearer {token}"} - with run(basedir / "configs" / "simple.config") as process: + with run(configsdir / "simple.config") as process: for _ in range(10): response = session.get(f"{process.baseurl}/authors_only", headers=headers) assert response.status_code == 200 @@ -223,7 +224,7 @@ def test_iat_claim(session): def test_app_settings(session): - with run(basedir / "configs" / "app-settings.config") as process: + with run(configsdir / "app-settings.config") as process: url = ( f"{process.baseurl}/rpc/get_guc_value?name=app.settings.external_api_secret" ) @@ -233,7 +234,7 @@ def test_app_settings(session): def test_app_settings_reload(session, tmp_path): - config = (basedir / "configs" / "sigusr2-settings.config").read_text() + config = (configsdir / "sigusr2-settings.config").read_text() configfile = tmp_path / "test.config" configfile.write_text(config) @@ -255,7 +256,7 @@ def test_app_settings_reload(session, tmp_path): def test_jwt_secret_reload(session, tmp_path): - config = (basedir / "configs" / "sigusr2-settings.config").read_text() + config = (configsdir / "sigusr2-settings.config").read_text() configfile = tmp_path / "test.config" configfile.write_text(config) @@ -279,7 +280,7 @@ def test_jwt_secret_reload(session, tmp_path): def test_db_schema_reload(session, tmp_path): - config = (basedir / "configs" / "sigusr2-settings.config").read_text() + config = (configsdir / "sigusr2-settings.config").read_text() configfile = tmp_path / "test.config" configfile.write_text(config) From 9c24cb242898dace24e0a98173722bf8b24a7293 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 11:09:13 +0100 Subject: [PATCH 26/44] parametrize tests directly --- test/io-tests/test_io.py | 59 ++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index f442fa5f26..955c639c5d 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -23,12 +23,7 @@ secret = "reallyreallyreallyreallyverysafe" basedir = pathlib.Path(os.path.realpath(__file__)).parent configsdir = basedir / "configs" -configs = [path for path in configsdir.iterdir() if path.is_file()] -expectedconfigs = list((configsdir / "expected").iterdir()) -secrets = [path for path in (basedir / "secrets").iterdir() if path.suffix != ".jwt"] -dburi = os.getenv("POSTGREST_TEST_CONNECTION").encode("utf-8") dburifromfileconfig = configsdir / "dburi-from-file.config" -roleclaimkeyconfig = configsdir / "role-claim-key.config" fixtures = yaml.load((basedir / "fixtures.yaml").read_text(), Loader=yaml.Loader) @@ -47,24 +42,9 @@ def session(): "Session for http requests." return requests_unixsocket.Session() - -@pytest.fixture(params=configs, ids=[conf.name for conf in configs]) -def configpath(request): - "Fixture for all config paths." - return configsdir / request.param - - -@pytest.fixture(params=expectedconfigs, ids=[conf.name for conf in expectedconfigs]) -def expectedconfig(request): - "Fixture for all expected configs." - return request.param - - -@pytest.fixture(params=secrets, ids=[secret.name for secret in secrets]) -def secretpath(request): - "Fixture for all secrets." - return request.param - +@pytest.fixture +def dburi(): + return os.getenv("POSTGREST_TEST_CONNECTION").encode("utf-8") def dumpconfig(configpath, moreenv=None): "Dump the config as parsed by PostgREST." @@ -117,6 +97,10 @@ def waitfor200(url): raise TimeOutException() +@pytest.mark.parametrize( + "expectedconfig", (configsdir / "expected").iterdir(), + ids=lambda config: config.name +) def test_expected_config(expectedconfig): """ Configs as dumped by PostgREST should match an expected output. @@ -126,11 +110,15 @@ def test_expected_config(expectedconfig): 'configs/expected' directory. """ - expected = (configsdir / "expected" / expectedconfig).read_text() - assert dumpconfig(configsdir / expectedconfig) == expected + expected = expectedconfig.read_text() + assert dumpconfig(configsdir / expectedconfig.name) == expected -def test_stable_config(tmp_path, configpath): +@pytest.mark.parametrize( + "config", [conf for conf in configsdir.iterdir() if conf.is_file()], + ids=lambda config: config.name +) +def test_stable_config(tmp_path, config): """ A dumped, re-read and re-dumped config should match the dumped config. @@ -142,7 +130,7 @@ def test_stable_config(tmp_path, configpath): "ROLE_CLAIM_KEY": '."https://www.example.com/roles"[0].value', "POSTGREST_TEST_SOCKET": "/tmp/postgrest.sock", } - dumped = dumpconfig(configpath, moreenv=env) + dumped = dumpconfig(config, moreenv=env) tmpconfigpath = tmp_path / "config" tmpconfigpath.write_text(dumped) @@ -161,6 +149,11 @@ def test_socket_connection(session, tmp_path): pass +@pytest.mark.parametrize( + "secretpath", + [path for path in (basedir / "secrets").iterdir() if path.suffix != ".jwt"], + ids=lambda secret: secret.name +) def test_read_secret_from_file(session, secretpath): if secretpath.suffix == ".b64": configfile = configsdir / "base64-secret-from-file.config" @@ -177,14 +170,14 @@ def test_read_secret_from_file(session, secretpath): assert response.status_code == 200 -def test_read_dburi_from_file_without_eol(session): - with run(dburifromfileconfig, stdin=dburi) as process: +def test_read_dburi_from_file_without_eol(session, dburi): + with run(configsdir / "dburi-from-file.config", stdin=dburi) as process: response = session.get(f"{process.baseurl}/") assert response.status_code == 200 -def test_read_dburi_from_file_with_eol(session): - with run(dburifromfileconfig, stdin=dburi + b"\n") as process: +def test_read_dburi_from_file_with_eol(session, dburi): + with run(configsdir / "dburi-from-file.config", stdin=dburi + b"\n") as process: response = session.get(f"{process.baseurl}/") assert response.status_code == 200 @@ -197,7 +190,7 @@ def test_role_claim_key(session, roleclaim): token = jwt.encode(roleclaim["data"], secret).decode("utf-8") headers = {"Authorization": f"Bearer {token}"} - with run(roleclaimkeyconfig, moreenv=env) as process: + with run(configsdir / "role-claim-key.config", moreenv=env) as process: response = session.get(f"{process.baseurl}/authors_only", headers=headers) assert response.status_code == roleclaim["expected_status"] @@ -207,7 +200,7 @@ def test_invalid_role_claim_key(invalidroleclaimkey): env = {"ROLE_CLAIM_KEY": invalidroleclaimkey} with pytest.raises(subprocess.CalledProcessError): - dumpconfig(roleclaimkeyconfig, moreenv=env) + dumpconfig(configsdir / "role-claim-key.config", moreenv=env) def test_iat_claim(session): From 4981a1350b5bf764b7994f37ae3540d99fdc7c20 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 11:13:57 +0100 Subject: [PATCH 27/44] properly mark globals --- test/io-tests/test_io.py | 63 +++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 955c639c5d..d2802ef4a5 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -20,11 +20,10 @@ BASEURL = "http://127.0.0.1:49421" -secret = "reallyreallyreallyreallyverysafe" -basedir = pathlib.Path(os.path.realpath(__file__)).parent -configsdir = basedir / "configs" -dburifromfileconfig = configsdir / "dburi-from-file.config" -fixtures = yaml.load((basedir / "fixtures.yaml").read_text(), Loader=yaml.Loader) +SECRET = "reallyreallyreallyreallyverysafe" +BASEDIR = pathlib.Path(os.path.realpath(__file__)).parent +CONFIGSDIR = BASEDIR / "configs" +FIXTURES = yaml.load((BASEDIR / "fixtures.yaml").read_text(), Loader=yaml.Loader) @dataclasses.dataclass @@ -42,10 +41,12 @@ def session(): "Session for http requests." return requests_unixsocket.Session() + @pytest.fixture def dburi(): return os.getenv("POSTGREST_TEST_CONNECTION").encode("utf-8") + def dumpconfig(configpath, moreenv=None): "Dump the config as parsed by PostgREST." env = {**os.environ, **(moreenv or {})} @@ -98,8 +99,9 @@ def waitfor200(url): @pytest.mark.parametrize( - "expectedconfig", (configsdir / "expected").iterdir(), - ids=lambda config: config.name + "expectedconfig", + (CONFIGSDIR / "expected").iterdir(), + ids=lambda config: config.name, ) def test_expected_config(expectedconfig): """ @@ -111,12 +113,13 @@ def test_expected_config(expectedconfig): """ expected = expectedconfig.read_text() - assert dumpconfig(configsdir / expectedconfig.name) == expected + assert dumpconfig(CONFIGSDIR / expectedconfig.name) == expected @pytest.mark.parametrize( - "config", [conf for conf in configsdir.iterdir() if conf.is_file()], - ids=lambda config: config.name + "config", + [conf for conf in CONFIGSDIR.iterdir() if conf.is_file()], + ids=lambda config: config.name, ) def test_stable_config(tmp_path, config): """ @@ -145,20 +148,20 @@ def test_socket_connection(session, tmp_path): "POSTGREST_TEST_SOCKET": str(socket), } - with run(configsdir / "unix-socket.config", socket=socket, moreenv=env): + 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=lambda secret: secret.name + [path for path in (BASEDIR / "secrets").iterdir() if path.suffix != ".jwt"], + ids=lambda secret: secret.name, ) def test_read_secret_from_file(session, secretpath): if secretpath.suffix == ".b64": - configfile = configsdir / "base64-secret-from-file.config" + configfile = CONFIGSDIR / "base64-secret-from-file.config" else: - configfile = configsdir / "secret-from-file.config" + configfile = CONFIGSDIR / "secret-from-file.config" secret = secretpath.read_bytes() @@ -171,44 +174,44 @@ def test_read_secret_from_file(session, secretpath): def test_read_dburi_from_file_without_eol(session, dburi): - with run(configsdir / "dburi-from-file.config", stdin=dburi) as process: + with run(CONFIGSDIR / "dburi-from-file.config", stdin=dburi) as process: response = session.get(f"{process.baseurl}/") assert response.status_code == 200 def test_read_dburi_from_file_with_eol(session, dburi): - with run(configsdir / "dburi-from-file.config", stdin=dburi + b"\n") as process: + with run(CONFIGSDIR / "dburi-from-file.config", stdin=dburi + b"\n") as process: response = session.get(f"{process.baseurl}/") assert response.status_code == 200 @pytest.mark.parametrize( - "roleclaim", fixtures["roleclaims"], ids=lambda claim: claim["key"] + "roleclaim", FIXTURES["roleclaims"], ids=lambda claim: claim["key"] ) def test_role_claim_key(session, roleclaim): env = {"ROLE_CLAIM_KEY": roleclaim["key"]} - token = jwt.encode(roleclaim["data"], secret).decode("utf-8") + token = jwt.encode(roleclaim["data"], SECRET).decode("utf-8") headers = {"Authorization": f"Bearer {token}"} - with run(configsdir / "role-claim-key.config", moreenv=env) as process: + with run(CONFIGSDIR / "role-claim-key.config", moreenv=env) as process: response = session.get(f"{process.baseurl}/authors_only", headers=headers) assert response.status_code == roleclaim["expected_status"] -@pytest.mark.parametrize("invalidroleclaimkey", fixtures["invalidroleclaimkeys"]) +@pytest.mark.parametrize("invalidroleclaimkey", FIXTURES["invalidroleclaimkeys"]) def test_invalid_role_claim_key(invalidroleclaimkey): env = {"ROLE_CLAIM_KEY": invalidroleclaimkey} with pytest.raises(subprocess.CalledProcessError): - dumpconfig(configsdir / "role-claim-key.config", moreenv=env) + dumpconfig(CONFIGSDIR / "role-claim-key.config", moreenv=env) def test_iat_claim(session): claim = {"role": "postgrest_test_author", "iat": datetime.utcnow()} - token = jwt.encode(claim, secret).decode("utf-8") + token = jwt.encode(claim, SECRET).decode("utf-8") headers = {"Authorization": f"Bearer {token}"} - with run(configsdir / "simple.config") as process: + with run(CONFIGSDIR / "simple.config") as process: for _ in range(10): response = session.get(f"{process.baseurl}/authors_only", headers=headers) assert response.status_code == 200 @@ -217,7 +220,7 @@ def test_iat_claim(session): def test_app_settings(session): - with run(configsdir / "app-settings.config") as process: + with run(CONFIGSDIR / "app-settings.config") as process: url = ( f"{process.baseurl}/rpc/get_guc_value?name=app.settings.external_api_secret" ) @@ -227,7 +230,7 @@ def test_app_settings(session): def test_app_settings_reload(session, tmp_path): - config = (configsdir / "sigusr2-settings.config").read_text() + config = (CONFIGSDIR / "sigusr2-settings.config").read_text() configfile = tmp_path / "test.config" configfile.write_text(config) @@ -249,12 +252,12 @@ def test_app_settings_reload(session, tmp_path): def test_jwt_secret_reload(session, tmp_path): - config = (configsdir / "sigusr2-settings.config").read_text() + config = (CONFIGSDIR / "sigusr2-settings.config").read_text() configfile = tmp_path / "test.config" configfile.write_text(config) claim = {"role": "postgrest_test_author"} - token = jwt.encode(claim, secret).decode("utf-8") + token = jwt.encode(claim, SECRET).decode("utf-8") headers = {"Authorization": f"Bearer {token}"} with run(configfile) as process: @@ -264,7 +267,7 @@ def test_jwt_secret_reload(session, tmp_path): assert response.status_code == 401 # change setting - configfile.write_text(config.replace("invalid" * 5, secret)) + configfile.write_text(config.replace("invalid" * 5, SECRET)) # reload process.process.send_signal(signal.SIGUSR2) @@ -273,7 +276,7 @@ def test_jwt_secret_reload(session, tmp_path): def test_db_schema_reload(session, tmp_path): - config = (configsdir / "sigusr2-settings.config").read_text() + config = (CONFIGSDIR / "sigusr2-settings.config").read_text() configfile = tmp_path / "test.config" configfile.write_text(config) From 69945e79af87c061daa29d5065e42d75a2b1d2fb Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 11:30:40 +0100 Subject: [PATCH 28/44] refactor jwts and auth headers --- test/io-tests/test_io.py | 43 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index d2802ef4a5..8a2a30f9ae 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -6,6 +6,7 @@ import pathlib import subprocess import tempfile +from operator import attrgetter import os import signal import time @@ -19,7 +20,6 @@ BASEURL = "http://127.0.0.1:49421" - SECRET = "reallyreallyreallyreallyverysafe" BASEDIR = pathlib.Path(os.path.realpath(__file__)).parent CONFIGSDIR = BASEDIR / "configs" @@ -99,9 +99,7 @@ def waitfor200(url): @pytest.mark.parametrize( - "expectedconfig", - (CONFIGSDIR / "expected").iterdir(), - ids=lambda config: config.name, + "expectedconfig", (CONFIGSDIR / "expected").iterdir(), ids=attrgetter("name") ) def test_expected_config(expectedconfig): """ @@ -119,7 +117,7 @@ def test_expected_config(expectedconfig): @pytest.mark.parametrize( "config", [conf for conf in CONFIGSDIR.iterdir() if conf.is_file()], - ids=lambda config: config.name, + ids=attrgetter("name"), ) def test_stable_config(tmp_path, config): """ @@ -155,7 +153,7 @@ def test_socket_connection(session, tmp_path): @pytest.mark.parametrize( "secretpath", [path for path in (BASEDIR / "secrets").iterdir() if path.suffix != ".jwt"], - ids=lambda secret: secret.name, + ids=attrgetter("name"), ) def test_read_secret_from_file(session, secretpath): if secretpath.suffix == ".b64": @@ -164,9 +162,7 @@ def test_read_secret_from_file(session, secretpath): configfile = CONFIGSDIR / "secret-from-file.config" secret = secretpath.read_bytes() - - jwt = secretpath.with_suffix(".jwt").read_text() - headers = {"Authorization": f"Bearer {jwt}"} + headers = authheader(secretpath.with_suffix(".jwt").read_text()) with run(configfile, stdin=secret) as process: response = session.get(f"{process.baseurl}/authors_only", headers=headers) @@ -175,14 +171,12 @@ def test_read_secret_from_file(session, secretpath): def test_read_dburi_from_file_without_eol(session, dburi): with run(CONFIGSDIR / "dburi-from-file.config", stdin=dburi) as process: - response = session.get(f"{process.baseurl}/") - assert response.status_code == 200 + pass def test_read_dburi_from_file_with_eol(session, dburi): with run(CONFIGSDIR / "dburi-from-file.config", stdin=dburi + b"\n") as process: - response = session.get(f"{process.baseurl}/") - assert response.status_code == 200 + pass @pytest.mark.parametrize( @@ -190,8 +184,7 @@ def test_read_dburi_from_file_with_eol(session, dburi): ) def test_role_claim_key(session, roleclaim): env = {"ROLE_CLAIM_KEY": roleclaim["key"]} - token = jwt.encode(roleclaim["data"], SECRET).decode("utf-8") - headers = {"Authorization": f"Bearer {token}"} + headers = jwtauthheader(roleclaim["data"], SECRET) with run(CONFIGSDIR / "role-claim-key.config", moreenv=env) as process: response = session.get(f"{process.baseurl}/authors_only", headers=headers) @@ -206,14 +199,24 @@ def test_invalid_role_claim_key(invalidroleclaimkey): dumpconfig(CONFIGSDIR / "role-claim-key.config", moreenv=env) +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")) + + def test_iat_claim(session): claim = {"role": "postgrest_test_author", "iat": datetime.utcnow()} - token = jwt.encode(claim, SECRET).decode("utf-8") - headers = {"Authorization": f"Bearer {token}"} + headers = jwtauthheader(claim, SECRET) with run(CONFIGSDIR / "simple.config") as process: for _ in range(10): - response = session.get(f"{process.baseurl}/authors_only", headers=headers) + url = f"{process.baseurl}/authors_only" + response = session.get(url, headers=headers) assert response.status_code == 200 time.sleep(0.5) @@ -256,9 +259,7 @@ def test_jwt_secret_reload(session, tmp_path): configfile = tmp_path / "test.config" configfile.write_text(config) - claim = {"role": "postgrest_test_author"} - token = jwt.encode(claim, SECRET).decode("utf-8") - headers = {"Authorization": f"Bearer {token}"} + headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET) with run(configfile) as process: url = f"{process.baseurl}/authors_only" From 6532dcc5186382cc60fe5049f82a68a787a5c3d5 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 12:34:31 +0100 Subject: [PATCH 29/44] cleanup and add docs --- test/io-tests/test_io.py | 158 +++++++++++++++++++++++++-------------- 1 file changed, 101 insertions(+), 57 deletions(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 8a2a30f9ae..e4848f18a7 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -1,11 +1,10 @@ -"Tests for inputs and outputs of PostgREST." +"Unit tests for Input/Ouput of PostgREST seen as a black box." import contextlib import dataclasses from datetime import datetime import pathlib import subprocess -import tempfile from operator import attrgetter import os import signal @@ -19,46 +18,59 @@ import yaml -BASEURL = "http://127.0.0.1:49421" -SECRET = "reallyreallyreallyreallyverysafe" 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." @dataclasses.dataclass class PostgrestProcess: + "Running PostgREST process and its corresponding endpoint." baseurl: str process: object -class TimeOutException(Exception): - pass - - @pytest.fixture def session(): - "Session for http requests." + "Session for HTTP requests that supports connecting to unix domain sockets." return requests_unixsocket.Session() @pytest.fixture def dburi(): + "Postgres database connection URI." return os.getenv("POSTGREST_TEST_CONNECTION").encode("utf-8") -def dumpconfig(configpath, moreenv=None): +def dumpconfig(configpath, moreenv=None, stdin=None): "Dump the config as parsed by PostgREST." env = {**os.environ, **(moreenv or {})} - command = ["postgrest", "--dump-config", configpath] - result = subprocess.run(command, env=env, capture_output=True, check=True) - return result.stdout.decode("utf-8") + 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." + "Run PostgREST and yield an endpoint that is ready for connections." env = {**os.environ, **(moreenv or {})} if socket: @@ -70,21 +82,22 @@ def run(configpath, stdin=None, moreenv=None, socket=None): process = subprocess.Popen(command, stdin=subprocess.PIPE, env=env) try: - if stdin: - process.stdin.write(stdin) + process.stdin.write(stdin or b"") process.stdin.close() - waitfor200(baseurl) + wait_until_ready(baseurl) + yield PostgrestProcess(baseurl=baseurl, process=process) finally: process.kill() process.wait() -def waitfor200(url): +def wait_until_ready(url): + "Wait for the given HTTP endpoint to return a status of 200." session = requests_unixsocket.Session() - for i in range(10): + for _ in range(10): try: response = session.get(url, timeout=0.1) @@ -95,7 +108,17 @@ def waitfor200(url): time.sleep(0.1) - raise TimeOutException() + 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( @@ -127,11 +150,18 @@ def test_stable_config(tmp_path, config): 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", } - dumped = dumpconfig(config, moreenv=env) + + # 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) @@ -140,7 +170,8 @@ def test_stable_config(tmp_path, config): assert dumped == redumped -def test_socket_connection(session, tmp_path): +def test_socket_connection(tmp_path): + "Connections via unix domain sockets should work." socket = tmp_path / "postgrest.sock" env = { "POSTGREST_TEST_SOCKET": str(socket), @@ -156,6 +187,7 @@ def test_socket_connection(session, tmp_path): ids=attrgetter("name"), ) def test_read_secret_from_file(session, secretpath): + "Authorization should succeed when the secret is read from a file." if secretpath.suffix == ".b64": configfile = CONFIGSDIR / "base64-secret-from-file.config" else: @@ -164,18 +196,20 @@ def test_read_secret_from_file(session, secretpath): secret = secretpath.read_bytes() headers = authheader(secretpath.with_suffix(".jwt").read_text()) - with run(configfile, stdin=secret) as process: - response = session.get(f"{process.baseurl}/authors_only", headers=headers) + with run(configfile, stdin=secret) as postgrest: + response = session.get(f"{postgrest.baseurl}/authors_only", headers=headers) assert response.status_code == 200 -def test_read_dburi_from_file_without_eol(session, dburi): - with run(CONFIGSDIR / "dburi-from-file.config", stdin=dburi) as process: +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(session, dburi): - with run(CONFIGSDIR / "dburi-from-file.config", stdin=dburi + b"\n") as process: +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 @@ -183,39 +217,38 @@ def test_read_dburi_from_file_with_eol(session, dburi): "roleclaim", FIXTURES["roleclaims"], ids=lambda claim: claim["key"] ) def test_role_claim_key(session, 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 process: - response = session.get(f"{process.baseurl}/authors_only", headers=headers) + with run(CONFIGSDIR / "role-claim-key.config", moreenv=env) as postgrest: + response = session.get(f"{postgrest.baseurl}/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(subprocess.CalledProcessError): + with pytest.raises(PostgrestError): dumpconfig(CONFIGSDIR / "role-claim-key.config", moreenv=env) -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")) +def test_iat_claim(session): + """ + A claim with an 'iat' (issued at) attribute should be successful. + The PostgREST time cache lead to issues here, see: + https://github.com/PostgREST/postgrest/issues/1139 -def test_iat_claim(session): + """ claim = {"role": "postgrest_test_author", "iat": datetime.utcnow()} headers = jwtauthheader(claim, SECRET) - with run(CONFIGSDIR / "simple.config") as process: + with run(CONFIGSDIR / "simple.config") as postgrest: for _ in range(10): - url = f"{process.baseurl}/authors_only" + url = f"{postgrest.baseurl}/authors_only" response = session.get(url, headers=headers) assert response.status_code == 200 @@ -223,22 +256,31 @@ def test_iat_claim(session): def test_app_settings(session): - with run(CONFIGSDIR / "app-settings.config") as process: - url = ( - f"{process.baseurl}/rpc/get_guc_value?name=app.settings.external_api_secret" - ) - response = session.get(url) + """ + 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 = session.get(postgrest.baseurl + uri) + assert response.status_code == 200 assert response.text == '"0123456789abcdef"' def test_app_settings_reload(session, 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) - with run(configfile) as process: - url = f"{process.baseurl}/rpc/get_guc_value?name=app.settings.name_var" + with run(configfile) as postgrest: + url = f"{postgrest.baseurl}/rpc/get_guc_value?name=app.settings.name_var" response = session.get(url) assert response.status_code == 200 @@ -247,7 +289,7 @@ def test_app_settings_reload(session, tmp_path): # change setting configfile.write_text(config.replace("John", "Jane")) # reload - process.process.send_signal(signal.SIGUSR2) + postgrest.process.send_signal(signal.SIGUSR2) response = session.get(url) assert response.status_code == 200 @@ -255,14 +297,15 @@ def test_app_settings_reload(session, tmp_path): def test_jwt_secret_reload(session, 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 process: - url = f"{process.baseurl}/authors_only" + with run(configfile) as postgrest: + url = f"{postgrest.baseurl}/authors_only" response = session.get(url, headers=headers) assert response.status_code == 401 @@ -270,21 +313,22 @@ def test_jwt_secret_reload(session, tmp_path): # change setting configfile.write_text(config.replace("invalid" * 5, SECRET)) # reload - process.process.send_signal(signal.SIGUSR2) + postgrest.process.send_signal(signal.SIGUSR2) response = session.get(url, headers=headers) assert response.status_code == 200 def test_db_schema_reload(session, 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 process: - url = f"{process.baseurl}/parents" + with run(configfile) as postgrest: + url = f"{postgrest.baseurl}/parents" response = session.get(url, headers=headers) assert response.status_code == 404 @@ -294,8 +338,8 @@ def test_db_schema_reload(session, tmp_path): config.replace('db-schema = "test"', 'db-schema = "test, v1"') ) # reload - process.process.send_signal(signal.SIGUSR2) - process.process.send_signal(signal.SIGUSR1) + postgrest.process.send_signal(signal.SIGUSR2) + postgrest.process.send_signal(signal.SIGUSR1) response = session.get(url, headers=headers) assert response.status_code == 200 From 3612abf8a6d0839478725a8b2a45fa295db5e442 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 12:41:24 +0100 Subject: [PATCH 30/44] print dumped config for wrongly accepted role key --- test/io-tests/fixtures.yaml | 1 + test/io-tests/test_io.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/io-tests/fixtures.yaml b/test/io-tests/fixtures.yaml index b796a09a8e..6991143de3 100644 --- a/test/io-tests/fixtures.yaml +++ b/test/io-tests/fixtures.yaml @@ -21,3 +21,4 @@ invalidroleclaimkeys: - ".#$$%&$%/" - "" - "1234" + - ".role" diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index e4848f18a7..c47c0c43ff 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -232,7 +232,7 @@ def test_invalid_role_claim_key(invalidroleclaimkey): env = {"ROLE_CLAIM_KEY": invalidroleclaimkey} with pytest.raises(PostgrestError): - dumpconfig(CONFIGSDIR / "role-claim-key.config", moreenv=env) + print(dumpconfig(CONFIGSDIR / "role-claim-key.config", moreenv=env)) def test_iat_claim(session): From 9d0867bb19d9a667184960201a795c30ba413192 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 12:46:33 +0100 Subject: [PATCH 31/44] filter output role invalid role claim keys --- test/io-tests/fixtures.yaml | 1 - test/io-tests/test_io.py | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/io-tests/fixtures.yaml b/test/io-tests/fixtures.yaml index 6991143de3..b796a09a8e 100644 --- a/test/io-tests/fixtures.yaml +++ b/test/io-tests/fixtures.yaml @@ -21,4 +21,3 @@ invalidroleclaimkeys: - ".#$$%&$%/" - "" - "1234" - - ".role" diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index c47c0c43ff..99b3a29ed5 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -232,7 +232,10 @@ def test_invalid_role_claim_key(invalidroleclaimkey): env = {"ROLE_CLAIM_KEY": invalidroleclaimkey} with pytest.raises(PostgrestError): - print(dumpconfig(CONFIGSDIR / "role-claim-key.config", moreenv=env)) + dump = dumpconfig(CONFIGSDIR / "role-claim-key.config", moreenv=env) + for line in dump.split("\n"): + if "role-claim-key" in line: + print(line) def test_iat_claim(session): From ae85b7488e95fb89ddf717669e5c1ec4f9f4c18a Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 12:59:22 +0100 Subject: [PATCH 32/44] fix db-schemas change --- test/io-tests/test_io.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 99b3a29ed5..de88612bf4 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -338,11 +338,13 @@ def test_db_schema_reload(session, tmp_path): # change setting configfile.write_text( - config.replace('db-schema = "test"', 'db-schema = "test, v1"') + config.replace('db-schemas = "test"', 'db-schemas = "test, v1"') ) # reload postgrest.process.send_signal(signal.SIGUSR2) postgrest.process.send_signal(signal.SIGUSR1) + time.sleep(.1) + response = session.get(url, headers=headers) assert response.status_code == 200 From ece3d97229ab728daa0363d261980c952edd362f Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 13:00:11 +0100 Subject: [PATCH 33/44] separate io tests in circleci --- .circleci/config.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index eb3ae183a8..80f2200c3a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -223,9 +223,11 @@ jobs: when: always - run: name: Run io tests - command: | - postgrest-test-io - postgrest-test-io-new + command: postgrest-test-io + when: always + - run: + name: Run new io tests + command: postgrest-test-io-new when: always - run: name: Run memory tests From 7cba071cdadcc9fdf8ea483213e4c2bcb2f75aa0 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 13:00:48 +0100 Subject: [PATCH 34/44] remove unneeded fix --- test/io-tests/test_io.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index de88612bf4..38ac816049 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -344,7 +344,5 @@ def test_db_schema_reload(session, tmp_path): postgrest.process.send_signal(signal.SIGUSR2) postgrest.process.send_signal(signal.SIGUSR1) - time.sleep(.1) - response = session.get(url, headers=headers) assert response.status_code == 200 From 4c6d2342d3c86027cd42fae3290dd7027cfa05e6 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 13:14:40 +0100 Subject: [PATCH 35/44] remove old io-tests --- .circleci/config.yml | 4 - nix/tests.nix | 19 +- test/io-tests.sh | 429 ------------------------------------------- 3 files changed, 1 insertion(+), 451 deletions(-) delete mode 100755 test/io-tests.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 80f2200c3a..f09cb966cb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -225,10 +225,6 @@ jobs: name: Run io tests command: postgrest-test-io when: always - - run: - name: Run new io tests - command: postgrest-test-io-new - when: always - run: name: Run memory tests command: postgrest-test-memory diff --git a/nix/tests.nix b/nix/tests.nix index 1ed727cbd0..7eb23d058e 100644 --- a/nix/tests.nix +++ b/nix/tests.nix @@ -75,21 +75,6 @@ let checkedShellScript "postgrest-test-spec-all" (lib.concatStringsSep "\n" testRunners); - testIO = - name: postgresql: - checkedShellScript - name - '' - env="$(cat ${postgrest.env})" - export PATH="$env/bin:${curl}/bin:${procps}/bin:${diffutils}/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 - ''; - ioTestPython = python3.withPackages (ps: [ ps.pytest @@ -99,8 +84,7 @@ let ps.pyyaml ]); - # Provisional name until all io-tests are migrated - testIONew = + testIO = name: postgresql: checkedShellScript name @@ -141,7 +125,6 @@ buildEnv (testSpec "postgrest-test-spec" postgresql).bin testSpecAllVersions.bin (testIO "postgrest-test-io" postgresql).bin - (testIONew "postgrest-test-io-new" postgresql).bin ] ++ testSpecVersions; } # The memory tests have large dependencies (a profiled build of PostgREST) diff --git a/test/io-tests.sh b/test/io-tests.sh deleted file mode 100755 index 277606ef34..0000000000 --- a/test/io-tests.sh +++ /dev/null @@ -1,429 +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"} -export POSTGREST_TEST_SOCKET="/tmp/postgrest.sock" - - -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 - -# 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 From f29b867df4ad82c9798f7ca2375db39102cdf124 Mon Sep 17 00:00:00 2001 From: Remo Rechkemmer <59358383+monacoremo@users.noreply.github.com> Date: Sun, 13 Dec 2020 14:15:49 +0100 Subject: [PATCH 36/44] travis shot in the dark --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3ebaf5fbb9..0086c6d505 100644 --- a/.travis.yml +++ b/.travis.yml @@ -87,6 +87,8 @@ jobs: update: true packages: - postgresql-12 + - python3 + - python3-pip cache: yarn: true timeout: 1000 @@ -107,11 +109,12 @@ 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) From 9b4eee73d89774a2b8f7e448e84154a334ac711c Mon Sep 17 00:00:00 2001 From: Remo Rechkemmer <59358383+monacoremo@users.noreply.github.com> Date: Sun, 13 Dec 2020 15:02:31 +0100 Subject: [PATCH 37/44] Update test/io-tests/test_io.py Co-authored-by: Wolfgang Walther --- test/io-tests/test_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 38ac816049..897585fbd9 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -234,7 +234,7 @@ def test_invalid_role_claim_key(invalidroleclaimkey): with pytest.raises(PostgrestError): dump = dumpconfig(CONFIGSDIR / "role-claim-key.config", moreenv=env) for line in dump.split("\n"): - if "role-claim-key" in line: + if line.startswith("jwt-role-claim-key"): print(line) From 9e1286a274a5fd887849769e9452981599ea5c44 Mon Sep 17 00:00:00 2001 From: Remo Rechkemmer <59358383+monacoremo@users.noreply.github.com> Date: Sun, 13 Dec 2020 15:02:49 +0100 Subject: [PATCH 38/44] Update test/io-tests/test_io.py Co-authored-by: Wolfgang Walther --- test/io-tests/test_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 897585fbd9..6e0adce246 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -242,7 +242,7 @@ def test_iat_claim(session): """ A claim with an 'iat' (issued at) attribute should be successful. - The PostgREST time cache lead to issues here, see: + The PostgREST time cache leads to issues here, see: https://github.com/PostgREST/postgrest/issues/1139 """ From ed0bbd97fe1ae7f3886730dd1cdbbbdf90d33a25 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 15:12:20 +0100 Subject: [PATCH 39/44] fixture as proper yaml and with single quotes --- test/io-tests/fixtures.yaml | 42 ++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/test/io-tests/fixtures.yaml b/test/io-tests/fixtures.yaml index b796a09a8e..6e3a0f4951 100644 --- a/test/io-tests/fixtures.yaml +++ b/test/io-tests/fixtures.yaml @@ -1,23 +1,35 @@ roleclaims: - - key: ".postgrest.a_role" - data: {"postgrest": {"a_role": "postgrest_test_author"}} + - 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"]}} + - 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"}]} + data: + 'https://www.example.com/roles': + - value: postgrest_test_author expected_status: 200 - - key: ".myDomain[3]" - data: {"myDomain": ["other", "postgrest_test_author"]} + - key: '.myDomain[3]' + data: + myDomain: + - other + - postgrest_test_author expected_status: 401 - - key: ".myRole" - data: {"role": "postgrest_test_author"} + - key: '.myRole' + data: + role: postgrest_test_author expected_status: 401 invalidroleclaimkeys: - - "role.other" - - ".role##" - - ".my_role;;domain" - - ".#$$%&$%/" - - "" - - "1234" + - 'role.other' + - '.role##' + - '.my_role;;domain' + - '.#$$%&$%/' + - '' + - '1234' From 3453301d61ab9e6e4c720e54349440e6c8fc79c9 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 15:13:45 +0100 Subject: [PATCH 40/44] filter config files --- test/io-tests/test_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 6e0adce246..5dc5f16e1f 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -139,7 +139,7 @@ def test_expected_config(expectedconfig): @pytest.mark.parametrize( "config", - [conf for conf in CONFIGSDIR.iterdir() if conf.is_file()], + [conf for conf in CONFIGSDIR.iterdir() if conf.suffix == ".config"], ids=attrgetter("name"), ) def test_stable_config(tmp_path, config): From 95645d17a01b64ed91ceaab40bba8f14ad8f9065 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 15:16:14 +0100 Subject: [PATCH 41/44] add comment on SIGUSR1 --- test/io-tests/test_io.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 5dc5f16e1f..66e3113e58 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -340,8 +340,11 @@ def test_db_schema_reload(session, tmp_path): configfile.write_text( config.replace('db-schemas = "test"', 'db-schemas = "test, v1"') ) - # reload + + # 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) response = session.get(url, headers=headers) From 7a99df7807a8d52540cc5f79218419b5f579c2d1 Mon Sep 17 00:00:00 2001 From: monacoremo Date: Sun, 13 Dec 2020 15:34:52 +0100 Subject: [PATCH 42/44] add and use Postgrest session object --- test/io-tests/test_io.py | 73 ++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index 66e3113e58..2628c479b9 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -33,17 +33,23 @@ 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." - baseurl: str process: object - - -@pytest.fixture -def session(): - "Session for HTTP requests that supports connecting to unix domain sockets." - return requests_unixsocket.Session() + session: object @pytest.fixture @@ -87,7 +93,7 @@ def run(configpath, stdin=None, moreenv=None, socket=None): wait_until_ready(baseurl) - yield PostgrestProcess(baseurl=baseurl, process=process) + yield PostgrestProcess(process=process, session=PostgrestSession(baseurl)) finally: process.kill() process.wait() @@ -186,7 +192,7 @@ def test_socket_connection(tmp_path): [path for path in (BASEDIR / "secrets").iterdir() if path.suffix != ".jwt"], ids=attrgetter("name"), ) -def test_read_secret_from_file(session, secretpath): +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" @@ -197,7 +203,7 @@ def test_read_secret_from_file(session, secretpath): headers = authheader(secretpath.with_suffix(".jwt").read_text()) with run(configfile, stdin=secret) as postgrest: - response = session.get(f"{postgrest.baseurl}/authors_only", headers=headers) + response = postgrest.session.get("/authors_only", headers=headers) assert response.status_code == 200 @@ -216,13 +222,13 @@ def test_read_dburi_from_file_with_eol(dburi): @pytest.mark.parametrize( "roleclaim", FIXTURES["roleclaims"], ids=lambda claim: claim["key"] ) -def test_role_claim_key(session, roleclaim): +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 = session.get(f"{postgrest.baseurl}/authors_only", headers=headers) + response = postgrest.session.get("/authors_only", headers=headers) assert response.status_code == roleclaim["expected_status"] @@ -238,7 +244,7 @@ def test_invalid_role_claim_key(invalidroleclaimkey): print(line) -def test_iat_claim(session): +def test_iat_claim(): """ A claim with an 'iat' (issued at) attribute should be successful. @@ -251,14 +257,13 @@ def test_iat_claim(session): with run(CONFIGSDIR / "simple.config") as postgrest: for _ in range(10): - url = f"{postgrest.baseurl}/authors_only" - response = session.get(url, headers=headers) + response = postgrest.session.get("/authors_only", headers=headers) assert response.status_code == 200 time.sleep(0.5) -def test_app_settings(session): +def test_app_settings(): """ App settings should not reset when the db pool times out. @@ -270,22 +275,21 @@ def test_app_settings(session): time.sleep(2) uri = "/rpc/get_guc_value?name=app.settings.external_api_secret" - response = session.get(postgrest.baseurl + uri) + response = postgrest.session.get(uri) assert response.status_code == 200 assert response.text == '"0123456789abcdef"' -def test_app_settings_reload(session, tmp_path): +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: - url = f"{postgrest.baseurl}/rpc/get_guc_value?name=app.settings.name_var" - - response = session.get(url) + response = postgrest.session.get(uri) assert response.status_code == 200 assert response.text == '"John"' @@ -294,12 +298,14 @@ def test_app_settings_reload(session, tmp_path): # reload postgrest.process.send_signal(signal.SIGUSR2) - response = session.get(url) + time.sleep(0.1) + + response = postgrest.session.get(uri) assert response.status_code == 200 assert response.text == '"Jane"' -def test_jwt_secret_reload(session, tmp_path): +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" @@ -308,21 +314,22 @@ def test_jwt_secret_reload(session, tmp_path): headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET) with run(configfile) as postgrest: - url = f"{postgrest.baseurl}/authors_only" - - response = session.get(url, headers=headers) + response = postgrest.session.get("/authors_only", headers=headers) assert response.status_code == 401 # change setting configfile.write_text(config.replace("invalid" * 5, SECRET)) - # reload + + # reload config postgrest.process.send_signal(signal.SIGUSR2) - response = session.get(url, headers=headers) + time.sleep(0.1) + + response = postgrest.session.get("/authors_only", headers=headers) assert response.status_code == 200 -def test_db_schema_reload(session, tmp_path): +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" @@ -331,9 +338,7 @@ def test_db_schema_reload(session, tmp_path): headers = {"Accept-Profile": "v1"} with run(configfile) as postgrest: - url = f"{postgrest.baseurl}/parents" - - response = session.get(url, headers=headers) + response = postgrest.session.get("/parents", headers=headers) assert response.status_code == 404 # change setting @@ -347,5 +352,7 @@ def test_db_schema_reload(session, tmp_path): # reload schema cache to verify that the config reload actually happened postgrest.process.send_signal(signal.SIGUSR1) - response = session.get(url, headers=headers) + time.sleep(0.1) + + response = postgrest.session.get("/parents", headers=headers) assert response.status_code == 200 From ebf03a8a348ff272b9cde70f509c4dd7f5daf3ef Mon Sep 17 00:00:00 2001 From: Remo Rechkemmer <59358383+monacoremo@users.noreply.github.com> Date: Sun, 13 Dec 2020 15:59:53 +0100 Subject: [PATCH 43/44] Second try on travis after all! --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0086c6d505..135b01a98a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -114,7 +114,7 @@ jobs: 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 pytest -v test/io-tests + 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) From f48d7ab83e2f80da6b6681cc66a4031f80344b59 Mon Sep 17 00:00:00 2001 From: Remo Rechkemmer <59358383+monacoremo@users.noreply.github.com> Date: Sun, 13 Dec 2020 16:04:21 +0100 Subject: [PATCH 44/44] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 135b01a98a..7bb84333d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -120,7 +120,7 @@ jobs: 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