Browse Source

feat(test): adds rudimentary e2e test framework using pytest

Nils Wisiol 4 years ago
parent
commit
0b3d129161

+ 12 - 0
.github/workflows/main.yml

@@ -62,6 +62,18 @@ jobs:
         docker-compose -f docker-compose.yml -f docker-compose.test-e2e.yml down -v
         docker-compose -f docker-compose.yml -f docker-compose.test-e2e.yml down -v
         docker-compose down -v
         docker-compose down -v
 
 
+    - name: Run e2e2 Tests
+      run: |
+        docker-compose -f docker-compose.yml -f docker-compose.test-e2e2.yml run -T test-e2e2 bash -c "./apiwait 45 && python3 -m pytest -vv ."
+
+    - name: e2e2 Tests Logs and Cleanup
+      if: always()
+      run: |
+        docker-compose -f docker-compose.yml -f docker-compose.test-e2e2.yml ps
+        docker-compose -f docker-compose.yml -f docker-compose.test-e2e2.yml logs
+        docker-compose -f docker-compose.yml -f docker-compose.test-e2e2.yml down -v
+        docker-compose down -v
+
     - name: Run API Tests
     - name: Run API Tests
       run: |
       run: |
         docker-compose -f docker-compose.yml -f docker-compose.test-api.yml run -T api bash -c "./entrypoint-tests.sh"
         docker-compose -f docker-compose.yml -f docker-compose.test-api.yml run -T api bash -c "./entrypoint-tests.sh"

+ 54 - 0
docker-compose.test-e2e2.yml

@@ -0,0 +1,54 @@
+version: '2.2'
+
+# mostly extending from main .yml
+services:
+  www:
+    environment:
+    - DESECSTACK_E2E_TEST=TRUE # increase abuse limits and such
+    volumes:
+    - autocert:/autocert/
+
+  api:
+    environment:
+    - DESECSTACK_E2E_TEST=TRUE # increase abuse limits and such
+
+  nslord:
+    networks:
+      front:
+        ipv4_address: ${DESECSTACK_IPV4_REAR_PREFIX16}.0.129 # make nslord available for test-e2e
+    environment:
+    - DESECSTACK_NSLORD_CACHE_TTL=0
+
+  test-e2e2:
+    build: test/e2e2
+    restart: "no"
+    environment:
+    - DESECSTACK_DOMAIN
+    - DESECSTACK_NS
+    - DESECSTACK_IPV4_REAR_PREFIX16
+    - DESECSTACK_IPV6_SUBNET
+    - DESECSTACK_IPV6_ADDRESS
+    - DESECSTACK_NSLORD_DEFAULT_TTL
+    volumes:
+    - autocert:/autocert/:ro
+    mac_address: 06:42:ac:10:00:7f
+    depends_on:
+    - www
+    - nslord
+    - nsmaster
+    networks:
+      front:
+        ipv4_address: ${DESECSTACK_IPV4_REAR_PREFIX16}.0.127
+    extra_hosts:
+    - "checkipv4.dedyn.${DESECSTACK_DOMAIN}:${DESECSTACK_IPV4_REAR_PREFIX16}.0.128"
+    - "checkipv6.dedyn.${DESECSTACK_DOMAIN}:${DESECSTACK_IPV4_REAR_PREFIX16}.0.128"
+    - "checkip.dedyn.${DESECSTACK_DOMAIN}:${DESECSTACK_IPV4_REAR_PREFIX16}.0.128"
+    - "dedyn.${DESECSTACK_DOMAIN}:${DESECSTACK_IPV4_REAR_PREFIX16}.0.128"
+    - "desec.${DESECSTACK_DOMAIN}:${DESECSTACK_IPV4_REAR_PREFIX16}.0.128"
+    - "update6.dedyn.${DESECSTACK_DOMAIN}:${DESECSTACK_IPV4_REAR_PREFIX16}.0.128"
+    - "update.dedyn.${DESECSTACK_DOMAIN}:${DESECSTACK_IPV4_REAR_PREFIX16}.0.128"
+    - "www.dedyn.${DESECSTACK_DOMAIN}:${DESECSTACK_IPV4_REAR_PREFIX16}.0.128"
+    - "www.desec.${DESECSTACK_DOMAIN}:${DESECSTACK_IPV4_REAR_PREFIX16}.0.128"
+
+volumes:
+  autocert:

+ 12 - 0
test/e2e2/Dockerfile

@@ -0,0 +1,12 @@
+FROM python:3.8
+
+RUN mkdir /e2e
+WORKDIR /e2e
+COPY requirements.txt .
+RUN python3 -m pip install -r requirements.txt
+
+COPY apiwait .
+COPY *.py .
+COPY ./spec .
+
+CMD ./apiwait 45 && python3 -m pytest -vv .

+ 23 - 0
test/e2e2/apiwait

@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+if [ -f ./.env ] ; then
+    source ../../.env
+fi
+
+TIME=0
+LIMIT=${1:-3}  # getting limit or default to 3 [sic]
+URL=https://www/api/v1/
+
+until curl --insecure --fail -H "Host: desec.$DESECSTACK_DOMAIN" $URL > /dev/null 2> /dev/null
+do
+    sleep 1
+    ((TIME+=1))
+
+    if [ $TIME -gt $LIMIT ]; then
+        curl --insecure -H "Host: desec.$DESECSTACK_DOMAIN" $URL
+        echo "waited $LIMIT seconds for api (desec.$DESECSTACK_DOMAIN) at $URL, giving up" > /dev/stderr
+        exit 1
+    fi
+done
+
+echo "api (desec.$DESECSTACK_DOMAIN) came up at $URL after $TIME seconds:"

