Prechádzať zdrojové kódy

feat(test): adds framework for e2e tests

Nils Wisiol 8 rokov pred
rodič
commit
15141cf762

+ 9 - 0
api/desecapi/settings.py

@@ -174,3 +174,12 @@ ABUSE_BY_REMOTE_IP_PERIOD_HRS = 48
 ABUSE_BY_EMAIL_HOSTNAME_LIMIT = 1
 ABUSE_BY_EMAIL_HOSTNAME_PERIOD_HRS = 24
 LIMIT_USER_DOMAIN_COUNT_DEFAULT = 5
+
+if os.environ.get('DESECSTACK_E2E_TEST', "").upper() == "TRUE":
+    DEBUG = True
+    EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
+    ABUSE_BY_REMOTE_IP_LIMIT = 100
+    ABUSE_BY_REMOTE_IP_PERIOD_HRS = 4800
+    ABUSE_BY_EMAIL_HOSTNAME_LIMIT = 100
+    ABUSE_BY_EMAIL_HOSTNAME_PERIOD_HRS = 2400
+    LIMIT_USER_DOMAIN_COUNT_DEFAULT = 5000

+ 11 - 0
api/entrypoint-tests.sh

@@ -0,0 +1,11 @@
+#!/bin/bash -e
+
+# wait for dependencies
+echo "waiting for dependencies ..."
+./wait
+
+# start cron
+/root/cronhook/start-cron.sh &
+
+echo Starting API tests ...
+python3 manage.py test -v 3 --noinput

+ 3 - 5
api/entrypoint.sh

@@ -1,10 +1,8 @@
 #!/bin/bash -e
 
-# wait for api database to come up
-host=dbapi; port=3306; n=120; i=0; while ! (echo > /dev/tcp/$host/$port) 2> /dev/null; do [[ $i -eq $n ]] && >&2 echo "$host:$port not up after $n seconds, exiting" && exit 1; echo "waiting for $host:$port to come up"; sleep 1; i=$((i+1)); done
-
-# wait for pdns api to come up
-host=nslord; port=8081; n=120; i=0; while ! (echo > /dev/tcp/$host/$port) 2> /dev/null; do [[ $i -eq $n ]] && >&2 echo "$host:$port not up after $n seconds, exiting" && exit 1; echo "waiting for $host:$port to come up"; sleep 1; i=$((i+1)); done
+# wait for dependencies
+echo "waiting for dependencies ..."
+./wait
 
 # start cron
 /root/cronhook/start-cron.sh &

+ 8 - 0
api/wait

@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -e
+
+# wait for api database to come up
+host=dbapi; port=3306; n=120; i=0; while ! (echo > /dev/tcp/$host/$port) 2> /dev/null; do [[ $i -eq $n ]] && >&2 echo "$host:$port not up after $n seconds, exiting" && exit 1; echo "waiting for $host:$port to come up"; sleep 1; i=$((i+1)); done
+
+# wait for pdns api to come up
+host=nslord; port=8081; n=120; i=0; while ! (echo > /dev/tcp/$host/$port) 2> /dev/null; do [[ $i -eq $n ]] && >&2 echo "$host:$port not up after $n seconds, exiting" && exit 1; echo "waiting for $host:$port to come up"; sleep 1; i=$((i+1)); done

+ 1 - 1
docker-compose.test.yml → docker-compose.test-api.yml

@@ -3,7 +3,7 @@ version: '2.1'
 # mostly extending from main .yml
 services:
   api:
-    command: python3 manage.py test -v 3 --noinput --failfast
+    command: "./entrypoint-tests.sh"
     logging:
       driver: "json-file"
     restart: "no"

+ 41 - 0
docker-compose.test-e2e.yml

@@ -0,0 +1,41 @@
+version: '2.1'
+
+# mostly extending from main .yml
+services:
+  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
+
+  static:
+    build: test/e2e/mock-static # build a mock static to save time executing tests
+
+  test-e2e:
+    build: test/e2e
+    restart: "no"
+    environment:
+    - DESECSTACK_DOMAIN
+    - DESECSTACK_IPV4_REAR_PREFIX16
+    - DESECSTACK_IPV6_SUBNET
+    - DESECSTACK_IPV6_ADDRESS
+    mac_address: 06:42:ac:10:00:7f
+    depends_on:
+    - www
+    - nslord
+    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"

