summary refs log tree commit diff
diff options
context:
space:
mode:
authorsternenseemann <0rpkxez4ksa01gb3typccl0i@systemli.org>2020-11-24 21:39:26 +0100
committersternenseemann <git@lukasepple.de>2020-11-29 21:52:38 +0100
commit415f284982888628091f1b73f67c86d9dfc816f0 (patch)
tree64dcddc38231d9832c00afe48989a7ea3bb46bb5
parent246fa5dcb8d6ad40f188c9d8e541be905f52ca2c (diff)
test(warteraum): add integration tests
Test:

* response formats
* queue properties
* authentication

in a sanity-check way using our python client library and pytest.

Also we run the warteraum server inside valgrind (which makes the
integration tests really slow unfortunately) in order to sanity check
for memory leaks.
-rw-r--r--.gitignore1
-rw-r--r--default.nix2
-rw-r--r--nix/warteraum.nix7
-rwxr-xr-xwarteraum/test/run41
-rwxr-xr-xwarteraum/test/test_integration.py154
5 files changed, 203 insertions, 2 deletions
diff --git a/.gitignore b/.gitignore
index 45373c3..32d80fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ vgcore.*
 /warteraum/warteraum
 /warteraum/hashtoken
 /warteraum/test/*.exe
+/warteraum/test/valgrind-log.txt
 
 # redo-sh
 .redo
diff --git a/default.nix b/default.nix
index 0161a20..6a3787d 100644
--- a/default.nix
+++ b/default.nix
@@ -24,6 +24,7 @@ rec {
     # todo clang?
     redo = pkgs.pkgsStatic.redo-c;
     inherit scryptSalt apiTokens rootSrc sourceName;
+    inherit (python3.pkgs) pytest pytest-randomly requests flipdot-gschichtler;
   }).overrideAttrs (old: {
     # musl, static linking
     postPatch = ''
@@ -37,6 +38,7 @@ rec {
     stdenv = pkgs.clangStdenv;
     redo = pkgs.redo-c;
     inherit scryptSalt apiTokens rootSrc sourceName;
+    inherit (python3.pkgs) pytest pytest-randomly requests flipdot-gschichtler;
   };
 
   bahnhofshalle =
diff --git a/nix/warteraum.nix b/nix/warteraum.nix
index 35ff950..992f67d 100644
--- a/nix/warteraum.nix
+++ b/nix/warteraum.nix
@@ -1,4 +1,5 @@
-{ stdenv, lib, redo, scrypt, jq
+{ stdenv, lib, redo, scrypt
+, jq, requests, pytest, pytest-randomly, flipdot-gschichtler, valgrind
 , scryptSalt ? null, apiTokens ? null
 , rootSrc, sourceName
 }:
@@ -62,7 +63,9 @@ stdenv.mkDerivation rec {
 
   doCheck = true;
   checkPhase = "./test/run";
-  checkInputs = [ jq ];
+  checkInputs = [
+    jq valgrind pytest pytest-randomly requests flipdot-gschichtler
+  ];
 
   installPhase = ''
     install -Dm755 warteraum -t $out/bin
diff --git a/warteraum/test/run b/warteraum/test/run
index ef85225..36539ed 100755
--- a/warteraum/test/run
+++ b/warteraum/test/run
@@ -25,3 +25,44 @@ if command -v jq > /dev/null; then
 else
   echo -e "json validity\tskipped (missing jq)"
 fi
+
+echo -e "\n# warteraum integration tests\n"
+
+if command -v pytest > /dev/null; then
+  redo ../warteraum
+
+  rm -f valgrind-log.txt
+
+  valgrind \
+    --log-file=./valgrind-log.txt \
+    --leak-check=full \
+    --track-origins=yes \
+    --show-leak-kinds=all \
+    --show-error-list=yes \
+    ../warteraum &
+  pid=$!
+
+  sleep 3
+
+  pytest ./test_integration.py
+
+  kill $pid
+
+  sleep 1
+
+  grep "All heap blocks were freed" valgrind-log.txt
+  all_freed=$?
+
+  grep "ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)" valgrind-log.txt
+  no_errors=$?
+
+  if ! (( $all_freed && $no_errors )); then
+    echo -e "\nvalgrind test\tokay"
+  else
+    echo -e "\nvalgrind test\tfail\n"
+    cat valgrind-log.txt
+    exit 1
+  fi
+else
+  echo "skipped (missing pytest)"
+fi
diff --git a/warteraum/test/test_integration.py b/warteraum/test/test_integration.py
new file mode 100755
index 0000000..df657dd
--- /dev/null
+++ b/warteraum/test/test_integration.py
@@ -0,0 +1,154 @@
+import requests
+from flipdot_gschichtler import FlipdotGschichtlerClient, FlipdotGschichtlerError
+import pytest
+import sys
+
+BASE_URL = 'http://localhost:9000'
+TOKEN = 'hannes'
+
+# technically somebody could use these tokens,
+# but tbh they deserve this test to fail then
+WRONG_TOKENS = [ 'password', 'admin', '12345678' ]
+
+MAX_TEXT_LEN = 512
+
+api = FlipdotGschichtlerClient(BASE_URL, api_token = TOKEN)
+
+# API response format
+
+def test_queue_add_format():
+    my_text = 'hello world'
+    r = requests.post(BASE_URL + '/api/v2/queue/add', data = { 'text' : my_text })
+
+    assert r.status_code == 200
+
+    assert r.json()['text'] == my_text
+    my_id = r.json()['id']
+
+    assert my_id >= 0
+
+def test_queue_format():
+    r = requests.get(BASE_URL + '/api/v2/queue')
+
+    assert r.status_code == 200
+    assert r.json()['length'] == len(r.json()['queue'])
+
+def test_queue_del_format():
+    my_id = api.add('test_queue_del_format')
+
+    r = requests.delete('{}/api/v2/queue/{}'.format(BASE_URL, my_id), data = { 'token' : TOKEN })
+
+    # accept unauthorized (we can't know the token if a custom one was set
+    # via the nix derivation arguments)
+    if r.status_code == 401:
+        pytest.xfail('invalid API token is configured')
+
+    assert r.status_code == 204
+
+def test_queue_404_format():
+    r = requests.get(BASE_URL + '/api/v2/coffee')
+
+    assert r.status_code == 404
+    assert 'not found' in r.json()['error']
+
+# /api/v2/queue/add input validation and normalization
+
+def test_strip_whitespace():
+    r = requests.post(BASE_URL + '/api/v2/queue/add', data = { 'text' : '   foo   ' })
+    assert r.json()['text'] == 'foo'
+
+def test_text_within_tolerance():
+    long_string = '?' * MAX_TEXT_LEN
+    my_id = api.add(long_string)
+
+    found = False
+    for q in api.queue():
+        if q['id'] == my_id:
+            assert q['text'] == long_string
+            found = True
+
+    assert found
+
+def test_too_long_text():
+    long_string = '!' * (MAX_TEXT_LEN + 1)
+
+    r = requests.post(BASE_URL + '/api/v2/queue/add', data = { 'text' : long_string })
+
+    assert r.status_code == 413
+
+def test_whitespace_irrelevant_for_length():
+    long_string = 'foo' + (MAX_TEXT_LEN * ' ')
+
+    r = requests.post(BASE_URL + '/api/v2/queue/add', data = { 'text' : long_string })
+
+    assert r.status_code == 200
+    assert r.json()['text'] == 'foo'
+
+# authentication
+
+def test_expected_authentication_failures():
+    for t in WRONG_TOKENS:
+        # client with wrong token
+        tmp_client = FlipdotGschichtlerClient(BASE_URL, api_token = t)
+
+        # should be able to add text
+        my_id = tmp_client.add(t)
+
+        # but not delete them
+        with pytest.raises(FlipdotGschichtlerError) as err:
+            tmp_client.delete(my_id)
+            assert err.status == 403
+
+        # what our normal client can do
+        api.delete(my_id)
+
+def test_correct_failure_with_valid_token():
+    q = api.queue()
+
+    if len(q) > 0:
+        highest_id = q[-1]['id']
+    else:
+        highest_id = 0
+
+    with pytest.raises(FlipdotGschichtlerError) as err:
+        api.delete(highest_id + 1)
+        assert err.status == 404
+
+# queue properties
+
+def test_queue_ascending_ids():
+    for x in range(15):
+        api.add(str(x))
+
+    last_id = -1
+    for q in api.queue():
+        assert q['id'] > last_id
+        last_id = q['id']
+
+def test_reassigning_only_after_emptying():
+    for x in range(15):
+        api.add(str(x))
+
+    queue = api.queue()
+
+    to_delete = [x['id'] for x in queue[:-1]]
+
+    for d in to_delete:
+        api.delete(d)
+
+    assert len(api.queue()) == 1
+
+    for x in range(15):
+        api.add(str(x))
+
+    ids_after = [x['id'] for x in api.queue()]
+
+    for d in to_delete:
+        assert not (d in ids_after)
+
+    for q in api.queue():
+        api.delete(q['id'])
+
+    assert api.add('only text') == 0
+    assert len(api.queue()) == 1
+    assert api.queue()[0]['id'] == 0