+ 99 - 0
test/e2e2/conftest.py

@@ -0,0 +1,99 @@
+import json
+import os
+import random
+import string
+from typing import Optional, Tuple
+
+import pytest
+import requests
+
+
+class DeSECAPIV1Client:
+    base_url = "https://desec." + os.environ["DESECSTACK_DOMAIN"] + "/api/v1"
+    headers = {
+        "Accept": "application/json",
+        "Content-Type": "application/json",
+        "User-Agent": "e2e2",
+    }
+
+    @staticmethod
+    def random_email() -> str:
+        return (
+            "".join(random.choice(string.ascii_letters) for _ in range(10))
+            + "@desec.test"
+        )
+
+    @staticmethod
+    def random_password() -> str:
+        return "".join(random.choice(string.ascii_letters) for _ in range(16))
+
+    def __init__(self) -> None:
+        super().__init__()
+        self.email = None
+        self.password = None
+
+    def _request(self, method: str, *, path: str, data: Optional[dict] = None, **kwargs) -> requests.Response:
+        if data is not None:
+            data = json.dumps(data)
+
+        return requests.request(
+            method,
+            self.base_url + path,
+            data=data,
+            headers=self.headers,
+            verify=f'/autocert/desec.{os.environ["DESECSTACK_DOMAIN"]}.cer',
+            **kwargs,
+        )
+
+    def get(self, path: str, **kwargs) -> requests.Response:
+        return self._request("GET", path=path, **kwargs)
+
+    def post(self, path: str, data: Optional[dict] = None, **kwargs) -> requests.Response:
+        return self._request("POST", path=path, data=data, **kwargs)
+
+    def register(self, email: str, password: str) -> Tuple[requests.Response, requests.Response]:
+        self.email = email
+        self.password = password
+        captcha = self.post("/captcha/")
+        return captcha, self.post(
+            "/auth/",
+            data={
+                "email": email,
+                "password": password,
+                "captcha": {
+                    "id": captcha.json()["id"],
+                    "solution": captcha.json()[
+                        "content"
+                    ],  # available via e2e configuration magic
+                },
+            },
+        )
+
+    def login(self, email: str, password: str) -> requests.Response:
+        token = self.post(
+            "/auth/login/", data={"email": email, "password": password}
+        )
+        self.headers["Authorization"] = f'Token {token.json()["token"]}'
+        return token
+
+
+@pytest.fixture
+def api_anon() -> DeSECAPIV1Client:
+    """
+    Anonymous access to the API.
+    """
+    return DeSECAPIV1Client()
+
+
+@pytest.fixture()
+def api_user(api_anon) -> DeSECAPIV1Client:
+    """
+    Access to the API with a fresh user account (zero domains, one token). Authorization header
+    is preconfigured, email address and password are randomly chosen.
+    """
+    api = api_anon
+    email = api.random_email()
+    password = api.random_password()
+    api.register(email, password)
+    api.login(email, password)
+    return api

+ 2 - 0
test/e2e2/requirements.txt

@@ -0,0 +1,2 @@
+pytest
+requests

+ 9 - 0
test/e2e2/spec/test_api_basic.py

@@ -0,0 +1,9 @@
+from conftest import DeSECAPIV1Client
+
+
+def test_homepage(api_anon: DeSECAPIV1Client):
+    assert api_anon.get("/").json() == {
+        "register": f"{api_anon.base_url}/auth/",
+        "login": f"{api_anon.base_url}/auth/login/",
+        "reset-password": f"{api_anon.base_url}/auth/account/reset-password/",
+    }

+ 29 - 0
test/e2e2/spec/test_api_user_mgmt.py

@@ -0,0 +1,29 @@
+from conftest import DeSECAPIV1Client
+
+
+def test_register(api_anon: DeSECAPIV1Client):
+    email = "e2e2@desec.test"
+    password = "foobar12"
+
+    assert api_anon.register(email, password)[1].json() == {"detail": "Welcome!"}
+    assert "token" in api_anon.login(email, password).json()
+    api = api_anon
+
+    assert api.get("/").json() == {
+        "domains": f"{api.base_url}/domains/",
+        "tokens": f"{api.base_url}/auth/tokens/",
+        "logout": f"{api.base_url}/auth/logout/",
+        "account": {
+            "change-email": f"{api.base_url}/auth/account/change-email/",
+            "delete": f"{api.base_url}/auth/account/delete/",
+            "reset-password": f"{api.base_url}/auth/account/reset-password/",
+            "show": f"{api.base_url}/auth/account/",
+        },
+    }
+
+
+def test_register2(api_user: DeSECAPIV1Client):
+    user = api_user.get("/auth/account/").json()
+    assert user["email"] == api_user.email
+    assert api_user.headers['Authorization'].startswith('Token ')
+    assert len(api_user.headers['Authorization']) > len('Token ') + 10