+ 21 - 0
test/e2e/Dockerfile

@@ -0,0 +1,21 @@
+FROM node:8
+
+RUN apt-get update && apt-get install -y \
+		dnsutils \
+		net-tools \
+		dirmngr gnupg \
+	--no-install-recommends && apt-get clean && rm -rf /var/lib/apt/lists/*
+
+RUN npm install -g chakram mocha
+
+RUN mkdir /usr/src/app
+WORKDIR /usr/src/app
+
+COPY ./package.json ./
+RUN npm install
+
+COPY setup.js ./setup.js
+COPY ./spec ./spec
+COPY ./apiwait ./apiwait
+
+CMD ./apiwait 45 && mocha ./spec

+ 7 - 0
test/e2e/README.md

@@ -0,0 +1,7 @@
+# End to End Tests
+
+These image sources provide a Docker container that automatically runs all tests against the REST API upon startup.
+
+To run the tests, use
+
+    docker-compose -f docker-compose.yml -f docker-compose.test-e2e.yml run -T test-e2e bash -c "./apiwait 45 && mocha ./spec"

+ 23 - 0
test/e2e/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:"

+ 1 - 0
test/e2e/mock-static/Dockerfile

@@ -0,0 +1 @@
+FROM nginx:stable

+ 18 - 0
test/e2e/package.json

@@ -0,0 +1,18 @@
+{
+  "name": "test-e2e",
+  "version": "1.0.0",
+  "description": "End-to-end tests for the desec-stack",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "Nils Wisiol",
+  "license": "MIT",
+  "dependencies": {
+    "mocha": "~3.5.0",
+    "jquery": "~3.2.1",
+    "jsdom": "~11.1.0",
+    "atob": "^2.0.3",
+    "btoa": "^1.1.2"
+  }
+}

+ 66 - 0
test/e2e/setup.js

@@ -0,0 +1,66 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
+
+var chakram = require('/usr/local/lib/node_modules/chakram/lib/chakram.js');
+var Q = require('q');
+
+// FIXME contacting nslord for DNS responses. This can changed to nsmaster as soon as changes to the DNS are applied
+// immediately, i.e. before pdns HTTP responses return to the API.
+const dns = require('dns');
+const resolver = new dns.Resolver();
+resolver.setServers([process.env.DESECSTACK_IPV4_REAR_PREFIX16 + '.0.129']);
+
+chakram.addMethod("body", function (respObj, expected) {
+    var body = respObj.body;
+
+    if (arguments.length === 1) {
+        this.assert(
+            body !== undefined && body !== null,
+            'expected body to exist',
+            'expected not to exist'
+        );
+    } else if (expected instanceof RegExp) {
+        this.assert(
+            expected.test(body),
+            'expected body with value ' + body + ' to match regex ' + expected,
+            'expected body with value ' + body + ' not to match regex ' + expected
+        );
+    } else if (typeof(expected) === 'function') {
+        expected(body);
+    } else {
+        this.assert(
+            body === expected,
+            'expected body with value ' + body + ' to match ' + expected,
+            'expected body with value ' + body + ' not to match ' + expected
+        );
+    }
+});
+
+var settings = {
+    headers: {},
+    followRedirect: false,
+    baseUrl: '',
+};
+
+chakram.getRequestSettings = function () {
+    return settings;
+};
+
+chakram.setRequestSettings = function (s) {
+    settings = s;
+    chakram.setRequestDefaults(settings);
+};
+chakram.setRequestSettings(settings);
+
+chakram.setBaseUrl = function (url) {
+    var s = chakram.getRequestSettings();
+    s.baseUrl = url;
+    chakram.setRequestSettings(s);
+};
+
+chakram.setRequestHeader = function (header, value) {
+    var s = chakram.getRequestSettings();
+    s.headers[header] = value;
+    chakram.setRequestSettings(s);
+};
+
+exports.chakram = chakram;

+ 21 - 0
test/e2e/spec/api_spec.js

@@ -0,0 +1,21 @@
+var chakram = require("./../setup.js").chakram;
+var expect = chakram.expect;
+
+describe("API", function () {
+
+    var URL = 'https://www/api/v1';
+
+    before(function () {
+        chakram.setRequestDefaults({
+            headers: {
+                'Host': 'desec.' + process.env.DESECSTACK_DOMAIN,
+            }
+        })
+    });
+
+    it("provides an index page", function () {
+        var response = chakram.get(URL + '/');
+        return expect(response).to.have.status(200);
+    });
+
+});

+ 18 - 0
test/e2e/spec/test_configuration_spec.js

@@ -0,0 +1,18 @@
+var chakram = require('./../setup.js').chakram;
+var expect = chakram.expect;
+
+describe("test configuration", function () {
+
+    it("has a hostname", function () {
+        return expect(process.env.DESECSTACK_DOMAIN).to.exist;
+    });
+
+    it("knows the ipv4 prefix", function () {
+        return expect(process.env.DESECSTACK_IPV4_REAR_PREFIX16).to.exist;
+    });
+
+    it("knows the ipv6 address of www", function () {
+        return expect(process.env.DESECSTACK_IPV6_ADDRESS).to.exist;
+    });
+
+});

+ 160 - 0
test/e2e/spec/www_spec.js

@@ -0,0 +1,160 @@
+var chakram = require("./../setup.js").chakram;
+var expect = chakram.expect;
+
+// obviously, I took this shamelessly and without further verification from stack overflow
+// https://stackoverflow.com/a/17871737
+var REGEX_IPV6_ADDRESS = /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/;
+
+describe("www/nginx", function () {
+
+    before(function () {
+        var s = chakram.getRequestSettings();
+        s.followRedirect = false;
+        s.baseUrl = '';
+        chakram.setRequestSettings(s);
+    });
+
+    describe("dedyn host", function () {
+
+        before(function () {
+            chakram.setRequestHeader('Host', 'dedyn.' + process.env.DESECSTACK_DOMAIN);
+        });
+
+        it("redirects to the desec host", function () {
+
+            [
+                'https://' + process.env.DESECSTACK_IPV4_REAR_PREFIX16 + '.0.128/',
+                'http://' + process.env.DESECSTACK_IPV4_REAR_PREFIX16 + '.0.128/',
+                'https://[' + process.env.DESECSTACK_IPV6_ADDRESS + ']/',
+                'http://[' + process.env.DESECSTACK_IPV6_ADDRESS + ']/',
+            ].forEach(function (url) {
+                var response = chakram.get(url);
+                expect(response).to.have.status(301);
+                expect(response).to.have.header('Location', 'https://desec.' + process.env.DESECSTACK_DOMAIN + '/');
+            });
+
+            return chakram.wait();
+        });
+
+    });
+
+    describe("checkip.dedyn host", function () {
+
+        before(function () {
+            chakram.setRequestHeader('Host', 'checkip.dedyn.' + process.env.DESECSTACK_DOMAIN);
+        });
+
+        describe("contacted through SSL/TLS", function () {
+
+            it('returns the ipv4 address when contacted through ipv4', function () {
+                var response = chakram.get('https://' + process.env.DESECSTACK_IPV4_REAR_PREFIX16 + '.0.128/');
+                return expect(response).to.have.body(process.env.DESECSTACK_IPV4_REAR_PREFIX16 + '.0.127');
+            });
+
+            it('returns an ipv6 address when contacted through ipv6', function () {
+                var response = chakram.get('https://[' + process.env.DESECSTACK_IPV6_ADDRESS + ']/');
+
+                // it's hard to find out which IPv6 address we actually expect here
+                // and as we are inside the docker network anyway (that is, we are
+                // topologically not in the same place as the end user), it's hard
+                // if the correct address is returned. Hence, we will stick to some
+                // simple tests.
+                expect(response).to.have.body(REGEX_IPV6_ADDRESS);
+                expect(response).to.not.have.body(process.env.DESECSTACK_IPV4_REAR_PREFIX16 + '.0.127');
+                return chakram.wait();
+            });
+
+        });
+
+        describe("contacted without encryption", function () {
+
+            it('redirects to SSL/TLS when contacted through ipv4', function () {
+                var response = chakram.get('http://' + process.env.DESECSTACK_IPV4_REAR_PREFIX16 + '.0.128/');
+                expect(response).to.have.status(301);
+                expect(response).to.have.header('Location', 'https://checkip.dedyn.' + process.env.DESECSTACK_DOMAIN + '/');
+                return chakram.wait();
+            });
+
+            it('redirects to SSL/TLS when contacted through ipv6', function () {
+                var response = chakram.get('http://[' + process.env.DESECSTACK_IPV6_ADDRESS + ']/');
+                expect(response).to.have.status(301);
+                expect(response).to.have.header('Location', 'https://checkip.dedyn.' + process.env.DESECSTACK_DOMAIN + '/');
+                return chakram.wait();
+            });
+
+        });
+
+    });
+
+    describe("checkipv4.dedyn host", function () {
+
+        before(function () {
+            chakram.setRequestHeader('Host', 'checkipv4.dedyn.' + process.env.DESECSTACK_DOMAIN);
+        });
+
+        it('returns the ipv4 address when contacted through ipv4', function () {
+            var response = chakram.get('https://' + process.env.DESECSTACK_IPV4_REAR_PREFIX16 + '.0.128/');
+            return expect(response).to.have.body(process.env.DESECSTACK_IPV4_REAR_PREFIX16 + '.0.127');
+        });
+
+        it('redirects to SSL/TLS when concated without encryption', function () {
+            var response = chakram.get('http://' + process.env.DESECSTACK_IPV4_REAR_PREFIX16 + '.0.128/');
+            expect(response).to.have.status(301);
+            expect(response).to.have.header('Location', 'https://checkipv4.dedyn.' + process.env.DESECSTACK_DOMAIN + '/');
+            return chakram.wait();
+        });
+
+        it('closes the connection when contacted through ipv6', function () {
+            var response = chakram.get('https://[' + process.env.DESECSTACK_IPV6_ADDRESS + ']/');
+            return expect(response).to.not.have.a.body();
+        });
+
+    });
+
+    describe("checkipv6.dedyn host", function () {
+
+        before(function () {
+            chakram.setRequestHeader('Host', 'checkipv6.dedyn.' + process.env.DESECSTACK_DOMAIN);
+        });
+
+        it('closes the connection when contacted through ipv4', function () {
+            var response = chakram.get('https://' + process.env.DESECSTACK_IPV4_REAR_PREFIX16 + '.0.128/');
+            return expect(response).to.not.have.a.body();
+        });
+
+        it('redirects to SSL/TLS when concated without encryption', function () {
+            var response = chakram.get('http://[' + process.env.DESECSTACK_IPV6_ADDRESS + ']/');
+            expect(response).to.have.status(301);
+            expect(response).to.have.header('Location', 'https://checkipv6.dedyn.' + process.env.DESECSTACK_DOMAIN + '/');
+            return chakram.wait();
+        });
+
+        it('returns an ipv6 address when contacted through ipv6', function () {
+            var response = chakram.get('https://[' + process.env.DESECSTACK_IPV6_ADDRESS + ']/');
+
+            // it's hard to find out which IPv6 address we actually expect here
+            // and as we are inside the docker network anyway (that is, we are
+            // topologically not in the same place as the end user), it's hard
+            // if the correct address is returned. Hence, we will stick to some
+            // simple tests.
+            expect(response).to.have.body(/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/);
+            expect(response).to.not.have.body(process.env.DESECSTACK_IPV4_REAR_PREFIX16 + '.0.127');
+            return chakram.wait();
+        });
+
+    });
+
+    describe("desec host", function () {
+
+        before(function () {
+            chakram.setRequestHeader('Host', 'desec.' + process.env.DESECSTACK_DOMAIN);
+        });
+
+        it("is alive", function () {
+            var response = chakram.get('https://www/');
+            return expect(response).to.have.status(200);
+        });
+
+    });
+
+});