Sfoglia il codice sorgente

Merged master, fixed BATS conflict

Steve Kamerman 8 anni fa
parent
commit
ad9af2884d
100 ha cambiato i file con 2620 aggiunte e 1904 eliminazioni
  1. 2 0
      .gitignore
  2. 7 11
      .travis.yml
  3. 1 1
      Dockerfile
  4. 1 1
      Dockerfile.alpine
  5. 9 14
      Makefile
  6. 1 1
      Procfile
  7. 43 5
      README.md
  8. 0 1
      docker-compose.yml
  9. 21 1
      nginx.tmpl
  10. 107 14
      test/README.md
  11. 81 0
      test/certs/README.md
  12. 21 0
      test/certs/ca-root.crt
  13. 27 0
      test/certs/ca-root.key
  14. 183 0
      test/certs/create_server_certificate.sh
  15. 461 0
      test/conftest.py
  16. 0 33
      test/default-host.bats
  17. 0 123
      test/docker.bats
  18. 0 139
      test/headers.bats
  19. 0 6
      test/lib/README.md
  20. 0 596
      test/lib/bats/batslib.bash
  21. 0 264
      test/lib/bats/batslib/output.bash
  22. 0 66
      test/lib/docker_helpers.bash
  23. 0 22
      test/lib/helpers.bash
  24. 0 24
      test/lib/ssl/nginx-proxy.bats.crt
  25. 0 28
      test/lib/ssl/nginx-proxy.bats.key
  26. 0 43
      test/multiple-hosts.bats
  27. 0 64
      test/multiple-ports.bats
  28. 3 0
      test/pytest.ini
  29. 24 0
      test/pytest.sh
  30. 5 0
      test/requirements/Dockerfile-nginx-proxy-tester
  31. 52 0
      test/requirements/README.md
  32. 6 0
      test/requirements/build.sh
  33. 5 0
      test/requirements/python-requirements.txt
  34. 8 0
      test/requirements/web/Dockerfile
  35. 15 0
      test/requirements/web/entrypoint.sh
  36. 7 3
      test/requirements/web/webserver.py
  37. 0 168
      test/ssl.bats
  38. 15 0
      test/test_DOCKER_HOST_unix_socket.py
  39. 24 0
      test/test_DOCKER_HOST_unix_socket.yml
  40. 10 0
      test/test_composev2.py
  41. 14 0
      test/test_composev2.yml
  42. 1 0
      test/test_custom/my_custom_proxy_settings.conf
  43. 1 0
      test/test_custom/my_custom_proxy_settings_bar.conf
  44. 28 0
      test/test_custom/test_defaults-location.py
  45. 30 0
      test/test_custom/test_defaults-location.yml
  46. 20 0
      test/test_custom/test_defaults.py
  47. 23 0
      test/test_custom/test_defaults.yml
  48. 22 0
      test/test_custom/test_location-per-vhost.py
  49. 23 0
      test/test_custom/test_location-per-vhost.yml
  50. 19 0
      test/test_custom/test_per-vhost.py
  51. 23 0
      test/test_custom/test_per-vhost.yml
  52. 20 0
      test/test_custom/test_proxy-wide.py
  53. 23 0
      test/test_custom/test_proxy-wide.yml
  54. 7 0
      test/test_default-host.py
  55. 17 0
      test/test_default-host.yml
  56. 1 0
      test/test_dockergen/.gitignore
  57. 38 0
      test/test_dockergen/test_dockergen_v2.py
  58. 26 0
      test/test_dockergen/test_dockergen_v2.yml
  59. 47 0
      test/test_dockergen/test_dockergen_v3.py
  60. 27 0
      test/test_dockergen/test_dockergen_v3.yml
  61. 46 0
      test/test_events.py
  62. 4 0
      test/test_events.yml
  63. 70 0
      test/test_headers/certs/web.nginx-proxy.tld.crt
  64. 27 0
      test/test_headers/certs/web.nginx-proxy.tld.key
  65. 81 0
      test/test_headers/test_http.py
  66. 13 0
      test/test_headers/test_http.yml
  67. 82 0
      test/test_headers/test_https.py
  68. 15 0
      test/test_headers/test_https.yml
  69. 0 183
      test/test_helpers.bash
  70. 35 0
      test/test_ipv6.py
  71. 23 0
      test/test_ipv6.yml
  72. 16 0
      test/test_multiple-hosts.py
  73. 13 0
      test/test_multiple-hosts.yml
  74. 15 0
      test/test_multiple-networks.py
  75. 34 0
      test/test_multiple-networks.yml
  76. 7 0
      test/test_multiple-ports/test_VIRTUAL_PORT.py
  77. 14 0
      test/test_multiple-ports/test_VIRTUAL_PORT.yml
  78. 7 0
      test/test_multiple-ports/test_default-80.py
  79. 13 0
      test/test_multiple-ports/test_default-80.yml
  80. 7 0
      test/test_multiple-ports/test_single-port-not-80.py
  81. 13 0
      test/test_multiple-ports/test_single-port-not-80.yml
  82. 24 0
      test/test_nominal.py
  83. 21 0
      test/test_nominal.yml
  84. 70 0
      test/test_ssl/certs/nginx-proxy.tld.crt
  85. 27 0
      test/test_ssl/certs/nginx-proxy.tld.key
  86. 71 0
      test/test_ssl/certs/web2.nginx-proxy.tld.crt
  87. 27 0
      test/test_ssl/certs/web2.nginx-proxy.tld.key
  88. 71 0
      test/test_ssl/certs/web3.nginx-proxy.tld.crt
  89. 27 0
      test/test_ssl/certs/web3.nginx-proxy.tld.key
  90. 18 0
      test/test_ssl/test_nohttp.py
  91. 15 0
      test/test_ssl/test_nohttp.yml
  92. 12 0
      test/test_ssl/test_nohttps.py
  93. 14 0
      test/test_ssl/test_nohttps.yml
  94. 19 0
      test/test_ssl/test_noredirect.py
  95. 15 0
      test/test_ssl/test_noredirect.yml
  96. 23 0
      test/test_ssl/test_wildcard.py
  97. 13 0
      test/test_ssl/test_wildcard.yml
  98. 32 0
      test/test_wildcard_host.py
  99. 37 0
      test/test_wildcard_host.yml
  100. 0 93
      test/wildcard-hosts.bats

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+**/__pycache__/
+**/.cache/

+ 7 - 11
.travis.yml

@@ -4,8 +4,11 @@ services:
   - docker
 
 env:
-    global:
-        - DOCKER_VERSION=1.12.3-0~trusty
+  global:
+  - DOCKER_VERSION=1.13.1-0~ubuntu-trusty
+  matrix:
+  - TEST_TARGET: test-debian
+  - TEST_TARGET: test-alpine
 
 before_install:
   # list docker-engine versions
@@ -14,15 +17,8 @@ before_install:
   - sudo apt-get -o Dpkg::Options::="--force-confnew" install -y --force-yes docker-engine=${DOCKER_VERSION}
   - docker version
   - docker info
-  - sudo add-apt-repository ppa:duggan/bats --yes
-  - sudo apt-get update -qq
-  - sudo apt-get install -qq bats
+  # prepare docker test requirements
   - make update-dependencies
 
-matrix:
-  include:
-    - env: TEST_ID=test-debian
-    - env: TEST_ID=test-alpine
-
 script:
-  - make $TEST_ID
+  - make $TEST_TARGET

+ 1 - 1
Dockerfile

@@ -1,4 +1,4 @@
-FROM nginx:1.11.8
+FROM nginx:1.11.10
 MAINTAINER Jason Wilder mail@jasonwilder.com
 
 # Install wget and install/updates certificates

+ 1 - 1
Dockerfile.alpine

@@ -1,4 +1,4 @@
-FROM nginx:1.11.8-alpine
+FROM nginx:1.11.10-alpine
 MAINTAINER Jason Wilder mail@jasonwilder.com
 
 # Install wget and install/updates certificates

+ 9 - 14
Makefile

@@ -1,21 +1,16 @@
 .SILENT :
-.PHONY : test
+.PHONY : test-debian test-alpine test
+
 
 update-dependencies:
-	docker pull jwilder/docker-gen:0.7.3
-	docker pull nginx:1.11.6
-	docker pull nginx:1.11.8-alpine
-	docker pull python:3
-	docker pull rancher/socat-docker:latest
-	docker pull appropriate/curl:latest
-	docker pull docker:1.10
+	test/requirements/build.sh
 
-test-debian:
-	docker build -t jwilder/nginx-proxy:bats .
-	bats test
+test-debian: update-dependencies
+	docker build -t jwilder/nginx-proxy:test .
+	test/pytest.sh
 
-test-alpine:
-	docker build -f Dockerfile.alpine -t jwilder/nginx-proxy:bats .
-	bats test
+test-alpine: update-dependencies
+	docker build -f Dockerfile.alpine -t jwilder/nginx-proxy:test .
+	test/pytest.sh
 
 test: test-debian test-alpine

+ 1 - 1
Procfile

@@ -1,2 +1,2 @@
-nginx: nginx
 dockergen: docker-gen -watch -notify "nginx -s reload" /app/nginx.tmpl /etc/nginx/conf.d/default.conf
+nginx: nginx

+ 43 - 5
README.md

@@ -1,4 +1,4 @@
-![nginx 1.11.8](https://img.shields.io/badge/nginx-1.11.8-brightgreen.svg) ![License MIT](https://img.shields.io/badge/license-MIT-blue.svg) [![Build Status](https://travis-ci.org/jwilder/nginx-proxy.svg?branch=master)](https://travis-ci.org/jwilder/nginx-proxy) [![](https://img.shields.io/docker/stars/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub') [![](https://img.shields.io/docker/pulls/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub')
+![nginx 1.11.10](https://img.shields.io/badge/nginx-1.11.10-brightgreen.svg) ![License MIT](https://img.shields.io/badge/license-MIT-blue.svg) [![Build Status](https://travis-ci.org/jwilder/nginx-proxy.svg?branch=master)](https://travis-ci.org/jwilder/nginx-proxy) [![](https://img.shields.io/docker/stars/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub') [![](https://img.shields.io/docker/pulls/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub')
 
 
 nginx-proxy sets up a container running nginx and [docker-gen][1].  docker-gen generates reverse proxy configs for nginx and reloads nginx when containers are started and stopped.
@@ -15,10 +15,26 @@ Then start any containers you want proxied with an env var `VIRTUAL_HOST=subdoma
 
     $ docker run -e VIRTUAL_HOST=foo.bar.com  ...
 
-The containers being proxied must [expose](https://docs.docker.com/reference/run/#expose-incoming-ports) the port to be proxied, either by using the `EXPOSE` directive in their `Dockerfile` or by using the `--expose` flag to `docker run` or `docker create`.
+The containers being proxied must [expose](https://docs.docker.com/engine/reference/run/#expose-incoming-ports) the port to be proxied, either by using the `EXPOSE` directive in their `Dockerfile` or by using the `--expose` flag to `docker run` or `docker create`.
 
 Provided your DNS is setup to forward foo.bar.com to the a host running nginx-proxy, the request will be routed to a container with the VIRTUAL_HOST env var set.
 
+### Image variants
+
+The nginx-proxy images are available in two flavors.
+
+#### jwilder/nginx-proxy:latest
+
+This image uses the debian:jessie based nginx image.
+
+    $ docker pull jwilder/nginx-proxy:latest
+
+#### jwilder/nginx-proxy:alpine
+
+This image is based on the nginx:alpine image.
+
+    $ docker pull jwilder/nginx-proxy:alpine
+
 ### Docker Compose
 
 ```yaml
@@ -45,6 +61,12 @@ $ curl -H "Host: whoami.local" localhost
 I'm 5b129ab83266
 ```
 
+### IPv6 support
+
+You can activate the IPv6 support for the nginx-proxy container by passing the value `true` to the `ENABLE_IPV6` environment variable:
+
+    $ docker run -d -p 80:80 -e ENABLE_IPV6=true -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy
+
 ### Multiple Ports
 
 If your container exposes multiple ports, nginx-proxy will default to the service running on port 80.  If you need to specify a different port, you can set a VIRTUAL_PORT env var to select a different one.  If your container only exposes one port and it has a VIRTUAL_HOST env var set, that port will be selected.
@@ -129,7 +151,7 @@ Finally, start your containers with `VIRTUAL_HOST` environment variables.
     $ docker run -e VIRTUAL_HOST=foo.bar.com  ...
 ### SSL Support using letsencrypt
 
-[letsencrypt-nginx-proxy-companion](https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion)is a lightweight companion container for the nginx-proxy. It allow the creation/renewal of Let's Encrypt certificates automatically. 
+[letsencrypt-nginx-proxy-companion](https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion) is a lightweight companion container for the nginx-proxy. It allow the creation/renewal of Let's Encrypt certificates automatically. 
 
 ### SSL Support
 
@@ -307,7 +329,7 @@ If you are using multiple hostnames for a single container (e.g. `VIRTUAL_HOST=e
 #### Per-VIRTUAL_HOST location default configuration
 
 If you want most of your virtual hosts to use a default single `location` block configuration and then override on a few specific ones, add those settings to the `/etc/nginx/vhost.d/default_location` file. This file
-will be used on any virtual host which does not have a `/etc/nginx/vhost.d/{VIRTUAL_HOST}` file associated with it.
+will be used on any virtual host which does not have a `/etc/nginx/vhost.d/{VIRTUAL_HOST}_location` file associated with it.
 
 ### Contributing
 
@@ -315,6 +337,22 @@ Before submitting pull requests or issues, please check github to make sure an e
 
 #### Running Tests Locally
 
-To run tests, you'll need to install [bats 0.4.0](https://github.com/sstephenson/bats).
+To run tests, you need to prepare the docker image to test which must be tagged `jwilder/nginx-proxy:test`:
+
+    docker build -t jwilder/nginx-proxy:test .  # build the Debian variant image
+    
+and call the [test/pytest.sh](test/pytest.sh) script.
+
+Then build the Alpine variant of the image:
+
+    docker build -f Dockerfile.alpine -t jwilder/nginx-proxy:test .  # build the Alpline variant image
+
+and call the [test/pytest.sh](test/pytest.sh) script again.
+
+
+If your system has the `make` command, you can automate those tasks by calling:
 
     make test
+    
+
+You can learn more about how the test suite works and how to write new tests in the [test/README.md](test/README.md) file.

+ 0 - 1
docker-compose.yml

@@ -12,4 +12,3 @@ services:
     image: jwilder/whoami
     environment:
       - VIRTUAL_HOST=whoami.local
-

+ 21 - 1
nginx.tmpl

@@ -77,9 +77,13 @@ proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
 proxy_set_header Proxy "";
 {{ end }}
 
+{{ $enable_ipv6 := eq (or ($.Env.ENABLE_IPV6) "") "true" }}
 server {
 	server_name _; # This is just an invalid value which will never trigger on a real hostname.
 	listen 80;
+	{{ if $enable_ipv6 }}
+	listen [::]:80;
+	{{ end }}
 	access_log /var/log/nginx/access.log vhost;
 	return 503;
 }
@@ -88,6 +92,9 @@ server {
 server {
 	server_name _; # This is just an invalid value which will never trigger on a real hostname.
 	listen 443 ssl http2;
+	{{ if $enable_ipv6 }}
+	listen [::]:443 ssl http2;
+	{{ end }}
 	access_log /var/log/nginx/access.log vhost;
 	return 503;
 
@@ -98,7 +105,8 @@ server {
 {{ end }}
 
 {{ range $host, $containers := groupByMulti $ "Env.VIRTUAL_HOST" "," }}
-{{ $upstream_name := sha1 $host }}
+{{ $is_regexp := hasPrefix "~" $host }}
+{{ $upstream_name := when $is_regexp (sha1 $host) $host }}
 # {{ $host }}
 upstream {{ $upstream_name }} {
 {{ range $container := $containers }}
@@ -155,6 +163,9 @@ upstream {{ $upstream_name }} {
 server {
 	server_name {{ $host }};
 	listen 80 {{ $default_server }};
+	{{ if $enable_ipv6 }}
+	listen [::]:80 {{ $default_server }};
+	{{ end }}
 	access_log /var/log/nginx/access.log vhost;
 	return 301 https://$host$request_uri;
 }
@@ -163,6 +174,9 @@ server {
 server {
 	server_name {{ $host }};
 	listen 443 ssl http2 {{ $default_server }};
+	{{ if $enable_ipv6 }}
+	listen [::]:443 ssl http2 {{ $default_server }};
+	{{ end }}
 	access_log /var/log/nginx/access.log vhost;
 
 	ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
@@ -216,6 +230,9 @@ server {
 server {
 	server_name {{ $host }};
 	listen 80 {{ $default_server }};
+	{{ if $enable_ipv6 }}
+	listen [::]:80 {{ $default_server }};
+	{{ end }}
 	access_log /var/log/nginx/access.log vhost;
 
 	{{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }}
@@ -247,6 +264,9 @@ server {
 server {
 	server_name {{ $host }};
 	listen 443 ssl http2 {{ $default_server }};
+	{{ if $enable_ipv6 }}
+	listen [::]:443 ssl http2 {{ $default_server }};
+	{{ end }}
 	access_log /var/log/nginx/access.log vhost;
 	return 500;
 

+ 107 - 14
test/README.md

@@ -1,14 +1,107 @@
-Test suite
-==========
-
-This test suite is implemented on top of the [Bats](https://github.com/sstephenson/bats/blob/master/README.md) test framework.
-
-It is intended to verify the correct behavior of the Docker image `jwilder/nginx-proxy:bats`.
-
-Running the test suite
-----------------------
-
-Make sure you have Bats installed, then run:
-
-    docker build -t jwilder/nginx-proxy:bats .
-    bats test/
+Nginx proxy test suite
+======================
+
+Install requirements
+--------------------
+
+You need [python 2.7](https://www.python.org/) and [pip](https://pip.pypa.io/en/stable/installing/) installed. Then run the commands:
+
+    requirements/build.sh
+    pip install -r requirements/python-requirements.txt
+
+If you can't install those requirements on your computer, you can alternatively use the _pytest.sh_ script which will run the tests from a Docker container which has those requirements.
+
+
+Prepare the nginx-proxy test image
+----------------------------------
+
+    docker build -t jwilder/nginx-proxy:test ..
+
+or if you want to test the alpine flavor:
+
+    docker build -t jwilder/nginx-proxy:test -f Dockerfile.alpine ..
+
+make sure to tag that test image exactly `jwilder/nginx-proxy:test` or the test suite won't work.
+
+
+Run the test suite
+------------------
+
+    pytest
+
+need more verbosity ?
+
+    pytest -s
+
+
+Run one single test module
+--------------------------
+
+    pytest test_nominal.py
+
+
+Write a test module
+-------------------
+
+This test suite uses [pytest](http://doc.pytest.org/en/latest/). The [conftest.py](conftest.py) file will be automatically loaded by pytest and will provide you with two useful pytest [fixtures](http://doc.pytest.org/en/latest/fixture.html#fixture): 
+
+- docker_compose
+- nginxproxy
+
+
+### docker_compose fixture
+
+When using the `docker_compose` fixture in a test, pytest will try to find a yml file named after your test module filename. For instance, if your test module is `test_example.py`, then the `docker_compose` fixture will try to load a `test_example.yml` [docker compose file](https://docs.docker.com/compose/compose-file/).
+
+Once the docker compose file found, the fixture will remove all containers, run `docker-compose up`, and finally your test will be executed.
+
+The fixture will run the _docker-compose_ command with the `-f` option to load the given compose file. So you can test your docker compose file syntax by running it yourself with:
+
+    docker-compose -f test_example.yml up -d
+
+In the case you are running pytest from within a docker container, the `docker_compose` fixture will make sure the container running pytest is attached to all docker networks. That way, your test will be able to reach any of them.
+
+In your tests, you can use the `docker_compose` variable to query and command the docker daemon as it provides you with a [client from the docker python module](https://docker-py.readthedocs.io/en/2.0.2/client.html#client-reference).
+
+Also this fixture alters the way the python interpreter resolves domain names to IP addresses in the following ways:
+
+Any domain name containing the substring `nginx-proxy` will resolve to the IP address of the container that was created from the `jwilder/nginx-proxy:test` image. So all the following domain names will resolve to the nginx-proxy container in tests:
+- `nginx-proxy`
+- `nginx-proxy.com`
+- `www.nginx-proxy.com`
+- `www.nginx-proxy.test`
+- `www.nginx-proxy`
+- `whatever.nginx-proxyooooooo`
+- ...
+
+Any domain name ending with `XXX.container.docker` will resolve to the IP address of the XXX container.
+- `web1.container.docker` will resolve to the IP address of the `web1` container
+- `f00.web1.container.docker` will resolve to the IP address of the `web1` container
+- `anything.whatever.web2.container.docker` will resolve to the IP address of the `web2` container
+
+Otherwise, domain names are resoved as usual using your system DNS resolver.
+
+
+### nginxproxy fixture
+
+The `nginxproxy` fixture will provide you with a replacement for the python [requests](https://pypi.python.org/pypi/requests/) module. This replacement will just repeat up to 30 times a requests if it receives the HTTP error 404 or 502. This error occurs when you try to send queries to nginx-proxy too early after the container creation.
+
+Also this requests replacement is preconfigured to use the Certificate Authority root certificate [certs/ca-root.crt](certs/) to validate https connections.
+
+Furthermore, the nginxproxy methods accept an additional keyword parameter: `ipv6` which forces requests made against containers to use the containers IPv6 address when set to `True`. If IPv6 is not supported by the system or docker, that particular test will be skipped.
+
+    def test_forwards_to_web1_ipv6(docker_compose, nginxproxy):
+        r = nginxproxy.get("http://web1.nginx-proxy.tld/port", ipv6=True)
+        assert r.status_code == 200   
+        assert r.text == "answer from port 81\n"
+
+
+
+### The web docker image
+
+When you ran the `requirements/build.sh` script earlier, you built a [`web`](requirements/README.md) docker image which is convenient for running a small web server in a container. This image can produce containers that listens on multiple ports at the same time.
+
+
+### Testing TLS
+
+If you need to create server certificates, use the [`certs/create_server_certificate.sh`](certs/) script. Pytest will be able to validate any certificate issued from this script.

+ 81 - 0
test/certs/README.md

@@ -0,0 +1,81 @@
+create_server_certificate.sh
+============================
+
+`create_server_certificate.sh` is a script helping with issuing server certificates that can be used to provide TLS on web servers.
+
+It also creates a Certificate Authority (CA) root key and certificate. This CA root certificate can be used to validate the server certificates it generates.
+
+For instance, with _curl_:
+
+    curl --cacert /somewhere/ca-root.crt https://www.example.com/
+
+or with _wget_:
+
+    wget --certificate=/somewhere/ca-root.crt https://www.example.com/
+
+or with the python _requests_ module:
+
+    import requests
+    r = requests.get("https://www.example.com", verify="/somewhere/ca-root.crt")
+
+Usage
+-----
+
+### Simple domain
+
+Create a server certificate for domain `www.example.com`:
+
+    ./create_server_certificate.sh www.example.com
+
+Will produce:
+ - `www.example.com.key`
+ - `www.example.com.crt`
+
+
+### Multiple domains 
+
+Create a server certificate for main domain `www.example.com` and alternative domains `example.com`, `foo.com` and `bar.com`:
+
+    ./create_server_certificate.sh www.example.com foo.com bar.com
+
+Will produce:
+ - `www.example.com.key`
+ - `www.example.com.crt`
+ 
+### Wildcard domain
+
+Create a server certificate for wildcard domain `*.example.com`:
+
+    ./create_server_certificate.sh "*.example.com"
+
+Note that you need to use quotes around the domain string or the shell would expand `*`.
+
+Will produce:
+ - `*.example.com.key`
+ - `*.example.com.crt`
+
+Again, to prevent your shell from expanding `*`, use quotes. i.e.: `cat "*.example.com.crt"`.
+
+Such a server certificate would be valid for domains:
+- `foo.example.com` 
+- `bar.example.com`
+
+but not for domains:
+- `example.com`
+- `foo.bar.example.com`
+
+
+### Wildcard domain on multiple levels
+
+While you can technically create a server certificate for wildcard domain `*.example.com` and alternative name `*.*.example.com`, client implementations generally do not support multiple wildcards in a domain name.
+
+For instance, a python script using urllib3 would fail to validate domain `foo.bar.example.com` presenting a certificate with name `*.*.example.com`. It is advised to stay away from producing such certificates.
+
+If you want to give it a try:
+
+    ./create_server_certificate.sh "*.example.com" "*.*.example.com"
+
+Such a server certificate would be valid for domains:
+- `foo.example.com` 
+- `bar.example.com`
+- `foo.bar.example.com`

+ 21 - 0
test/certs/ca-root.crt

@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDZDCCAkygAwIBAgIJAJmW6Ju6iJNNMA0GCSqGSIb3DQEBCwUAMD8xHzAdBgNV
+BAoMFm5naW54LXByb3h5IHRlc3Qgc3VpdGUxHDAaBgNVBAMME3d3dy5uZ2lueC1w
+cm94eS50bGQwHhcNMTcwMTEwMDAwODUxWhcNMjcwMTA4MDAwODUxWjA/MR8wHQYD
+VQQKDBZuZ2lueC1wcm94eSB0ZXN0IHN1aXRlMRwwGgYDVQQDDBN3d3cubmdpbngt
+cHJveHkudGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAndjE3OPr
+48hIOQigk/HejrowsQDLNfkkc6vej0J983rJitGTgBfxqq27fOPfqhE5bi1M5JDk
+KkrOrSitxCJLgpq+4Ls9/RXg8skZFHRAQbNwuKBehaDkPdamJ0i3dv6e4kZy41oI
+RqxQ/MKdminC4LShFZvPoKeh9ae7w1MgB2/4E68LO66bYiHlLNL7ENViSHhLyCmt
+qIE7kdV9jgn2NuVJ37m6/6SNQ3GBiIjEW+ooRQ3HEVKBCismcwq80+BD5VS/yW18
+KqX8m4sBM+IgZbcOqrV+APMbGvd8iNJgQSSQC/r0Wscgt7UeggVYKDazjDSPvLUE
+FUN5wEmydkP2AQIDAQABo2MwYTAdBgNVHQ4EFgQUJL59pHomt+8dUNxv8HgrYjKf
+OA8wHwYDVR0jBBgwFoAUJL59pHomt+8dUNxv8HgrYjKfOA8wDwYDVR0TAQH/BAUw
+AwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggEBABALxY96YqsZ
+CL2crzY0FIGhfjLE7P3mtUGklUpFu7xyI6mGUyL1nJYSnHB5IEV6QLhVVUE/CojI
+crXorQWBDkx26AgCt/eIOdvPYC0JDeXiIhH6sld3yH7JGwGqJkfXaUUfUkuwMae7
+mMIEG9e6vfSh/YNTRxs0KBjBcXHHl5K+Dz4h9r14OqnQFqVFZaR6T6td44tDDNhn
+beW8iIfCWRqDsnvIcJzLa2QR4onmJSw5DaSeFFaKefhdHEzEBZntLfyFbjRYHT/O
++BRdewhg6rSDkGLcL8n/ZnRLOa+xmegjQ/Op94OmWO3TfXOITJAtkaO2YVZoyek8
+T6ckVovq4zU=
+-----END CERTIFICATE-----

+ 27 - 0
test/certs/ca-root.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAndjE3OPr48hIOQigk/HejrowsQDLNfkkc6vej0J983rJitGT
+gBfxqq27fOPfqhE5bi1M5JDkKkrOrSitxCJLgpq+4Ls9/RXg8skZFHRAQbNwuKBe
+haDkPdamJ0i3dv6e4kZy41oIRqxQ/MKdminC4LShFZvPoKeh9ae7w1MgB2/4E68L
+O66bYiHlLNL7ENViSHhLyCmtqIE7kdV9jgn2NuVJ37m6/6SNQ3GBiIjEW+ooRQ3H
+EVKBCismcwq80+BD5VS/yW18KqX8m4sBM+IgZbcOqrV+APMbGvd8iNJgQSSQC/r0
+Wscgt7UeggVYKDazjDSPvLUEFUN5wEmydkP2AQIDAQABAoIBAQCDM4zetix6lx1B
+GuSuVFrTc/vJBInkgQRFiVRi67fZS/R+CJl73WsonWO7+YUNzWdZJxpE2hJs/OUx
+lSBqaL8u/gUuszRhS3BBHdpU4BQRCF/ndpVaqVNN+z78ZDrrE9Vo63nPdCRw6gYf
+MnzhiVjMghdq6Kn6NZwvno45WrzCsIbrrQ4zU+S2PhG8MTA53jzqqQ8mUSJX0lAl
+6b9+1aWA0d0Jnk3M3doaFU/Dlnz3n6kkx0AdqNe8bdsFrPfwsrF+dwGx04SGgLmK
+V2OjIDFYYGtiHp3PJ9IYIA32ij+UloSDDZ2BxXkma8Zilw04ytY5l8tlk2ZDWTD9
+U2MXxjmBAoGBAMmmI19I/asTPjljlqzrOsrdRkklJvnCHgy/yw9u3nMfkJ0lLGAp
+mZoCqJIEsAqlLGM5bOjKy3KQ3n2SBX3mz7/RajnpJRTnNLeJIPAAXHN9TDyKcWRo
+Los6xHN7YMSLYKs4HMihXp9Yu4Ms88/8nO/01nufjN0rTgFnWdL0WfxJAoGBAMhk
+Qm92ukMmbrXSrV0WF+eFooHwgPmUWZ1oZY5ZHmO3FCuSBHiICGrWKmdbcG6H5zmZ
+oFZ0unsvk2Yjl+/+tntxr/dwp6Q+chsqkLms8GE76NWEO8qn4hQNywkFgpKlPci3
+n5IqpuQ2DpJ1PAQkwgZD/5rSscNidNMezXO5Uvv5AoGBALR291kjXcJpKlr6AbMn
+oipD9c8obMVBMNuAGh7pvjORoD7DMf+tu0XV8z8a6uHcCOmUTx/XvlP9yuDeegO/
+OVYV+NdzDDi04r0PAGdKK3NAQ6Y60Fhn1J/OLFqdpHDBu/X/9eKoaKJ7KvWumVUe
+YuVtXTauB8c4JkujTwQ4ov/hAoGAHxvhbGhkFhSbT0K7gx3w7BJE3iM2AojTOKqC
+SYzwOM6tJO5wHz4PAHbq8kyxsZcLgFenGoTYhlMmcM7JwYorThKiHKmyfL7s++ap
+vQlp785bIPp8RcO2RyK1CFuAn79jTgujjA9vBTKXJIlqncIPFOXtgl1/FzPrqvK3
+NmXoyhECgYEAje9hM9RYO0jbfmTZoQh+onMRz34SM9XWLH+NQGgfvsGtjeRnrUKK
+GuWQz/GQGJLy/Uc1KHIdrfPDjvQhZXmPL1v7pNfCrqyj+EnKCNDPPnYq5Zq4WLsB
+x1hKPH0LmfEBkXOiFGrD3h3KAuBK5nb0/EFBDR4JuMaySC5CpbOds9o=
+-----END RSA PRIVATE KEY-----

+ 183 - 0
test/certs/create_server_certificate.sh

@@ -0,0 +1,183 @@
+#!/bin/bash
+set -u
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+if [[ "$#" -eq 0 ]]; then
+	cat <<-EOF
+
+	To generate a server certificate, provide the domain name as a parameter:
+	    $(basename $0) www.my-domain.tdl
+	    $(basename $0) www.my-domain.tdl alternate.domain.tld
+
+	You can also create certificates for wildcard domains:
+	    $(basename $0) '*.my-domain.tdl'
+	
+	EOF
+	exit 0
+else
+	DOMAIN="$1"
+	ALTERNATE_DOMAINS="DNS:$( echo "$@" | sed 's/ /,DNS:/g')"
+fi
+
+
+###############################################################################
+# Create a nginx container (which conveniently provides the `openssl` command)
+###############################################################################
+
+CONTAINER=$(docker run -d -v $DIR:/work -w /work -e SAN="$ALTERNATE_DOMAINS" nginx:1.11.8)
+# Configure openssl 
+docker exec $CONTAINER bash -c '
+	mkdir -p /ca/{certs,crl,private,newcerts} 2>/dev/null
+	echo 1000 > /ca/serial
+	touch /ca/index.txt
+	cat > /ca/openssl.cnf <<-"OESCRIPT"
+		[ ca ]
+		# `man ca`
+		default_ca = CA_default
+
+		[ CA_default ]
+		# Directory and file locations.
+		dir               = /ca
+		certs             = $dir/certs
+		crl_dir           = $dir/crl
+		new_certs_dir     = $dir/newcerts
+		database          = $dir/index.txt
+		serial            = $dir/serial
+		RANDFILE          = $dir/private/.rand
+
+		# The root key and root certificate.
+		private_key       = /work/ca-root.key
+		certificate       = /work/ca-root.crt
+
+		# SHA-1 is deprecated, so use SHA-2 instead.
+		default_md        = sha256
+
+		name_opt          = ca_default
+		cert_opt          = ca_default
+		default_days      = 10000
+		preserve          = no
+		policy            = policy_loose
+
+		[ policy_loose ]
+		countryName             = optional
+		stateOrProvinceName     = optional
+		localityName            = optional
+		organizationName        = optional
+		organizationalUnitName  = optional
+		commonName              = supplied
+		emailAddress            = optional
+
+		[ req ]
+		# Options for the `req` tool (`man req`).
+		default_bits        = 2048
+		distinguished_name  = req_distinguished_name
+		string_mask         = utf8only
+
+		# SHA-1 is deprecated, so use SHA-2 instead.
+		default_md          = sha256
+
+		# Extension to add when the -x509 option is used.
+		x509_extensions     = v3_ca
+
+		[ req_distinguished_name ]
+		# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
+		countryName                     = Country Name (2 letter code)
+		stateOrProvinceName             = State or Province Name
+		localityName                    = Locality Name
+		0.organizationName              = Organization Name
+		organizationalUnitName          = Organizational Unit Name
+		commonName                      = Common Name
+		emailAddress                    = Email Address
+
+		[ v3_ca ]
+		# Extensions for a typical CA (`man x509v3_config`).
+		subjectKeyIdentifier = hash
+		authorityKeyIdentifier = keyid:always,issuer
+		basicConstraints = critical, CA:true
+		keyUsage = critical, digitalSignature, cRLSign, keyCertSign
+
+		[ server_cert ]
+		# Extensions for server certificates (`man x509v3_config`).
+		basicConstraints = CA:FALSE
+		nsCertType = server
+		nsComment = server certificate generated for test purpose (nginx-proxy test suite)
+		subjectKeyIdentifier = hash
+		authorityKeyIdentifier = keyid,issuer:always
+		keyUsage = critical, digitalSignature, keyEncipherment
+		extendedKeyUsage = serverAuth
+
+		[ san_env ]
+		subjectAltName=${ENV::SAN}
+	OESCRIPT
+'
+
+# shortcut for calling `openssl` inside the container
+function openssl {
+	docker exec $CONTAINER openssl "$@"
+}
+
+function exitfail {
+		echo 
+		echo ERROR: "$@"
+		docker rm -f $CONTAINER
+		exit 1
+}
+
+
+###############################################################################
+# Setup Certificate authority
+###############################################################################
+
+if ! [[ -f "$DIR/ca-root.key" ]]; then
+	echo 
+	echo "> Create a Certificate Authority root key: $DIR/ca-root.key"
+	openssl genrsa -out ca-root.key 2048
+	[[ $? -eq 0 ]] || exitfail failed to generate CA root key
+fi
+
+# Create a CA root certificate 
+if ! [[ -f "$DIR/ca-root.crt" ]]; then
+	echo 
+	echo "> Create a CA root certificate: $DIR/ca-root.crt"
+	openssl req -config /ca/openssl.cnf \
+	-key ca-root.key \
+	-new -x509 -days 3650 -subj "/O=nginx-proxy test suite/CN=www.nginx-proxy.tld" -extensions v3_ca \
+	-out ca-root.crt
+	[[ $? -eq 0 ]] || exitfail failed to generate CA root certificate
+
+	# Verify certificate
+	openssl x509 -noout -text -in ca-root.crt
+fi
+
+
+###############################################################################
+# create server key and certificate signed by the certificate authority
+###############################################################################
+
+echo 
+echo "> Create a host key: $DIR/$DOMAIN.key"
+openssl genrsa -out "$DOMAIN.key" 2048
+
+echo 
+echo "> Create a host certificate signing request"
+
+SAN="$ALTERNATE_DOMAINS" openssl req -config /ca/openssl.cnf \
+	-key "$DOMAIN.key" \
+	-new -out "/ca/$DOMAIN.csr" -days 1000 -extensions san_env -subj "/CN=$DOMAIN" 
+	[[ $? -eq 0 ]] || exitfail failed to generate server certificate signing request
+
+echo 
+echo "> Create server certificate: $DIR/$DOMAIN.crt"
+SAN="$ALTERNATE_DOMAINS" openssl ca -config /ca/openssl.cnf -batch \
+		-extensions server_cert \
+		-extensions san_env \
+		-in "/ca/$DOMAIN.csr" \
+		-out "$DOMAIN.crt" 
+	[[ $? -eq 0 ]] || exitfail failed to generate server certificate
+
+
+# Verify host certificate
+#openssl x509 -noout -text -in "$DOMAIN.crt" 
+
+
+docker rm -f $CONTAINER >/dev/null

+ 461 - 0
test/conftest.py

@@ -0,0 +1,461 @@
+from __future__ import print_function
+import contextlib
+import logging
+import os
+import shlex
+import socket
+import subprocess
+import time
+import re
+
+import backoff
+import docker
+import pytest
+import requests
+from _pytest._code.code import ReprExceptionInfo
+from requests.packages.urllib3.util.connection import HAS_IPV6
+
+logging.basicConfig(level=logging.INFO)
+logging.getLogger('backoff').setLevel(logging.INFO)
+logging.getLogger('DNS').setLevel(logging.DEBUG)
+logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.WARN)
+
+CA_ROOT_CERTIFICATE = os.path.join(os.path.dirname(__file__), 'certs/ca-root.crt')
+I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER = os.path.isfile("/.dockerenv")
+FORCE_CONTAINER_IPV6 = False  # ugly global state to consider containers' IPv6 address instead of IPv4
+
+
+docker_client = docker.from_env()
+
+
+###############################################################################
+# 
+# utilities
+# 
+###############################################################################
+
+@contextlib.contextmanager
+def ipv6(force_ipv6=True):
+    """
+    Meant to be used as a context manager to force IPv6 sockets:
+
+        with ipv6():
+            nginxproxy.get("http://something.nginx-proxy.local")  # force use of IPv6
+
+        with ipv6(False):
+            nginxproxy.get("http://something.nginx-proxy.local")  # legacy behavior
+
+
+    """
+    global FORCE_CONTAINER_IPV6
+    FORCE_CONTAINER_IPV6 = force_ipv6
+    yield
+    FORCE_CONTAINER_IPV6 = False
+
+
+class requests_for_docker(object):
+    """
+    Proxy for calling methods of the requests module. 
+    When a HTTP response failed due to HTTP Error 404 or 502, retry a few times.
+    Provides method `get_conf` to extract the nginx-proxy configuration content.
+    """
+    def __init__(self):
+        self.session = requests.Session()
+        if os.path.isfile(CA_ROOT_CERTIFICATE):
+            self.session.verify = CA_ROOT_CERTIFICATE
+
+    def get_conf(self):
+        """
+        Return the nginx config file
+        """
+        nginx_proxy_containers = docker_client.containers.list(filters={"ancestor": "jwilder/nginx-proxy:test"})
+        if len(nginx_proxy_containers) > 1:
+            pytest.fail("Too many running jwilder/nginx-proxy:test containers", pytrace=False)
+        elif len(nginx_proxy_containers) == 0:
+            pytest.fail("No running jwilder/nginx-proxy:test container", pytrace=False)
+        return get_nginx_conf_from_container(nginx_proxy_containers[0])
+
+    def get(self, *args, **kwargs):
+        with ipv6(kwargs.pop('ipv6', False)):
+            @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
+            def _get(*args, **kwargs):
+                return self.session.get(*args, **kwargs)
+            return _get(*args, **kwargs)
+
+    def post(self, *args, **kwargs):
+        with ipv6(kwargs.pop('ipv6', False)):
+            @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
+            def _post(*args, **kwargs):
+                return self.session.post(*args, **kwargs)
+            return _post(*args, **kwargs)
+
+    def put(self, *args, **kwargs):
+        with ipv6(kwargs.pop('ipv6', False)):
+            @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
+            def _put(*args, **kwargs):
+                return self.session.put(*args, **kwargs)
+            return _put(*args, **kwargs)
+
+    def head(self, *args, **kwargs):
+        with ipv6(kwargs.pop('ipv6', False)):
+            @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
+            def _head(*args, **kwargs):
+                return self.session.head(*args, **kwargs)
+            return _head(*args, **kwargs)
+
+    def delete(self, *args, **kwargs):
+        with ipv6(kwargs.pop('ipv6', False)):
+            @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
+            def _delete(*args, **kwargs):
+                return self.session.delete(*args, **kwargs)
+            return _delete(*args, **kwargs)
+
+    def options(self, *args, **kwargs):
+        with ipv6(kwargs.pop('ipv6', False)):
+            @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
+            def _options(*args, **kwargs):
+                return self.session.options(*args, **kwargs)
+            return _options(*args, **kwargs)
+
+    def __getattr__(self, name):
+        return getattr(requests, name)
+
+
+def container_ip(container):
+    """
+    return the IP address of a container.
+
+    If the global FORCE_CONTAINER_IPV6 flag is set, return the IPv6 address
+    """
+    global FORCE_CONTAINER_IPV6
+    if FORCE_CONTAINER_IPV6:
+        if not HAS_IPV6:
+            pytest.skip("This system does not support IPv6")
+        ip = container_ipv6(container)
+        if ip == '':
+            pytest.skip("Container %s has no IPv6 address" % container.name)
+        else:
+            return ip
+    else:
+        net_info = container.attrs["NetworkSettings"]["Networks"]
+        if "bridge" in net_info:
+            return net_info["bridge"]["IPAddress"]
+
+        # not default bridge network, fallback on first network defined
+        network_name = net_info.keys()[0]
+        return net_info[network_name]["IPAddress"]
+
+
+def container_ipv6(container):
+    """
+    return the IPv6 address of a container.
+    """
+    net_info = container.attrs["NetworkSettings"]["Networks"]
+    if "bridge" in net_info:
+        return net_info["bridge"]["GlobalIPv6Address"]
+
+    # not default bridge network, fallback on first network defined
+    network_name = net_info.keys()[0]
+    return net_info[network_name]["GlobalIPv6Address"]
+
+
+def nginx_proxy_dns_resolver(domain_name):
+    """
+    if "nginx-proxy" if found in host, return the ip address of the docker container
+    issued from the docker image jwilder/nginx-proxy:test.
+
+    :return: IP or None
+    """
+    log = logging.getLogger('DNS')
+    log.debug("nginx_proxy_dns_resolver(%r)" % domain_name)
+    if 'nginx-proxy' in domain_name:
+        nginxproxy_containers = docker_client.containers.list(filters={"status": "running", "ancestor": "jwilder/nginx-proxy:test"})
+        if len(nginxproxy_containers) == 0:
+            log.warn("no container found from image jwilder/nginx-proxy:test while resolving %r", domain_name)
+            return
+        nginxproxy_container = nginxproxy_containers[0]
+        ip = container_ip(nginxproxy_container)
+        log.info("resolving domain name %r as IP address %s of nginx-proxy container %s" % (domain_name, ip, nginxproxy_container.name))
+        return ip
+
+def docker_container_dns_resolver(domain_name):
+    """
+    if domain name is of the form "XXX.container.docker" or "anything.XXX.container.docker", return the ip address of the docker container
+    named XXX.
+
+    :return: IP or None
+    """
+    log = logging.getLogger('DNS')
+    log.debug("docker_container_dns_resolver(%r)" % domain_name)
+
+    match = re.search('(^|.+\.)(?P<container>[^.]+)\.container\.docker$', domain_name)
+    if not match:
+        log.debug("%r does not match" % domain_name)
+        return
+
+    container_name = match.group('container')
+    log.debug("looking for container %r" % container_name)
+    try:
+        container = docker_client.containers.get(container_name)
+    except docker.errors.NotFound:
+        log.warn("container named %r not found while resolving %r" % (container_name, domain_name))
+        return
+    log.debug("container %r found (%s)" % (container.name, container.short_id))
+
+    ip = container_ip(container)
+    log.info("resolving domain name %r as IP address %s of container %s" % (domain_name, ip, container.name))
+    return ip 
+
+
+def monkey_patch_urllib_dns_resolver():
+    """
+    Alter the behavior of the urllib DNS resolver so that any domain name
+    containing substring 'nginx-proxy' will resolve to the IP address
+    of the container created from image 'jwilder/nginx-proxy:test'.
+    """
+    prv_getaddrinfo = socket.getaddrinfo
+    dns_cache = {}
+    def new_getaddrinfo(*args):
+        logging.getLogger('DNS').debug("resolving domain name %s" % repr(args))
+        _args = list(args)
+
+        # custom DNS resolvers
+        ip = nginx_proxy_dns_resolver(args[0])
+        if ip is None:
+            ip = docker_container_dns_resolver(args[0])
+        if ip is not None:
+            _args[0] = ip
+
+        # call on original DNS resolver, with eventually the original host changed to the wanted IP address
+        try:
+            return dns_cache[tuple(_args)]
+        except KeyError:
+            res = prv_getaddrinfo(*_args)
+            dns_cache[tuple(_args)] = res
+            return res
+    socket.getaddrinfo = new_getaddrinfo
+    return prv_getaddrinfo
+
+def restore_urllib_dns_resolver(getaddrinfo_func):
+    socket.getaddrinfo = getaddrinfo_func
+
+
+def remove_all_containers():
+    for container in docker_client.containers.list(all=True):
+        if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER and container.id.startswith(socket.gethostname()):
+            continue  # pytest is running within a Docker container, so we do not want to remove that particular container
+        logging.info("removing container %s" % container.name)
+        container.remove(v=True, force=True)
+
+
+def get_nginx_conf_from_container(container):
+    """
+    return the nginx /etc/nginx/conf.d/default.conf file content from a container
+    """
+    import tarfile
+    from cStringIO import StringIO
+    strm, stat = container.get_archive('/etc/nginx/conf.d/default.conf')
+    with tarfile.open(fileobj=StringIO(strm.read())) as tf:
+        conffile = tf.extractfile('default.conf')
+    return conffile.read()
+
+
+def docker_compose_up(compose_file='docker-compose.yml'):
+    logging.info('docker-compose -f %s up -d' % compose_file)
+    try:
+        subprocess.check_output(shlex.split('docker-compose -f %s up -d' % compose_file), stderr=subprocess.STDOUT)
+    except subprocess.CalledProcessError, e:
+        pytest.fail("Error while runninng 'docker-compose -f %s up -d':\n%s" % (compose_file, e.output), pytrace=False)
+
+
+def docker_compose_down(compose_file='docker-compose.yml'):
+    logging.info('docker-compose -f %s down' % compose_file)
+    try:
+        subprocess.check_output(shlex.split('docker-compose -f %s down' % compose_file), stderr=subprocess.STDOUT)
+    except subprocess.CalledProcessError, e:
+        pytest.fail("Error while runninng 'docker-compose -f %s down':\n%s" % (compose_file, e.output), pytrace=False)
+
+
+def wait_for_nginxproxy_to_be_ready():
+    """
+    If one (and only one) container started from image jwilder/nginx-proxy:test is found, 
+    wait for its log to contain substring "Watching docker events"
+    """
+    containers = docker_client.containers.list(filters={"ancestor": "jwilder/nginx-proxy:test"})
+    if len(containers) != 1:
+        return
+    container = containers[0]
+    for line in container.logs(stream=True):
+        if "Watching docker events" in line:
+            logging.debug("nginx-proxy ready")
+            break
+
+def find_docker_compose_file(request):
+    """
+    helper for fixture functions to figure out the name of the docker-compose file to consider.
+
+    - if the test module provides a `docker_compose_file` variable, take that
+    - else, if a yaml file exists with the same name as the test module (but for the `.yml` extension), use that
+    - otherwise use `docker-compose.yml`.
+    """
+    test_module_dir = os.path.dirname(request.module.__file__)
+    yml_file = os.path.join(test_module_dir, request.module.__name__ + '.yml')
+    yaml_file = os.path.join(test_module_dir, request.module.__name__ + '.yaml')
+    default_file = os.path.join(test_module_dir, 'docker-compose.yml')
+
+    docker_compose_file_module_variable = getattr(request.module, "docker_compose_file", None)
+    if docker_compose_file_module_variable is not None:
+        docker_compose_file = os.path.join( test_module_dir, docker_compose_file_module_variable)
+        if not os.path.isfile(docker_compose_file):
+            raise ValueError("docker compose file %r could not be found. Check your test module `docker_compose_file` variable value." % docker_compose_file)
+    else:
+        if os.path.isfile(yml_file):
+            docker_compose_file = yml_file
+        elif os.path.isfile(yaml_file):
+            docker_compose_file = yaml_file
+        else:
+            docker_compose_file = default_file
+
+    if not os.path.isfile(docker_compose_file):
+        logging.error("Could not find any docker-compose file named either '{0}.yml', '{0}.yaml' or 'docker-compose.yml'".format(request.module.__name__))
+
+    logging.debug("using docker compose file %s" % docker_compose_file)
+    return docker_compose_file
+
+
+def connect_to_network(network):
+    """
+    If we are running from a container, connect our container to the given network
+
+    :return: the name of the network we were connected to, or None
+    """
+    if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER:
+        try:
+            my_container = docker_client.containers.get(socket.gethostname())
+        except docker.errors.NotFound:
+            logging.warn("container %r not found" % socket.gethostname())
+            return
+
+        # figure out our container networks
+        my_networks = my_container.attrs["NetworkSettings"]["Networks"].keys()
+
+        # make sure our container is connected to the nginx-proxy's network
+        if network not in my_networks:
+            logging.info("Connecting to docker network: %s" % network.name)
+            network.connect(my_container)
+            return network
+
+
+def disconnect_from_network(network=None):
+    """
+    If we are running from a container, disconnect our container from the given network.
+
+    :param network: name of a docker network to disconnect from
+    """
+    if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER and network is not None:
+        try:
+            my_container = docker_client.containers.get(socket.gethostname())
+        except docker.errors.NotFound:
+            logging.warn("container %r not found" % socket.gethostname())
+            return
+
+        # figure out our container networks
+        my_networks_names = my_container.attrs["NetworkSettings"]["Networks"].keys()
+
+        # disconnect our container from the given network
+        if network.name in my_networks_names:
+            logging.info("Disconnecting from network %s" % network.name)
+            network.disconnect(my_container)
+
+
+def connect_to_all_networks():
+    """
+    If we are running from a container, connect our container to all current docker networks.
+
+    :return: a list of networks we connected to
+    """
+    if not I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER:
+        return []
+    else:
+        # find the list of docker networks
+        networks = filter(lambda network: len(network.containers) > 0 and network.name != 'bridge', docker_client.networks.list())
+        return [connect_to_network(network) for network in networks]
+
+
+###############################################################################
+# 
+# Py.test fixtures
+# 
+###############################################################################
+
+@pytest.yield_fixture(scope="module")
+def docker_compose(request):
+    """
+    pytest fixture providing containers described in a docker compose file. After the tests, remove the created containers
+    
+    A custom docker compose file name can be defined in a variable named `docker_compose_file`.
+    
+    Also, in the case where pytest is running from a docker container, this fixture makes sure
+    our container will be attached to all the docker networks.
+    """
+    docker_compose_file = find_docker_compose_file(request)
+    original_dns_resolver = monkey_patch_urllib_dns_resolver()
+    remove_all_containers()
+    docker_compose_up(docker_compose_file)
+    networks = connect_to_all_networks()
+    wait_for_nginxproxy_to_be_ready()
+    time.sleep(3)  # give time to containers to be ready
+    yield docker_client
+    for network in networks:
+        disconnect_from_network(network)
+    docker_compose_down(docker_compose_file)
+    restore_urllib_dns_resolver(original_dns_resolver)
+
+
+@pytest.yield_fixture()
+def nginxproxy():
+    """
+    Provides the `nginxproxy` object that can be used in the same way the requests module is:
+
+    r = nginxproxy.get("http://foo.com")
+
+    The difference is that in case an HTTP requests has status code 404 or 502 (which mostly
+    indicates that nginx has just reloaded), we retry up to 30 times the query.
+
+    Also, the nginxproxy methods accept an additional keyword parameter: `ipv6` which forces requests
+    made against containers to use the containers IPv6 address when set to `True`. If IPv6 is not
+    supported by the system or docker, that particular test will be skipped.
+    """
+    yield requests_for_docker()
+
+
+###############################################################################
+# 
+# Py.test hooks
+# 
+###############################################################################
+
+# pytest hook to display additionnal stuff in test report
+def pytest_runtest_logreport(report):
+    if report.failed:
+        if isinstance(report.longrepr, ReprExceptionInfo):
+            test_containers = docker_client.containers.list(all=True, filters={"ancestor": "jwilder/nginx-proxy:test"})
+            for container in test_containers:
+                report.longrepr.addsection('nginx-proxy logs', container.logs())
+                report.longrepr.addsection('nginx-proxy conf', get_nginx_conf_from_container(container))
+
+
+
+###############################################################################
+# 
+# Check requirements
+# 
+###############################################################################
+
+try:
+    docker_client.images.get('jwilder/nginx-proxy:test')
+except docker.errors.ImageNotFound:
+    pytest.exit("The docker image 'jwilder/nginx-proxy:test' is missing")
+
+if docker.__version__ != "2.0.2":
+    pytest.exit("This test suite is meant to work with the python docker module v2.0.2")

+ 0 - 33
test/default-host.bats

@@ -1,33 +0,0 @@
-#!/usr/bin/env bats
-load test_helpers
-
-function setup {
-	# make sure to stop any web container before each test so we don't
-	# have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set
-	stop_bats_containers web
-}
-
-
-@test "[$TEST_FILE] DEFAULT_HOST=web1.bats" {
-	SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-1
-
-	# GIVEN a webserver with VIRTUAL_HOST set to web.bats
-	prepare_web_container bats-web 80 -e VIRTUAL_HOST=web.bats
-
-	# WHEN nginx-proxy runs with DEFAULT_HOST set to web.bats
-	run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro -e DEFAULT_HOST=web.bats
-	assert_success
-	docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
-
-	# THEN querying the proxy without Host header → 200
-	run curl_container $SUT_CONTAINER / --head
-	assert_output -l 0 $'HTTP/1.1 200 OK\r'
-
-	# THEN querying the proxy with any other Host header → 200
-	run curl_container $SUT_CONTAINER / --head --header "Host: something.I.just.made.up"
-	assert_output -l 0 $'HTTP/1.1 200 OK\r'
-}
-
-@test "[$TEST_FILE] stop all bats containers" {
-	stop_bats_containers
-}

+ 0 - 123
test/docker.bats

@@ -1,123 +0,0 @@
-#!/usr/bin/env bats
-load test_helpers
-
-
-@test "[$TEST_FILE] start 2 web containers" {
-	prepare_web_container bats-web1 81 -e VIRTUAL_HOST=web1.bats
-	prepare_web_container bats-web2 82 -e VIRTUAL_HOST=web2.bats
-}
-
-
-@test "[$TEST_FILE] -v /var/run/docker.sock:/tmp/docker.sock:ro" {
-	SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-1
-
-	# WHEN nginx-proxy runs on our docker host using the default unix socket
-	run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro
-	assert_success
-	docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
-
-	# THEN
-	assert_nginxproxy_behaves $SUT_CONTAINER
-}
-
-
-@test "[$TEST_FILE] -v /var/run/docker.sock:/f00.sock:ro -e DOCKER_HOST=unix:///f00.sock" {
-	SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-2
-
-	# WHEN nginx-proxy runs on our docker host using a custom unix socket
-	run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/f00.sock:ro -e DOCKER_HOST=unix:///f00.sock
-	assert_success
-	docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
-
-	# THEN
-	assert_nginxproxy_behaves $SUT_CONTAINER
-}
-
-
-@test "[$TEST_FILE] -e DOCKER_HOST=tcp://..." {
-	SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-3
-	# GIVEN a container exposing our docker host over TCP
-	run docker_tcp bats-docker-tcp
-	assert_success
-	sleep 1s
-
-	# WHEN nginx-proxy runs on our docker host using tcp to connect to our docker host
-	run nginxproxy $SUT_CONTAINER -e DOCKER_HOST="tcp://bats-docker-tcp:2375" --link bats-docker-tcp:bats-docker-tcp
-	assert_success
-	docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
-
-	# THEN
-	assert_nginxproxy_behaves $SUT_CONTAINER
-}
-
-
-@test "[$TEST_FILE] separated containers (nginx + docker-gen + nginx.tmpl)" {
-	docker_clean bats-nginx
-	docker_clean bats-docker-gen
-
-	# GIVEN a simple nginx container
-	run docker run -d \
-		--label bats-type="nginx" \
-		--name bats-nginx \
-		-v /etc/nginx/conf.d/ \
-		-v /etc/nginx/certs/ \
-		nginx:latest
-	assert_success
-	run retry 5 1s docker run --label bats-type="curl" appropriate/curl --silent --fail --head http://$(docker_ip bats-nginx)/
-	assert_output -l 0 $'HTTP/1.1 200 OK\r'
-
-	# WHEN docker-gen runs on our docker host
-	run docker run -d \
-		--label bats-type="docker-gen" \
-		--name bats-docker-gen \
-		-v /var/run/docker.sock:/tmp/docker.sock:ro \
-		-v $BATS_TEST_DIRNAME/../nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro \
-		--volumes-from bats-nginx \
-		--expose 80 \
-		jwilder/docker-gen:0.7.3 \
-			-notify-sighup bats-nginx \
-			-watch \
-			-only-exposed \
-			/etc/docker-gen/templates/nginx.tmpl \
-			/etc/nginx/conf.d/default.conf
-	assert_success
-	docker_wait_for_log bats-docker-gen 9 "Watching docker events"
-
-	# Give some time to the docker-gen container to notify bats-nginx so it
-	# reloads its config
-	sleep 2s
-
-	run docker_running_state bats-nginx
-	assert_output "true" || {
-		docker logs bats-docker-gen
-		false
-	} >&2
-
-	# THEN
-	assert_nginxproxy_behaves bats-nginx
-}
-
-@test "[$TEST_FILE] stop all bats containers" {
-	stop_bats_containers
-}
-
-
-# $1 nginx-proxy container
-function assert_nginxproxy_behaves {
-	local -r container=$1
-
-	# Querying the proxy without Host header → 503
-	run curl_container $container / --head
-	assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
-
-	# Querying the proxy with Host header → 200
-	run curl_container $container /port --header "Host: web1.bats"
-	assert_output "answer from port 81"
-
-	run curl_container $container /port --header "Host: web2.bats"
-	assert_output "answer from port 82"
-
-	# Querying the proxy with unknown Host header → 503
-	run curl_container $container /port --header "Host: webFOO.bats" --head
-	assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
-}

+ 0 - 139
test/headers.bats

@@ -1,139 +0,0 @@
-#!/usr/bin/env bats
-load test_helpers
-SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}
-
-function setup {
-	# make sure to stop any web container before each test so we don't
-	# have any unexpected container running with VIRTUAL_HOST or VIRUTAL_PORT set
-	stop_bats_containers web
-}
-
-
-@test "[$TEST_FILE] start a nginx-proxy container" {
-	# GIVEN
-	run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro
-	assert_success
-	docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
-}
-
-@test "[$TEST_FILE] nginx-proxy passes arbitrary header" {
-	# WHEN
-	prepare_web_container bats-host-1 80 -e VIRTUAL_HOST=web.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-host-1
-	sleep 1
-
-	# THEN
-	run curl_container $SUT_CONTAINER /headers -H "Foo: Bar" -H "Host: web.bats"
-	assert_output -l 'Foo: Bar'
-}
-
-##### Testing the handling of X-Forwarded-For #####
-
-@test "[$TEST_FILE] nginx-proxy generates X-Forwarded-For" {
-	# WHEN
-	prepare_web_container bats-host-2 80 -e VIRTUAL_HOST=web.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-host-2
-	sleep 1
-
-	# THEN
-	run curl_container $SUT_CONTAINER /headers -H "Host: web.bats"
-	assert_output -p 'X-Forwarded-For:'
-}
-
-@test "[$TEST_FILE] nginx-proxy passes X-Forwarded-For" {
-	# WHEN
-	prepare_web_container bats-host-3 80 -e VIRTUAL_HOST=web.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-host-3
-	sleep 1
-
-	# THEN
-	run curl_container $SUT_CONTAINER /headers -H "X-Forwarded-For: 1.2.3.4" -H "Host: web.bats"
-	assert_output -p 'X-Forwarded-For: 1.2.3.4, '
-}
-
-##### Testing the handling of X-Forwarded-Proto #####
-
-@test "[$TEST_FILE] nginx-proxy generates X-Forwarded-Proto" {
-	# WHEN
-	prepare_web_container bats-host-4 80 -e VIRTUAL_HOST=web.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-host-4
-	sleep 1
-
-	# THEN
-	run curl_container $SUT_CONTAINER /headers -H "Host: web.bats"
-	assert_output -l 'X-Forwarded-Proto: http'
-}
-
-@test "[$TEST_FILE] nginx-proxy passes X-Forwarded-Proto" {
-	# WHEN
-	prepare_web_container bats-host-5 80 -e VIRTUAL_HOST=web.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-host-5
-	sleep 1
-
-	# THEN
-	run curl_container $SUT_CONTAINER /headers -H "X-Forwarded-Proto: https" -H "Host: web.bats"
-	assert_output -l 'X-Forwarded-Proto: https'
-}
-
-##### Testing the handling of X-Forwarded-Port #####
-
-@test "[$TEST_FILE] nginx-proxy generates X-Forwarded-Port" {
-	# WHEN
-	prepare_web_container bats-host-6 80 -e VIRTUAL_HOST=web.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-host-6
-	sleep 1
-
-	# THEN
-	run curl_container $SUT_CONTAINER /headers -H "Host: web.bats"
-	assert_output -l 'X-Forwarded-Port: 80'
-}
-
-@test "[$TEST_FILE] nginx-proxy passes X-Forwarded-Port" {
-	# WHEN
-	prepare_web_container bats-host-7 80 -e VIRTUAL_HOST=web.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-host-7
-	sleep 1
-
-	# THEN
-	run curl_container $SUT_CONTAINER /headers -H "X-Forwarded-Port: 1234" -H "Host: web.bats"
-	assert_output -l 'X-Forwarded-Port: 1234'
-}
-
-##### Other headers
-
-@test "[$TEST_FILE] nginx-proxy generates X-Real-IP" {
-	# WHEN
-	prepare_web_container bats-host-8 80 -e VIRTUAL_HOST=web.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-host-8
-	sleep 1
-
-	# THEN
-	run curl_container $SUT_CONTAINER /headers -H "Host: web.bats"
-	assert_output -p 'X-Real-IP: '
-}
-
-@test "[$TEST_FILE] nginx-proxy passes Host" {
-	# WHEN
-	prepare_web_container bats-host-9 80 -e VIRTUAL_HOST=web.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-host-9
-	sleep 1
-
-	# THEN
-	run curl_container $SUT_CONTAINER /headers -H "Host: web.bats"
-	assert_output -l 'Host: web.bats'
-}
-
-@test "[$TEST_FILE] nginx-proxy supresses Proxy for httpoxy protection" {
-	# WHEN
-	prepare_web_container bats-host-10 80 -e VIRTUAL_HOST=web.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-host-10
-	sleep 1
-
-	# THEN
-	run curl_container $SUT_CONTAINER /headers -H "Proxy: tcp://foo.com" -H "Host: web.bats"
-	refute_output -l 'Proxy: tcp://foo.com'
-}
-
-@test "[$TEST_FILE] stop all bats containers" {
-	stop_bats_containers
-}

+ 0 - 6
test/lib/README.md

@@ -1,6 +0,0 @@
-bats lib
-========
-
-found on https://github.com/sstephenson/bats/pull/110
-
-When that pull request will be merged, the `test/lib/bats` won't be necessary anymore.

+ 0 - 596
test/lib/bats/batslib.bash

@@ -1,596 +0,0 @@
-#
-# batslib.bash
-# ------------
-#
-# The Standard Library is a collection of test helpers intended to
-# simplify testing. It contains the following types of test helpers.
-#
-#   - Assertions are functions that perform a test and output relevant
-#     information on failure to help debugging. They return 1 on failure
-#     and 0 otherwise.
-#
-# All output is formatted for readability using the functions of
-# `output.bash' and sent to the standard error.
-#
-
-source "${BATS_LIB}/batslib/output.bash"
-
-
-########################################################################
-#                               ASSERTIONS
-########################################################################
-
-# Fail and display a message. When no parameters are specified, the
-# message is read from the standard input. Other functions use this to
-# report failure.
-#
-# Globals:
-#   none
-# Arguments:
-#   $@ - [=STDIN] message
-# Returns:
-#   1 - always
-# Inputs:
-#   STDIN - [=$@] message
-# Outputs:
-#   STDERR - message
-fail() {
-  (( $# == 0 )) && batslib_err || batslib_err "$@"
-  return 1
-}
-
-# Fail and display details if the expression evaluates to false. Details
-# include the expression, `$status' and `$output'.
-#
-# NOTE: The expression must be a simple command. Compound commands, such
-#       as `[[', can be used only when executed with `bash -c'.
-#
-# Globals:
-#   status
-#   output
-# Arguments:
-#   $1 - expression
-# Returns:
-#   0 - expression evaluates to TRUE
-#   1 - otherwise
-# Outputs:
-#   STDERR - details, on failure
-assert() {
-  if ! "$@"; then
-    { local -ar single=(
-        'expression' "$*"
-        'status'     "$status"
-      )
-      local -ar may_be_multi=(
-        'output'     "$output"
-      )
-      local -ir width="$( batslib_get_max_single_line_key_width \
-                            "${single[@]}" "${may_be_multi[@]}" )"
-      batslib_print_kv_single "$width" "${single[@]}"
-      batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
-    } | batslib_decorate 'assertion failed' \
-      | fail
-  fi
-}
-
-# Fail and display details if the expected and actual values do not
-# equal. Details include both values.
-#
-# Globals:
-#   none
-# Arguments:
-#   $1 - actual value
-#   $2 - expected value
-# Returns:
-#   0 - values equal
-#   1 - otherwise
-# Outputs:
-#   STDERR - details, on failure
-assert_equal() {
-  if [[ $1 != "$2" ]]; then
-    batslib_print_kv_single_or_multi 8 \
-        'expected' "$2" \
-        'actual'   "$1" \
-      | batslib_decorate 'values do not equal' \
-      | fail
-  fi
-}
-
-# Fail and display details if `$status' is not 0. Details include
-# `$status' and `$output'.
-#
-# Globals:
-#   status
-#   output
-# Arguments:
-#   none
-# Returns:
-#   0 - `$status' is 0
-#   1 - otherwise
-# Outputs:
-#   STDERR - details, on failure
-assert_success() {
-  if (( status != 0 )); then
-    { local -ir width=6
-      batslib_print_kv_single "$width" 'status' "$status"
-      batslib_print_kv_single_or_multi "$width" 'output' "$output"
-    } | batslib_decorate 'command failed' \
-      | fail
-  fi
-}
-
-# Fail and display details if `$status' is 0. Details include `$output'.
-#
-# Optionally, when the expected status is specified, fail when it does
-# not equal `$status'. In this case, details include the expected and
-# actual status, and `$output'.
-#
-# Globals:
-#   status
-#   output
-# Arguments:
-#   $1 - [opt] expected status
-# Returns:
-#   0 - `$status' is not 0, or
-#       `$status' equals the expected status
-#   1 - otherwise
-# Outputs:
-#   STDERR - details, on failure
-assert_failure() {
-  (( $# > 0 )) && local -r expected="$1"
-  if (( status == 0 )); then
-    batslib_print_kv_single_or_multi 6 'output' "$output" \
-      | batslib_decorate 'command succeeded, but it was expected to fail' \
-      | fail
-  elif (( $# > 0 )) && (( status != expected )); then
-    { local -ir width=8
-      batslib_print_kv_single "$width" \
-          'expected' "$expected" \
-          'actual'   "$status"
-      batslib_print_kv_single_or_multi "$width" \
-          'output' "$output"
-    } | batslib_decorate 'command failed as expected, but status differs' \
-      | fail
-  fi
-}
-
-# Fail and display details if the expected does not match the actual
-# output or a fragment of it.
-#
-# By default, the entire output is matched. The assertion fails if the
-# expected output does not equal `$output'. Details include both values.
-#
-# When `-l <index>' is used, only the <index>-th line is matched. The
-# assertion fails if the expected line does not equal
-# `${lines[<index>}'. Details include the compared lines and <index>.
-#
-# When `-l' is used without the <index> argument, the output is searched
-# for the expected line. The expected line is matched against each line
-# in `${lines[@]}'. If no match is found the assertion fails. Details
-# include the expected line and `$output'.
-#
-# By default, literal matching is performed. Options `-p' and `-r'
-# enable partial (i.e. substring) and extended regular expression
-# matching, respectively. Specifying an invalid extended regular
-# expression with `-r' displays an error.
-#
-# Options `-p' and `-r' are mutually exclusive. When used
-# simultaneously, an error is displayed.
-#
-# Globals:
-#   output
-#   lines
-# Options:
-#   -l <index> - match against the <index>-th element of `${lines[@]}'
-#   -l - search `${lines[@]}' for the expected line
-#   -p - partial matching
-#   -r - extended regular expression matching
-# Arguments:
-#   $1 - expected output
-# Returns:
-#   0 - expected matches the actual output
-#   1 - otherwise
-# Outputs:
-#   STDERR - details, on failure
-#            error message, on error
-assert_output() {
-  local -i is_match_line=0
-  local -i is_match_contained=0
-  local -i is_mode_partial=0
-  local -i is_mode_regex=0
-
-  # Handle options.
-  while (( $# > 0 )); do
-    case "$1" in
-      -l)
-        if (( $# > 2 )) && [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
-          is_match_line=1
-          local -ri idx="$2"
-          shift
-        else
-          is_match_contained=1;
-        fi
-        shift
-        ;;
-      -p) is_mode_partial=1; shift ;;
-      -r) is_mode_regex=1; shift ;;
-      --) break ;;
-      *) break ;;
-    esac
-  done
-
-  if (( is_match_line )) && (( is_match_contained )); then
-    echo "\`-l' and \`-l <index>' are mutually exclusive" \
-      | batslib_decorate 'ERROR: assert_output' \
-      | fail
-    return $?
-  fi
-
-  if (( is_mode_partial )) && (( is_mode_regex )); then
-    echo "\`-p' and \`-r' are mutually exclusive" \
-      | batslib_decorate 'ERROR: assert_output' \
-      | fail
-    return $?
-  fi
-
-  # Arguments.
-  local -r expected="$1"
-
-  if (( is_mode_regex == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then
-    echo "Invalid extended regular expression: \`$expected'" \
-      | batslib_decorate 'ERROR: assert_output' \
-      | fail
-    return $?
-  fi
-
-  # Matching.
-  if (( is_match_contained )); then
-    # Line contained in output.
-    if (( is_mode_regex )); then
-      local -i idx
-      for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
-        [[ ${lines[$idx]} =~ $expected ]] && return 0
-      done
-      { local -ar single=(
-          'regex'  "$expected"
-        )
-        local -ar may_be_multi=(
-          'output' "$output"
-        )
-        local -ir width="$( batslib_get_max_single_line_key_width \
-                              "${single[@]}" "${may_be_multi[@]}" )"
-        batslib_print_kv_single "$width" "${single[@]}"
-        batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
-      } | batslib_decorate 'no output line matches regular expression' \
-        | fail
-    elif (( is_mode_partial )); then
-      local -i idx
-      for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
-        [[ ${lines[$idx]} == *"$expected"* ]] && return 0
-      done
-      { local -ar single=(
-          'substring' "$expected"
-        )
-        local -ar may_be_multi=(
-          'output'    "$output"
-        )
-        local -ir width="$( batslib_get_max_single_line_key_width \
-                              "${single[@]}" "${may_be_multi[@]}" )"
-        batslib_print_kv_single "$width" "${single[@]}"
-        batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
-      } | batslib_decorate 'no output line contains substring' \
-        | fail
-    else
-      local -i idx
-      for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
-        [[ ${lines[$idx]} == "$expected" ]] && return 0
-      done
-      { local -ar single=(
-          'line'   "$expected"
-        )
-        local -ar may_be_multi=(
-          'output' "$output"
-        )
-        local -ir width="$( batslib_get_max_single_line_key_width \
-                            "${single[@]}" "${may_be_multi[@]}" )"
-        batslib_print_kv_single "$width" "${single[@]}"
-        batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
-      } | batslib_decorate 'output does not contain line' \
-        | fail
-    fi
-  elif (( is_match_line )); then
-    # Specific line.
-    if (( is_mode_regex )); then
-      if ! [[ ${lines[$idx]} =~ $expected ]]; then
-        batslib_print_kv_single 5 \
-            'index' "$idx" \
-            'regex' "$expected" \
-            'line'  "${lines[$idx]}" \
-          | batslib_decorate 'regular expression does not match line' \
-          | fail
-      fi
-    elif (( is_mode_partial )); then
-      if [[ ${lines[$idx]} != *"$expected"* ]]; then
-        batslib_print_kv_single 9 \
-            'index'     "$idx" \
-            'substring' "$expected" \
-            'line'      "${lines[$idx]}" \
-          | batslib_decorate 'line does not contain substring' \
-          | fail
-      fi
-    else
-      if [[ ${lines[$idx]} != "$expected" ]]; then
-        batslib_print_kv_single 8 \
-            'index'    "$idx" \
-            'expected' "$expected" \
-            'actual'   "${lines[$idx]}" \
-          | batslib_decorate 'line differs' \
-          | fail
-      fi
-    fi
-  else
-    # Entire output.
-    if (( is_mode_regex )); then
-      if ! [[ $output =~ $expected ]]; then
-        batslib_print_kv_single_or_multi 6 \
-            'regex'  "$expected" \
-            'output' "$output" \
-          | batslib_decorate 'regular expression does not match output' \
-          | fail
-      fi
-    elif (( is_mode_partial )); then
-      if [[ $output != *"$expected"* ]]; then
-        batslib_print_kv_single_or_multi 9 \
-            'substring' "$expected" \
-            'output'    "$output" \
-          | batslib_decorate 'output does not contain substring' \
-          | fail
-      fi
-    else
-      if [[ $output != "$expected" ]]; then
-        batslib_print_kv_single_or_multi 8 \
-            'expected' "$expected" \
-            'actual'   "$output" \
-          | batslib_decorate 'output differs' \
-          | fail
-      fi
-    fi
-  fi
-}
-
-# Fail and display details if the unexpected matches the actual output
-# or a fragment of it.
-#
-# By default, the entire output is matched. The assertion fails if the
-# unexpected output equals `$output'. Details include `$output'.
-#
-# When `-l <index>' is used, only the <index>-th line is matched. The
-# assertion fails if the unexpected line equals `${lines[<index>}'.
-# Details include the compared line and <index>.
-#
-# When `-l' is used without the <index> argument, the output is searched
-# for the unexpected line. The unexpected line is matched against each
-# line in `${lines[<index>]}'. If a match is found the assertion fails.
-# Details include the unexpected line, the index where it was found and
-# `$output' (with the unexpected line highlighted in it if `$output` is
-# longer than one line).
-#
-# By default, literal matching is performed. Options `-p' and `-r'
-# enable partial (i.e. substring) and extended regular expression
-# matching, respectively. On failure, the substring or the regular
-# expression is added to the details (if not already displayed).
-# Specifying an invalid extended regular expression with `-r' displays
-# an error.
-#
-# Options `-p' and `-r' are mutually exclusive. When used
-# simultaneously, an error is displayed.
-#
-# Globals:
-#   output
-#   lines
-# Options:
-#   -l <index> - match against the <index>-th element of `${lines[@]}'
-#   -l - search `${lines[@]}' for the unexpected line
-#   -p - partial matching
-#   -r - extended regular expression matching
-# Arguments:
-#   $1 - unexpected output
-# Returns:
-#   0 - unexpected matches the actual output
-#   1 - otherwise
-# Outputs:
-#   STDERR - details, on failure
-#            error message, on error
-refute_output() {
-  local -i is_match_line=0
-  local -i is_match_contained=0
-  local -i is_mode_partial=0
-  local -i is_mode_regex=0
-
-  # Handle options.
-  while (( $# > 0 )); do
-    case "$1" in
-      -l)
-        if (( $# > 2 )) && [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
-          is_match_line=1
-          local -ri idx="$2"
-          shift
-        else
-          is_match_contained=1;
-        fi
-        shift
-        ;;
-      -L) is_match_contained=1; shift ;;
-      -p) is_mode_partial=1; shift ;;
-      -r) is_mode_regex=1; shift ;;
-      --) break ;;
-      *) break ;;
-    esac
-  done
-
-  if (( is_match_line )) && (( is_match_contained )); then
-    echo "\`-l' and \`-l <index>' are mutually exclusive" \
-      | batslib_decorate 'ERROR: refute_output' \
-      | fail
-    return $?
-  fi
-
-  if (( is_mode_partial )) && (( is_mode_regex )); then
-    echo "\`-p' and \`-r' are mutually exclusive" \
-      | batslib_decorate 'ERROR: refute_output' \
-      | fail
-    return $?
-  fi
-
-  # Arguments.
-  local -r unexpected="$1"
-
-  if (( is_mode_regex == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then
-    echo "Invalid extended regular expression: \`$unexpected'" \
-      | batslib_decorate 'ERROR: refute_output' \
-      | fail
-    return $?
-  fi
-
-  # Matching.
-  if (( is_match_contained )); then
-    # Line contained in output.
-    if (( is_mode_regex )); then
-      local -i idx
-      for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
-        if [[ ${lines[$idx]} =~ $unexpected ]]; then
-          { local -ar single=(
-              'regex'  "$unexpected"
-              'index'  "$idx"
-            )
-            local -a may_be_multi=(
-              'output' "$output"
-            )
-            local -ir width="$( batslib_get_max_single_line_key_width \
-                                "${single[@]}" "${may_be_multi[@]}" )"
-            batslib_print_kv_single "$width" "${single[@]}"
-            if batslib_is_single_line "${may_be_multi[1]}"; then
-              batslib_print_kv_single "$width" "${may_be_multi[@]}"
-            else
-              may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \
-                                    | batslib_prefix \
-                                    | batslib_mark '>' "$idx" )"
-              batslib_print_kv_multi "${may_be_multi[@]}"
-            fi
-          } | batslib_decorate 'no line should match the regular expression' \
-            | fail
-          return $?
-        fi
-      done
-    elif (( is_mode_partial )); then
-      local -i idx
-      for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
-        if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
-          { local -ar single=(
-              'substring' "$unexpected"
-              'index'     "$idx"
-            )
-            local -a may_be_multi=(
-              'output'    "$output"
-            )
-            local -ir width="$( batslib_get_max_single_line_key_width \
-                                "${single[@]}" "${may_be_multi[@]}" )"
-            batslib_print_kv_single "$width" "${single[@]}"
-            if batslib_is_single_line "${may_be_multi[1]}"; then
-              batslib_print_kv_single "$width" "${may_be_multi[@]}"
-            else
-              may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \
-                                    | batslib_prefix \
-                                    | batslib_mark '>' "$idx" )"
-              batslib_print_kv_multi "${may_be_multi[@]}"
-            fi
-          } | batslib_decorate 'no line should contain substring' \
-            | fail
-          return $?
-        fi
-      done
-    else
-      local -i idx
-      for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
-        if [[ ${lines[$idx]} == "$unexpected" ]]; then
-          { local -ar single=(
-              'line'   "$unexpected"
-              'index'  "$idx"
-            )
-            local -a may_be_multi=(
-              'output' "$output"
-            )
-            local -ir width="$( batslib_get_max_single_line_key_width \
-                                "${single[@]}" "${may_be_multi[@]}" )"
-            batslib_print_kv_single "$width" "${single[@]}"
-            if batslib_is_single_line "${may_be_multi[1]}"; then
-              batslib_print_kv_single "$width" "${may_be_multi[@]}"
-            else
-              may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" \
-                                    | batslib_prefix \
-                                    | batslib_mark '>' "$idx" )"
-              batslib_print_kv_multi "${may_be_multi[@]}"
-            fi
-          } | batslib_decorate 'line should not be in output' \
-            | fail
-          return $?
-        fi
-      done
-    fi
-  elif (( is_match_line )); then
-    # Specific line.
-    if (( is_mode_regex )); then
-      if [[ ${lines[$idx]} =~ $unexpected ]] || (( $? == 0 )); then
-        batslib_print_kv_single 5 \
-            'index' "$idx" \
-            'regex' "$unexpected" \
-            'line'  "${lines[$idx]}" \
-          | batslib_decorate 'regular expression should not match line' \
-          | fail
-      fi
-    elif (( is_mode_partial )); then
-      if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
-        batslib_print_kv_single 9 \
-            'index'     "$idx" \
-            'substring' "$unexpected" \
-            'line'      "${lines[$idx]}" \
-          | batslib_decorate 'line should not contain substring' \
-          | fail
-      fi
-    else
-      if [[ ${lines[$idx]} == "$unexpected" ]]; then
-        batslib_print_kv_single 5 \
-            'index' "$idx" \
-            'line'  "${lines[$idx]}" \
-          | batslib_decorate 'line should differ' \
-          | fail
-      fi
-    fi
-  else
-    # Entire output.
-    if (( is_mode_regex )); then
-      if [[ $output =~ $unexpected ]] || (( $? == 0 )); then
-        batslib_print_kv_single_or_multi 6 \
-            'regex'  "$unexpected" \
-            'output' "$output" \
-          | batslib_decorate 'regular expression should not match output' \
-          | fail
-      fi
-    elif (( is_mode_partial )); then
-      if [[ $output == *"$unexpected"* ]]; then
-        batslib_print_kv_single_or_multi 9 \
-            'substring' "$unexpected" \
-            'output'    "$output" \
-          | batslib_decorate 'output should not contain substring' \
-          | fail
-      fi
-    else
-      if [[ $output == "$unexpected" ]]; then
-        batslib_print_kv_single_or_multi 6 \
-            'output' "$output" \
-          | batslib_decorate 'output equals, but it was expected to differ' \
-          | fail
-      fi
-    fi
-  fi
-}

+ 0 - 264
test/lib/bats/batslib/output.bash

@@ -1,264 +0,0 @@
-#
-# output.bash
-# -----------
-#
-# Private functions implementing output formatting. Used by public
-# helper functions.
-#
-
-# Print a message to the standard error. When no parameters are
-# specified, the message is read from the standard input.
-#
-# Globals:
-#   none
-# Arguments:
-#   $@ - [=STDIN] message
-# Returns:
-#   none
-# Inputs:
-#   STDIN - [=$@] message
-# Outputs:
-#   STDERR - message
-batslib_err() {
-  { if (( $# > 0 )); then
-      echo "$@"
-    else
-      cat -
-    fi
-  } >&2
-}
-
-# Count the number of lines in the given string.
-#
-# TODO(ztombol): Fix tests and remove this note after #93 is resolved!
-# NOTE: Due to a bug in Bats, `batslib_count_lines "$output"' does not
-#       give the same result as `${#lines[@]}' when the output contains
-#       empty lines.
-#       See PR #93 (https://github.com/sstephenson/bats/pull/93).
-#
-# Globals:
-#   none
-# Arguments:
-#   $1 - string
-# Returns:
-#   none
-# Outputs:
-#   STDOUT - number of lines
-batslib_count_lines() {
-  local -i n_lines=0
-  local line
-  while IFS='' read -r line || [[ -n $line ]]; do
-    (( ++n_lines ))
-  done < <(printf '%s' "$1")
-  echo "$n_lines"
-}
-
-# Determine whether all strings are single-line.
-#
-# Globals:
-#   none
-# Arguments:
-#   $@ - strings
-# Returns:
-#   0 - all strings are single-line
-#   1 - otherwise
-batslib_is_single_line() {
-  for string in "$@"; do
-    (( $(batslib_count_lines "$string") > 1 )) && return 1
-  done
-  return 0
-}
-
-# Determine the length of the longest key that has a single-line value.
-#
-# This function is useful in determining the correct width of the key
-# column in two-column format when some keys may have multi-line values
-# and thus should be excluded.
-#
-# Globals:
-#   none
-# Arguments:
-#   $odd - key
-#   $even - value of the previous key
-# Returns:
-#   none
-# Outputs:
-#   STDOUT - length of longest key
-batslib_get_max_single_line_key_width() {
-  local -i max_len=-1
-  while (( $# != 0 )); do
-    local -i key_len="${#1}"
-    batslib_is_single_line "$2" && (( key_len > max_len )) && max_len="$key_len"
-    shift 2
-  done
-  echo "$max_len"
-}
-
-# Print key-value pairs in two-column format.
-#
-# Keys are displayed in the first column, and their corresponding values
-# in the second. To evenly line up values, the key column is fixed-width
-# and its width is specified with the first parameter (possibly computed
-# using `batslib_get_max_single_line_key_width').
-#
-# Globals:
-#   none
-# Arguments:
-#   $1 - width of key column
-#   $even - key
-#   $odd - value of the previous key
-# Returns:
-#   none
-# Outputs:
-#   STDOUT - formatted key-value pairs
-batslib_print_kv_single() {
-  local -ir col_width="$1"; shift
-  while (( $# != 0 )); do
-    printf '%-*s : %s\n' "$col_width" "$1" "$2"
-    shift 2
-  done
-}
-
-# Print key-value pairs in multi-line format.
-#
-# The key is displayed first with the number of lines of its
-# corresponding value in parenthesis. Next, starting on the next line,
-# the value is displayed. For better readability, it is recommended to
-# indent values using `batslib_prefix'.
-#
-# Globals:
-#   none
-# Arguments:
-#   $odd - key
-#   $even - value of the previous key
-# Returns:
-#   none
-# Outputs:
-#   STDOUT - formatted key-value pairs
-batslib_print_kv_multi() {
-  while (( $# != 0 )); do
-    printf '%s (%d lines):\n' "$1" "$( batslib_count_lines "$2" )"
-    printf '%s\n' "$2"
-    shift 2
-  done
-}
-
-# Print all key-value pairs in either two-column or multi-line format
-# depending on whether all values are single-line.
-#
-# If all values are single-line, print all pairs in two-column format
-# with the specified key column width (identical to using
-# `batslib_print_kv_single').
-#
-# Otherwise, print all pairs in multi-line format after indenting values
-# with two spaces for readability (identical to using `batslib_prefix'
-# and `batslib_print_kv_multi')
-#
-# Globals:
-#   none
-# Arguments:
-#   $1 - width of key column (for two-column format)
-#   $even - key
-#   $odd - value of the previous key
-# Returns:
-#   none
-# Outputs:
-#   STDOUT - formatted key-value pairs
-batslib_print_kv_single_or_multi() {
-  local -ir width="$1"; shift
-  local -a pairs=( "$@" )
-
-  local -a values=()
-  local -i i
-  for (( i=1; i < ${#pairs[@]}; i+=2 )); do
-    values+=( "${pairs[$i]}" )
-  done
-
-  if batslib_is_single_line "${values[@]}"; then
-    batslib_print_kv_single "$width" "${pairs[@]}"
-  else
-    local -i i
-    for (( i=1; i < ${#pairs[@]}; i+=2 )); do
-      pairs[$i]="$( batslib_prefix < <(printf '%s' "${pairs[$i]}") )"
-    done
-    batslib_print_kv_multi "${pairs[@]}"
-  fi
-}
-
-# Prefix each line read from the standard input with the given string.
-#
-# Globals:
-#   none
-# Arguments:
-#   $1 - [=  ] prefix string
-# Returns:
-#   none
-# Inputs:
-#   STDIN - lines
-# Outputs:
-#   STDOUT - prefixed lines
-batslib_prefix() {
-  local -r prefix="${1:-  }"
-  local line
-  while IFS='' read -r line || [[ -n $line ]]; do
-    printf '%s%s\n' "$prefix" "$line"
-  done
-}
-
-# Mark select lines of the text read from the standard input by
-# overwriting their beginning with the given string.
-#
-# Usually the input is indented by a few spaces using `batslib_prefix'
-# first.
-#
-# Globals:
-#   none
-# Arguments:
-#   $1 - marking string
-#   $@ - indices (zero-based) of lines to mark
-# Returns:
-#   none
-# Inputs:
-#   STDIN - lines
-# Outputs:
-#   STDOUT - lines after marking
-batslib_mark() {
-  local -r symbol="$1"; shift
-  # Sort line numbers.
-  set -- $( sort -nu <<< "$( printf '%d\n' "$@" )" )
-
-  local line
-  local -i idx=0
-  while IFS='' read -r line || [[ -n $line ]]; do
-    if (( ${1:--1} == idx )); then
-      printf '%s\n' "${symbol}${line:${#symbol}}"
-      shift
-    else
-      printf '%s\n' "$line"
-    fi
-    (( ++idx ))
-  done
-}
-
-# Enclose the input text in header and footer lines.
-#
-# The header contains the given string as title. The output is preceded
-# and followed by an additional newline to make it stand out more.
-#
-# Globals:
-#   none
-# Arguments:
-#   $1 - title
-# Returns:
-#   none
-# Inputs:
-#   STDIN - text
-# Outputs:
-#   STDOUT - decorated text
-batslib_decorate() {
-  echo
-  echo "-- $1 --"
-  cat -
-  echo '--'
-  echo
-}

+ 0 - 66
test/lib/docker_helpers.bash

@@ -1,66 +0,0 @@
-## functions to help deal with docker
-
-# Removes container $1
-function docker_clean {
-	docker kill $1 &>/dev/null ||:
-	sleep .25s
-	docker rm -vf $1 &>/dev/null ||:
-	sleep .25s
-}
-
-# get the ip of docker container $1
-function docker_ip {
-	docker inspect --format '{{ .NetworkSettings.IPAddress }}' $1
-}
-
-# get the ip of docker container $1
-function docker_id {
-	docker inspect --format '{{ .ID }}' $1
-}
-
-# get the running state of container $1
-# → true/false
-# fails if the container does not exist
-function docker_running_state {
-	docker inspect -f {{.State.Running}} $1
-}
-
-# get the docker container $1 PID
-function docker_pid {
-	docker inspect --format {{.State.Pid}} $1
-}
-
-# asserts logs from container $1 contains $2
-function docker_assert_log {
-	local -r container=$1
-	shift
-	run docker logs $container
-	assert_output -p "$*"
-}
-
-# wait for a container to produce a given text in its log
-# $1 container
-# $2 timeout in second
-# $* text to wait for
-function docker_wait_for_log {
-	local -r container=$1
-	local -ir timeout_sec=$2
-	shift 2
-	retry $(( $timeout_sec * 2 )) .5s docker_assert_log $container "$*"
-}
-
-# Create a docker container named $1 which exposes the docker host unix 
-# socket over tcp on port 2375.
-#
-# $1 container name
-function docker_tcp {
-	local container_name="$1"
-	docker_clean $container_name
-	docker run -d \
-		--label bats-type="socat" \
-		--name $container_name \
-		--expose 2375 \
-		-v /var/run/docker.sock:/var/run/docker.sock \
-		rancher/socat-docker
-	docker run --label bats-type="docker" --link "$container_name:docker" docker:1.10 version
-}

+ 0 - 22
test/lib/helpers.bash

@@ -1,22 +0,0 @@
-## add the retry function to bats
-
-# Retry a command $1 times until it succeeds. Wait $2 seconds between retries.
-function retry {
-    local attempts=$1
-    shift
-    local delay=$1
-    shift
-    local i
-
-    for ((i=0; i < attempts; i++)); do
-        run "$@"
-        if [ "$status" -eq 0 ]; then
-            echo "$output"
-            return 0
-        fi
-        sleep $delay
-    done
-
-    echo "Command \"$@\" failed $attempts times. Status: $status. Output: $output" >&2
-    false
-}

+ 0 - 24
test/lib/ssl/nginx-proxy.bats.crt

@@ -1,24 +0,0 @@
------BEGIN CERTIFICATE-----
-MIID7TCCAtWgAwIBAgIJAOGkf5EnexJVMA0GCSqGSIb3DQEBCwUAMIGMMQswCQYD
-VQQGEwJVUzERMA8GA1UECAwIVmlyZ2luaWExDzANBgNVBAcMBlJlc3RvbjERMA8G
-A1UECgwIRmFrZSBPcmcxGzAZBgNVBAMMEioubmdpbngtcHJveHkuYmF0czEpMCcG
-CSqGSIb3DQEJARYad2VibWFzdGVyQG5naW54LXByb3h5LmJhdHMwHhcNMTYwNDIw
-MTUzOTUxWhcNMjYwNDE4MTUzOTUxWjCBjDELMAkGA1UEBhMCVVMxETAPBgNVBAgM
-CFZpcmdpbmlhMQ8wDQYDVQQHDAZSZXN0b24xETAPBgNVBAoMCEZha2UgT3JnMRsw
-GQYDVQQDDBIqLm5naW54LXByb3h5LmJhdHMxKTAnBgkqhkiG9w0BCQEWGndlYm1h
-c3RlckBuZ2lueC1wcm94eS5iYXRzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
-CgKCAQEA0Amkj3iaQn8Z2CW6n24zSuWu2OoLCkHZAk8eprkI4kKoPBvjusynkm8E
-phq65jebToHoldfuQ0wM61DzhD15bHwS3x9CrOVbShsmdnGALz+wdR0/4Likx50I
-YZdecTOAlkoZudnX5FZ4ngOxjqcym7p5T8TrSS97a0fx99gitZY0p+Nu2tip4o3t
-WBMs+SoPWTlQ1SrSmL8chC8O2knyBl/w1nHmDnMuR6FGcHdhLncApw9t5spgfv7p
-OrMF4tQxJQNk10TnflmEMkGmy+pfk2e0cQ1Kwp3Nmzm7ECkggxxyjU3ihKiFK+09
-8aSCi7gDAY925+mV6LZ5oLMpO3KJvQIDAQABo1AwTjAdBgNVHQ4EFgQU+NvFo37z
-9Dyq8Mu82SPtV7q1gYQwHwYDVR0jBBgwFoAU+NvFo37z9Dyq8Mu82SPtV7q1gYQw
-DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAI1ityDV0UsCVHSpB2LN+
-QXlk8XS0ACIJ8Q0hbOj3BmYrdAVglG4P6upDEueaaxwsaBTagkTP8nxZ9dhfZHyZ
-5YLNwYsiG5iqb8e0ecHx3uJT/0YiXn/8rBvxEZna4Fl8seGdp7BjOWUAS2Nv8tn4
-EJJvRdfX/O8XgPc95DM4lwQ/dvyWmavMI4lnl0n1IQV9WPGaIQhYPU9WEQK6iMUB
-o1kx8YbOJQD0ZBRfqpriNt1/8ylkkSYYav8QT9JFvQFCWEvaX71QF+cuOwC7ZYBH
-4ElXwEUrYBHKiPo0q0VsTtMvLh7h/T5czrIhG/NpfVJPtQOk8aVwNScL3/n+TGU8
-6g==
------END CERTIFICATE-----

+ 0 - 28
test/lib/ssl/nginx-proxy.bats.key

@@ -1,28 +0,0 @@
------BEGIN PRIVATE KEY-----
-MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQCaSPeJpCfxnY
-JbqfbjNK5a7Y6gsKQdkCTx6muQjiQqg8G+O6zKeSbwSmGrrmN5tOgeiV1+5DTAzr
-UPOEPXlsfBLfH0Ks5VtKGyZ2cYAvP7B1HT/guKTHnQhhl15xM4CWShm52dfkVnie
-A7GOpzKbunlPxOtJL3trR/H32CK1ljSn427a2Knije1YEyz5Kg9ZOVDVKtKYvxyE
-Lw7aSfIGX/DWceYOcy5HoUZwd2EudwCnD23mymB+/uk6swXi1DElA2TXROd+WYQy
-QabL6l+TZ7RxDUrCnc2bObsQKSCDHHKNTeKEqIUr7T3xpIKLuAMBj3bn6ZXotnmg
-syk7com9AgMBAAECggEAa7wCp3XqVPNjW+c1/ShhkbDeWmDhtL8i9aopkmeSbTHd
-07sRtQQU56Vsf+Sp010KpZ5q52Z6cglpS1eRtHLtdbvPPhL/QXBJVVg4E/B1VIKk
-DBJIqUSVuPXeiEOOWgs01R+ssO1ae1o4foQlKF33vGPWPPQacL0RKh6I9TPNzcD7
-n4rujlHk72N/bNydyK2rnyKB4vAI5TbZPLps+Xe123CmgZnW3JClcWV9B4foRmiu
-a5Iq1WYAK2GYKbYwgqDRyYBC27m91a7U31pE4GQD+xQdlz6kcOlCU5hAcPK3h7j0
-fLQqn8g+YAtc0nBKKB4NZe3QEzTiVMorT0VitxI71QKBgQDnirardZaXOFzYGzB3
-j+FGB9BUW54hnHr5BxOYrfmEJ5umJjJWaGupfYrQsPArrJP1//WbqVZIPvdQParD
-mQhLmSp1r/VNzGB6pISmzU1ZGDHsmBxYseh366om5YBQUFU2vmbil9VkrkM4fsJG
-tcS9V/nVY/EM7Yp3PzjfLlhC1wKBgQDmA1YJmnZvIbLp3PoKqM69QiCLKztVm7nX
-xpu3b3qbXEzXkt2sP5PHmr+s13hOPQFKRJ2hk4UN9WqpnFoHw5E5eWWhSa/peUZm
-r10Y5XspiFtRHHiu6ABXB49eB4fen+vHEZHKyRJ4rFthKjjBHdNPC8bmwnT3jE85
-/8a26FLZiwKBgQDXEi8JZslBn9YF2oOTm28KCLoHka551AsaA+u892T8z3mxxGsf
-fhD7N6TYonIEb2Jkr6OpOortwqcgvpc+5oghCJ27AX2fDUdUxDp/YdYF+wZsmQJD
-lMW1lo7PYIBmmaf9mLCiq5xIz+GauYul+LNNmUl0YEgI1SC4EV63WCodswKBgDMX
-GJxHd/kVViVGFTAa8NjvAEWJU8OfNHduQRZMp8IsjVDw6VYiRRP4Fo0wyyMtv8Sc
-WxsRpmNEWO3VsdW5pd9LTLy3nmBQtMeIOjiWeHXwOMBaf5/yHmk2X6z2JULY6Mkt
-6OFPKlAtkJqTg0m58z7Ckeqd1NdLjimG27+y+PwjAoGAFt0cbC1Ust2BE6YEspSX
-ofpAnJsyKrbF9iVUyXDUP99sdqYQfPJ5uqPGkP59lJGkTLtebuitqi6FCyrsT6Fq
-AWLiExbqebAqcuAZw2S+iuK27S4rrkjVGF53J7vH3rOzCBUXaRx6GKfTjUqedHdg
-9Kw+LP6IFnMTb+EGLo+GqHs=
------END PRIVATE KEY-----

+ 0 - 43
test/multiple-hosts.bats

@@ -1,43 +0,0 @@
-#!/usr/bin/env bats
-load test_helpers
-SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}
-
-function setup {
-	# make sure to stop any web container before each test so we don't
-	# have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set
-	stop_bats_containers web
-}
-
-
-@test "[$TEST_FILE] start a nginx-proxy container" {
-	run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro
-	assert_success
-	docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
-}
-
-@test "[$TEST_FILE] nginx-proxy forwards requests for 2 hosts" {
-	# WHEN a container runs a web server with VIRTUAL_HOST set for multiple hosts
-	prepare_web_container bats-multiple-hosts-1 80 -e VIRTUAL_HOST=multiple-hosts-1-A.bats,multiple-hosts-1-B.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-multiple-hosts-1
-	sleep 1
-
-	# THEN querying the proxy without Host header → 503
-	run curl_container $SUT_CONTAINER / --head
-	assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
-
-	# THEN querying the proxy with unknown Host header → 503
-	run curl_container $SUT_CONTAINER /port --header "Host: webFOO.bats" --head
-	assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
-
-	# THEN
-	run curl_container $SUT_CONTAINER /port --header 'Host: multiple-hosts-1-A.bats'
-	assert_output "answer from port 80"
-
-	# THEN
-	run curl_container $SUT_CONTAINER /port --header 'Host: multiple-hosts-1-B.bats'
-	assert_output "answer from port 80"
-}
-
-@test "[$TEST_FILE] stop all bats containers" {
-	stop_bats_containers
-}

+ 0 - 64
test/multiple-ports.bats

@@ -1,64 +0,0 @@
-#!/usr/bin/env bats
-load test_helpers
-SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}
-
-function setup {
-	# make sure to stop any web container before each test so we don't
-	# have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set
-	stop_bats_containers web
-}
-
-
-@test "[$TEST_FILE] start a nginx-proxy container" {
-	# GIVEN nginx-proxy
-	run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro
-	assert_success
-	docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
-}
-
-
-@test "[$TEST_FILE] nginx-proxy defaults to the service running on port 80" {
-	# WHEN
-	prepare_web_container bats-web-${TEST_FILE}-1 "80 90" -e VIRTUAL_HOST=web.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-web-${TEST_FILE}-1
-	sleep 1
-
-	# THEN
-	assert_response_is_from_port 80
-}
-
-
-@test "[$TEST_FILE] VIRTUAL_PORT=90 while port 80 is also exposed" {
-	# GIVEN
-	prepare_web_container bats-web-${TEST_FILE}-2 "80 90" -e VIRTUAL_HOST=web.bats -e VIRTUAL_PORT=90
-	dockergen_wait_for_event $SUT_CONTAINER start bats-web-${TEST_FILE}-2
-	sleep 1
-
-	# THEN
-	assert_response_is_from_port 90
-}
-
-
-@test "[$TEST_FILE] single exposed port != 80" {
-	# GIVEN
-	prepare_web_container bats-web-${TEST_FILE}-3 1234 -e VIRTUAL_HOST=web.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-web-${TEST_FILE}-3
-	sleep 1
-
-	# THEN
-	assert_response_is_from_port 1234
-}
-
-@test "[$TEST_FILE] stop all bats containers" {
-	stop_bats_containers
-}
-
-
-# assert querying nginx-proxy provides a response from the expected port of the web container
-# $1 port we are expecting an response from
-function assert_response_is_from_port {
-	local -r port=$1
-	run curl_container $SUT_CONTAINER /port --header "Host: web.bats"
-	assert_output "answer from port $port"
-}
-

+ 3 - 0
test/pytest.ini

@@ -0,0 +1,3 @@
+[pytest]
+# disable the creation of the `.cache` folders
+addopts = -p no:cacheprovider --ignore=requirements --ignore=certs -r s -v

+ 24 - 0
test/pytest.sh

@@ -0,0 +1,24 @@
+#!/bin/bash
+###############################################################################
+#                                                                             #
+# This script is meant to run the test suite from a Docker container.         #
+#                                                                             #
+# This is usefull when you want to run the test suite from Mac or             #
+# Docker Toolbox.                                                             #
+#                                                                             #
+###############################################################################
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+ARGS="$@"
+
+# check requirements
+echo "> Building nginx-proxy-tester image..."
+docker build -t nginx-proxy-tester -f $DIR/requirements/Dockerfile-nginx-proxy-tester $DIR/requirements
+
+# run the nginx-proxy-tester container setting the correct value for the working dir in order for 
+# docker-compose to work properly when run from within that container.
+exec docker run --rm -it \
+	-v ${DIR}:/${DIR} \
+	-w ${DIR} \
+	-v /var/run/docker.sock:/var/run/docker.sock \
+	nginx-proxy-tester ${ARGS}

+ 5 - 0
test/requirements/Dockerfile-nginx-proxy-tester

@@ -0,0 +1,5 @@
+FROM python:2.7
+COPY python-requirements.txt /requirements.txt
+RUN pip install -r /requirements.txt
+WORKDIR /test
+ENTRYPOINT ["pytest"]

+ 52 - 0
test/requirements/README.md

@@ -0,0 +1,52 @@
+This directory contains resources to build Docker images tests depend on
+
+# Build images
+
+    ./build.sh   
+
+
+# python-requirements.txt
+
+If you want to run the test suite from your computer, you need python and a few python modules.
+The _python-requirements.txt_ file describes the python modules required. To install them, use
+pip:
+
+    pip install -r python-requirements.txt
+
+If you don't want to run the test from your computer, you can run the tests from a docker container, see the _pytest.sh_ script.
+
+
+# Images
+
+## web
+
+This container will run one or many webservers, each of them listening on a single port.
+
+Ports are specified using the `WEB_PORTS` environment variable:
+
+    docker run -d -e WEB_PORTS=80 web  # will create a container running one webserver listening on port 80
+    docker run -d -e WEB_PORTS="80 81" web  # will create a container running two webservers, one listening on port 80 and a second one listening on port 81
+
+The webserver answers on two paths:
+
+- `/headers`
+- `/port`
+
+```
+$ docker run -d -e WEB_PORTS=80 -p 80:80 web
+$ curl http://127.0.0.1:80/headers
+Host: 127.0.0.1
+User-Agent: curl/7.47.0
+Accept: */*
+
+$ curl http://127.0.0.1:80/port
+answer from port 80
+
+```
+
+
+## nginx-proxy-tester
+
+This is an optional requirement which is usefull if you cannot (or don't want to) install pytest and its requirements on your computer. In this case, you can use the `nginx-proxy-tester` docker image to run the test suite from a Docker container.
+
+To use this image, it is mandatory to run the container using the `pytest.sh` shell script. The script will build the image and run a container from it with the appropriate volumes and settings.

+ 6 - 0
test/requirements/build.sh

@@ -0,0 +1,6 @@
+#!/bin/bash
+set -e
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+docker build -t web $DIR/web

+ 5 - 0
test/requirements/python-requirements.txt

@@ -0,0 +1,5 @@
+backoff==1.3.2
+docker-compose==1.11.1
+docker==2.0.2
+pytest==3.0.5
+requests==2.11.1

+ 8 - 0
test/requirements/web/Dockerfile

@@ -0,0 +1,8 @@
+# Docker Image running one (or multiple) webservers listening on all given ports from WEB_PORTS environment variable
+
+FROM python:3
+COPY ./webserver.py /
+COPY ./entrypoint.sh /
+WORKDIR /opt
+ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
+

+ 15 - 0
test/requirements/web/entrypoint.sh

@@ -0,0 +1,15 @@
+#!/bin/bash
+set -u
+
+trap '[ ${#PIDS[@]} -gt 0 ] && kill -TERM ${PIDS[@]}' TERM
+declare -a PIDS
+
+for port in $WEB_PORTS; do
+	echo starting a web server listening on port $port;
+	/webserver.py $port &
+	PIDS+=($!)
+done
+
+wait ${PIDS[@]}
+trap - TERM
+wait ${PIDS[@]}

+ 7 - 3
test/web_helpers/webserver.py → test/requirements/web/webserver.py

@@ -4,9 +4,9 @@ import os, sys
 import http.server
 import socketserver
 
-class BatsHandler(http.server.SimpleHTTPRequestHandler):
+
+class Handler(http.server.SimpleHTTPRequestHandler):
     def do_GET(self):
-        root = os.getcwd()
         
         self.send_response(200)
         self.send_header("Content-Type", "text/plain")
@@ -17,11 +17,15 @@ class BatsHandler(http.server.SimpleHTTPRequestHandler):
         elif self.path == "/port":
             response = "answer from port %s\n" % PORT
             self.wfile.write(response.encode())
+        elif self.path == "/":
+            response = "I'm %s\n" % os.environ['HOSTNAME']
+            self.wfile.write(response.encode())
         else:
             self.wfile.write("No route for this path!\n".encode())
 
+
 if __name__ == '__main__':
     PORT = int(sys.argv[1])
     socketserver.TCPServer.allow_reuse_address = True
-    httpd = socketserver.TCPServer(('0.0.0.0', PORT), BatsHandler)
+    httpd = socketserver.TCPServer(('0.0.0.0', PORT), Handler)
     httpd.serve_forever()

+ 0 - 168
test/ssl.bats

@@ -1,168 +0,0 @@
-#!/usr/bin/env bats
-load test_helpers
-SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}
-
-function setup {
-	# make sure to stop any web container before each test so we don't
-	# have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set
-	stop_bats_containers web
-}
-
-
-@test "[$TEST_FILE] start a nginx-proxy container" {
-	run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro -v ${DIR}/lib/ssl:/etc/nginx/certs:ro
-	assert_success
-	docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
-}
-
-@test "[$TEST_FILE] test SSL for VIRTUAL_HOST=*.nginx-proxy.bats" {
-	# WHEN
-	prepare_web_container bats-ssl-hosts-1 "80" \
-		-e VIRTUAL_HOST=*.nginx-proxy.bats \
-		-e CERT_NAME=nginx-proxy.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-1
-	sleep 1
-
-	# THEN
-	assert_301 test.nginx-proxy.bats
-	assert_200_https test.nginx-proxy.bats
-}
-
-@test "[$TEST_FILE] test HTTPS_METHOD=nohttp" {
-	# WHEN
-	prepare_web_container bats-ssl-hosts-2 "80" \
-		-e VIRTUAL_HOST=*.nginx-proxy.bats \
-		-e CERT_NAME=nginx-proxy.bats \
-		-e HTTPS_METHOD=nohttp
-	dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-2
-	sleep 1
-
-	# THEN
-	assert_503 test.nginx-proxy.bats
-	assert_200_https test.nginx-proxy.bats
-}
-
-@test "[$TEST_FILE] test HTTPS_METHOD=noredirect" {
-	# WHEN
-	prepare_web_container bats-ssl-hosts-3 "80" \
-		-e VIRTUAL_HOST=*.nginx-proxy.bats \
-		-e CERT_NAME=nginx-proxy.bats \
-		-e HTTPS_METHOD=noredirect
-	dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-3
-	sleep 1
-
-	# THEN
-	assert_200 test.nginx-proxy.bats
-	assert_200_https test.nginx-proxy.bats
-}
-
-@test "[$TEST_FILE] test SSL Strict-Transport-Security" {
-	# WHEN
-	prepare_web_container bats-ssl-hosts-4 "80" \
-		-e VIRTUAL_HOST=*.nginx-proxy.bats \
-		-e CERT_NAME=nginx-proxy.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-4
-	sleep 1
-
-	# THEN
-	assert_301 test.nginx-proxy.bats
-	assert_200_https test.nginx-proxy.bats
-	assert_output -p "Strict-Transport-Security: max-age=31536000"
-}
-
-@test "[$TEST_FILE] test HTTPS_METHOD=noredirect disables Strict-Transport-Security" {
-	# WHEN
-	prepare_web_container bats-ssl-hosts-5 "80" \
-		-e VIRTUAL_HOST=*.nginx-proxy.bats \
-		-e CERT_NAME=nginx-proxy.bats \
-		-e HTTPS_METHOD=noredirect
-	dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-5
-	sleep 1
-
-	# THEN
-	assert_200 test.nginx-proxy.bats
-	assert_200_https test.nginx-proxy.bats
-	refute_output -p "Strict-Transport-Security: max-age=31536000"
-}
-
-@test "[$TEST_FILE] test HTTPS_METHOD=nohttps" {
-	# WHEN
-	prepare_web_container bats-ssl-hosts-6 "80" \
-		-e VIRTUAL_HOST=*.nginx-proxy.bats \
-		-e CERT_NAME=nginx-proxy.bats \
-		-e HTTPS_METHOD=nohttps
-	dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-6
-	sleep 1
-
-	# THEN
-	assert_down_https test.nginx-proxy.bats
-	assert_200 test.nginx-proxy.bats
-}
-
-@test "[$TEST_FILE] stop all bats containers" {
-	stop_bats_containers
-}
-
-
-# assert that querying nginx-proxy with the given Host header produces a `HTTP 200` response
-# $1 Host HTTP header to use when querying nginx-proxy
-function assert_200 {
-	local -r host=$1
-
-	run curl_container $SUT_CONTAINER / --head --header "Host: $host"
-	assert_output -l 0 $'HTTP/1.1 200 OK\r'
-}
-
-# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response
-# $1 Host HTTP header to use when querying nginx-proxy
-function assert_503 {
-	local -r host=$1
-
-	run curl_container $SUT_CONTAINER / --head --header "Host: $host"
-	assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
-}
-
-# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response
-# $1 Host HTTP header to use when querying nginx-proxy
-function assert_301 {
-	local -r host=$1
-
-	run curl_container $SUT_CONTAINER / --head --header "Host: $host"
-	assert_output -l 0 $'HTTP/1.1 301 Moved Permanently\r'
-}
-
-# assert that querying nginx-proxy with the given Host header fails because the host is down
-# $1 Host HTTP header to use when querying nginx-proxy
-function assert_down_https {
-	local -r host=$1
-
-	run curl_container_https $SUT_CONTAINER / --head --header "Host: $host"
-	assert_failure
-}
-
-# assert that querying nginx-proxy with the given Host header produces a `HTTP 200` response
-# $1 Host HTTP header to use when querying nginx-proxy
-function assert_200_https {
-	local -r host=$1
-
-	run curl_container_https $SUT_CONTAINER / --head --header "Host: $host"
-	assert_output -l 0 $'HTTP/1.1 200 OK\r'
-}
-
-# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response
-# $1 Host HTTP header to use when querying nginx-proxy
-function assert_503_https {
-	local -r host=$1
-
-	run curl_container_https $SUT_CONTAINER / --head --header "Host: $host"
-	assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
-}
-
-# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response
-# $1 Host HTTP header to use when querying nginx-proxy
-function assert_301_https {
-	local -r host=$1
-
-	run curl_container_https $SUT_CONTAINER / --head --header "Host: $host"
-	assert_output -l 0 $'HTTP/1.1 301 Moved Permanently\r'
-}

+ 15 - 0
test/test_DOCKER_HOST_unix_socket.py

@@ -0,0 +1,15 @@
+import pytest
+
+def test_unknown_virtual_host(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/port")
+    assert r.status_code == 503
+
+def test_forwards_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.tld/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+
+def test_forwards_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert r.text == "answer from port 82\n" 

+ 24 - 0
test/test_DOCKER_HOST_unix_socket.yml

@@ -0,0 +1,24 @@
+web1:
+  image: web 
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: 81
+    VIRTUAL_HOST: web1.nginx-proxy.tld
+
+web2:
+  image: web
+  expose: 
+    - "82"
+  environment:
+    WEB_PORTS: 82
+    VIRTUAL_HOST: web2.nginx-proxy.tld
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/f00.sock:ro
+  environment:
+    DOCKER_HOST: unix:///f00.sock
+

+ 10 - 0
test/test_composev2.py

@@ -0,0 +1,10 @@
+import pytest
+
+def test_unknown_virtual_host(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+
+def test_forwards_to_whoami(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"

+ 14 - 0
test/test_composev2.yml

@@ -0,0 +1,14 @@
+version: '2'
+services:
+  nginx-proxy:
+    image: jwilder/nginx-proxy:test
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+
+  web:
+    image: web 
+    expose:
+      - "81"
+    environment:
+      WEB_PORTS: 81
+      VIRTUAL_HOST: web.nginx-proxy.local

+ 1 - 0
test/test_custom/my_custom_proxy_settings.conf

@@ -0,0 +1 @@
+add_header  X-test  f00;

+ 1 - 0
test/test_custom/my_custom_proxy_settings_bar.conf

@@ -0,0 +1 @@
+add_header  X-test  bar;

+ 28 - 0
test/test_custom/test_defaults-location.py

@@ -0,0 +1,28 @@
+import pytest
+
+def test_custom_default_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+    assert "X-test" not in r.headers
+
+def test_custom_default_conf_applies_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]
+
+def test_custom_default_conf_applies_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 82\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]
+
+
+def test_custom_default_conf_is_overriden_for_web3(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web3.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 83\n"
+    assert "X-test" in r.headers
+    assert "bar" == r.headers["X-test"]

+ 30 - 0
test/test_custom/test_defaults-location.yml

@@ -0,0 +1,30 @@
+nginx-proxy:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ./my_custom_proxy_settings.conf:/etc/nginx/vhost.d/default_location:ro
+    - ./my_custom_proxy_settings_bar.conf:/etc/nginx/vhost.d/web3.nginx-proxy.local_location:ro
+
+web1:
+  image: web 
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: 81
+    VIRTUAL_HOST: web1.nginx-proxy.local
+
+web2:
+  image: web 
+  expose:
+    - "82"
+  environment:
+    WEB_PORTS: 82
+    VIRTUAL_HOST: web2.nginx-proxy.local
+
+web3:
+  image: web 
+  expose:
+    - "83"
+  environment:
+    WEB_PORTS: 83
+    VIRTUAL_HOST: web3.nginx-proxy.local

+ 20 - 0
test/test_custom/test_defaults.py

@@ -0,0 +1,20 @@
+import pytest
+
+def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+    assert "X-test" not in r.headers
+
+def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]
+
+def test_custom_conf_applies_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 82\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]

+ 23 - 0
test/test_custom/test_defaults.yml

@@ -0,0 +1,23 @@
+version: '2'
+services:
+  nginx-proxy:
+    image: jwilder/nginx-proxy:test
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+      - ./my_custom_proxy_settings.conf:/etc/nginx/proxy.conf:ro
+
+  web1:
+    image: web 
+    expose:
+      - "81"
+    environment:
+      WEB_PORTS: 81
+      VIRTUAL_HOST: web1.nginx-proxy.local
+
+  web2:
+    image: web 
+    expose:
+      - "82"
+    environment:
+      WEB_PORTS: 82
+      VIRTUAL_HOST: web2.nginx-proxy.local

+ 22 - 0
test/test_custom/test_location-per-vhost.py

@@ -0,0 +1,22 @@
+import pytest
+
+def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+    assert "X-test" not in r.headers
+
+def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]
+
+def test_custom_conf_does_not_apply_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 82\n"
+    assert "X-test" not in r.headers
+
+def test_custom_block_is_present_in_nginx_generated_conf(docker_compose, nginxproxy):
+    assert "include /etc/nginx/vhost.d/web1.nginx-proxy.local_location;" in nginxproxy.get_conf()

+ 23 - 0
test/test_custom/test_location-per-vhost.yml

@@ -0,0 +1,23 @@
+version: '2'
+services:
+  nginx-proxy:
+    image: jwilder/nginx-proxy:test
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+      - ./my_custom_proxy_settings.conf:/etc/nginx/vhost.d/web1.nginx-proxy.local_location:ro
+
+  web1:
+    image: web 
+    expose:
+      - "81"
+    environment:
+      WEB_PORTS: 81
+      VIRTUAL_HOST: web1.nginx-proxy.local
+
+  web2:
+    image: web 
+    expose:
+      - "82"
+    environment:
+      WEB_PORTS: 82
+      VIRTUAL_HOST: web2.nginx-proxy.local

+ 19 - 0
test/test_custom/test_per-vhost.py

@@ -0,0 +1,19 @@
+import pytest
+
+def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+    assert "X-test" not in r.headers
+
+def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]
+
+def test_custom_conf_does_not_apply_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 82\n"
+    assert "X-test" not in r.headers

+ 23 - 0
test/test_custom/test_per-vhost.yml

@@ -0,0 +1,23 @@
+version: '2'
+services:
+  nginx-proxy:
+    image: jwilder/nginx-proxy:test
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+      - ./my_custom_proxy_settings.conf:/etc/nginx/vhost.d/web1.nginx-proxy.local:ro
+
+  web1:
+    image: web 
+    expose:
+      - "81"
+    environment:
+      WEB_PORTS: 81
+      VIRTUAL_HOST: web1.nginx-proxy.local
+
+  web2:
+    image: web 
+    expose:
+      - "82"
+    environment:
+      WEB_PORTS: 82
+      VIRTUAL_HOST: web2.nginx-proxy.local

+ 20 - 0
test/test_custom/test_proxy-wide.py

@@ -0,0 +1,20 @@
+import pytest
+
+def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+    assert "X-test" not in r.headers
+
+def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]
+
+def test_custom_conf_applies_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 82\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]

+ 23 - 0
test/test_custom/test_proxy-wide.yml

@@ -0,0 +1,23 @@
+version: '2'
+services:
+  nginx-proxy:
+    image: jwilder/nginx-proxy:test
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+      - ./my_custom_proxy_settings.conf:/etc/nginx/conf.d/my_custom_proxy_settings.conf:ro
+
+  web1:
+    image: web 
+    expose:
+      - "81"
+    environment:
+      WEB_PORTS: 81
+      VIRTUAL_HOST: web1.nginx-proxy.local
+
+  web2:
+    image: web 
+    expose:
+      - "82"
+    environment:
+      WEB_PORTS: 82
+      VIRTUAL_HOST: web2.nginx-proxy.local

+ 7 - 0
test/test_default-host.py

@@ -0,0 +1,7 @@
+import pytest
+
+
+def test_fallback_on_default(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://unknown.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert r.text == "answer from port 81\n"

+ 17 - 0
test/test_default-host.yml

@@ -0,0 +1,17 @@
+# GIVEN a webserver with VIRTUAL_HOST set to web1.tld
+web1:
+  image: web
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: 81
+    VIRTUAL_HOST: web1.tld
+
+
+# WHEN nginx-proxy runs with DEFAULT_HOST set to web1.tld
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+  environment:
+    DEFAULT_HOST: web1.tld

+ 1 - 0
test/test_dockergen/.gitignore

@@ -0,0 +1 @@
+nginx.tmpl

+ 38 - 0
test/test_dockergen/test_dockergen_v2.py

@@ -0,0 +1,38 @@
+import os
+import docker
+import logging
+import pytest
+
+
+@pytest.yield_fixture(scope="module")
+def nginx_tmpl():
+    """
+    pytest fixture which extracts the the nginx config template from
+    the jwilder/nginx-proxy:test image
+    """
+    script_dir = os.path.dirname(__file__)
+    logging.info("extracting nginx.tmpl from jwilder/nginx-proxy:test")
+    docker_client = docker.from_env()
+    print(docker_client.containers.run(
+        image='jwilder/nginx-proxy:test',
+        remove=True,
+        volumes=['{current_dir}:{current_dir}'.format(current_dir=script_dir)],
+        entrypoint='sh',
+        command='-xc "cp /app/nginx.tmpl {current_dir} && chmod 777 {current_dir}/nginx.tmpl"'.format(
+            current_dir=script_dir),
+        stderr=True))
+    yield
+    logging.info("removing nginx.tmpl")
+    os.remove(os.path.join(script_dir, "nginx.tmpl"))
+
+
+def test_unknown_virtual_host_is_503(nginx_tmpl, docker_compose, nginxproxy):
+    r = nginxproxy.get("http://unknown.nginx.container.docker/")
+    assert r.status_code == 503
+
+
+def test_forwards_to_whoami(nginx_tmpl, docker_compose, nginxproxy):
+    r = nginxproxy.get("http://whoami.nginx.container.docker/")
+    assert r.status_code == 200
+    whoami_container = docker_compose.containers.get("whoami")
+    assert r.text == "I'm %s\n" % whoami_container.id[:12]

+ 26 - 0
test/test_dockergen/test_dockergen_v2.yml

@@ -0,0 +1,26 @@
+version: '2'
+
+services:
+  nginx:
+    image: nginx
+    container_name: nginx
+    volumes:
+      - /etc/nginx/conf.d
+
+  dockergen:
+    image: jwilder/docker-gen
+    command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
+    volumes_from:
+      - nginx
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+      - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl
+
+  web:
+    image: web
+    container_name: whoami
+    expose:
+      - "80"
+    environment:
+      WEB_PORTS: 80
+      VIRTUAL_HOST: whoami.nginx.container.docker

+ 47 - 0
test/test_dockergen/test_dockergen_v3.py

@@ -0,0 +1,47 @@
+import os
+import docker
+import logging
+import pytest
+
+
+def versiontuple(v):
+    return tuple(map(int, (v.split("."))))
+
+
+docker_version = docker.from_env().version()['Version']
+pytestmark = pytest.mark.skipif(versiontuple(docker_version) < versiontuple('1.13'),
+                                reason="Docker compose syntax v3 requires docker engine v1.13")
+
+
+@pytest.yield_fixture(scope="module")
+def nginx_tmpl():
+    """
+    pytest fixture which extracts the the nginx config template from
+    the jwilder/nginx-proxy:test image
+    """
+    script_dir = os.path.dirname(__file__)
+    logging.info("extracting nginx.tmpl from jwilder/nginx-proxy:test")
+    docker_client = docker.from_env()
+    print(docker_client.containers.run(
+        image='jwilder/nginx-proxy:test',
+        remove=True,
+        volumes=['{current_dir}:{current_dir}'.format(current_dir=script_dir)],
+        entrypoint='sh',
+        command='-xc "cp /app/nginx.tmpl {current_dir} && chmod 777 {current_dir}/nginx.tmpl"'.format(
+            current_dir=script_dir),
+        stderr=True))
+    yield
+    logging.info("removing nginx.tmpl")
+    os.remove(os.path.join(script_dir, "nginx.tmpl"))
+
+
+def test_unknown_virtual_host_is_503(nginx_tmpl, docker_compose, nginxproxy):
+    r = nginxproxy.get("http://unknown.nginx.container.docker/")
+    assert r.status_code == 503
+
+
+def test_forwards_to_whoami(nginx_tmpl, docker_compose, nginxproxy):
+    r = nginxproxy.get("http://whoami.nginx.container.docker/")
+    assert r.status_code == 200
+    whoami_container = docker_compose.containers.get("whoami")
+    assert r.text == "I'm %s\n" % whoami_container.id[:12]

+ 27 - 0
test/test_dockergen/test_dockergen_v3.yml

@@ -0,0 +1,27 @@
+version: '3'
+services:
+  nginx:
+    image: nginx
+    container_name: nginx
+    volumes:
+      - nginx_conf:/etc/nginx/conf.d
+
+  dockergen:
+    image: jwilder/docker-gen
+    command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+      - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl
+      - nginx_conf:/etc/nginx/conf.d
+
+  web:
+    image: web
+    container_name: whoami
+    expose:
+      - "80"
+    environment:
+      WEB_PORTS: 80
+      VIRTUAL_HOST: whoami.nginx.container.docker
+
+volumes:
+  nginx_conf: {}

+ 46 - 0
test/test_events.py

@@ -0,0 +1,46 @@
+"""
+Test that nginx-proxy detects new containers
+"""
+from time import sleep
+
+import pytest
+from docker.errors import NotFound
+
+
+@pytest.yield_fixture()
+def web1(docker_compose):
+    """
+    pytest fixture creating a web container with `VIRTUAL_HOST=web1.nginx-proxy` listening on port 81.
+    """
+    container = docker_compose.containers.run(
+        name="web1",
+        image="web",
+        detach=True,
+        environment={
+            "WEB_PORTS": "81",
+            "VIRTUAL_HOST": "web1.nginx-proxy"
+        },
+        ports={"81/tcp": None}
+    )
+    sleep(2)  # give it some time to initialize and for docker-gen to detect it
+    yield container
+    try:
+        docker_compose.containers.get("web1").remove(force=True)
+    except NotFound:
+        pass
+
+
+def test_nginx_proxy_behavior_when_alone(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+
+
+def test_new_container_is_detected(web1, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy/port")
+    assert r.status_code == 200
+    assert "answer from port 81\n" == r.text
+
+    web1.remove(force=True)
+    sleep(2)
+    r = nginxproxy.get("http://web1.nginx-proxy/port")
+    assert r.status_code == 503

+ 4 - 0
test/test_events.yml

@@ -0,0 +1,4 @@
+nginxproxy:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro

+ 70 - 0
test/test_headers/certs/web.nginx-proxy.tld.crt

@@ -0,0 +1,70 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 4096 (0x1000)
+    Signature Algorithm: sha256WithRSAEncryption
+        Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
+        Validity
+            Not Before: Jan 13 03:06:39 2017 GMT
+            Not After : May 31 03:06:39 2044 GMT
+        Subject: CN=web.nginx-proxy.tld
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:95:56:c7:0d:48:a5:2b:3c:65:49:3f:26:e1:38:
+                    2b:61:30:56:e4:92:d7:63:e0:eb:ad:ac:f9:33:9b:
+                    b2:31:f1:39:13:0b:e5:43:7b:c5:bd:8a:85:c8:d9:
+                    3d:d8:ac:71:ba:16:e7:81:96:b2:ab:ae:c6:c0:bd:
+                    be:a7:d1:96:8f:b2:9b:df:ba:f9:4d:a1:3b:7e:21:
+                    4a:cd:b6:45:f9:6d:79:50:bf:24:8f:c1:6b:c1:09:
+                    19:5b:62:cb:96:e8:04:14:20:e8:d4:16:62:6a:f2:
+                    37:c1:96:e2:9d:53:05:0b:52:1d:e7:68:92:db:8b:
+                    36:68:cd:8d:5b:02:ff:12:f0:ac:5d:0c:c4:e0:7a:
+                    55:a2:49:60:9f:ff:47:1f:52:73:55:4d:d4:f2:d1:
+                    62:a2:f4:50:9d:c9:f6:f1:43:b3:dc:57:e1:31:76:
+                    b4:e0:a4:69:7e:f2:6d:34:ae:b9:8d:74:26:7b:d9:
+                    f6:07:00:ef:4b:36:61:b3:ef:7a:a1:36:3a:b6:d0:
+                    9e:f8:b8:a9:0d:4c:30:a2:ed:eb:ab:6b:eb:2e:e2:
+                    0b:28:be:f7:04:b1:e9:e0:84:d6:5d:31:77:7c:dc:
+                    d2:1f:d4:1d:71:6f:6f:6c:6d:1b:bf:31:e2:5b:c3:
+                    52:d0:14:fc:8b:fb:45:ea:41:ec:ca:c7:3b:67:12:
+                    c4:df
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Subject Alternative Name: 
+                DNS:web.nginx-proxy.tld
+    Signature Algorithm: sha256WithRSAEncryption
+         4e:48:7d:81:66:ba:2f:50:3d:24:42:61:3f:1f:de:cf:ec:1b:
+         1b:bd:0a:67:b6:62:c8:79:9d:31:a0:fd:a9:61:ce:ff:69:bf:
+         0e:f4:f7:e6:15:2b:b0:f0:e4:f2:f4:d2:8f:74:02:b1:1e:4a:
+         a8:6f:26:0a:77:32:29:cf:dc:b5:61:82:3e:58:47:61:92:f0:
+         0c:20:25:f8:41:4d:34:09:44:bc:39:9e:aa:82:06:83:13:8b:
+         1e:2c:3d:cf:cd:1a:f7:77:39:38:e0:a3:a7:f3:09:da:02:8d:
+         73:75:38:b4:dd:24:a7:f9:03:db:98:c6:88:54:87:dc:e0:65:
+         4c:95:c5:39:9c:00:30:dc:f0:d3:2c:19:ca:f1:f4:6c:c6:d9:
+         b5:c4:4a:c7:bc:a1:2e:88:7b:b5:33:d0:ff:fb:48:5e:3e:29:
+         fa:58:e5:03:de:d8:17:de:ed:96:fc:7e:1f:fe:98:f6:be:99:
+         38:87:51:c0:d3:b7:9a:0f:26:92:e5:53:1b:d6:25:4c:ac:48:
+         f3:29:fc:74:64:9d:07:6a:25:57:24:aa:a7:70:fa:8f:6c:a7:
+         2b:b7:9d:81:46:10:32:93:b9:45:6d:0f:16:18:b2:21:1f:f3:
+         30:24:62:3f:e1:6c:07:1d:71:28:cb:4c:bb:f5:39:05:f9:b2:
+         5b:a0:05:1b
+-----BEGIN CERTIFICATE-----
+MIIC+zCCAeOgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
+bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
+ZDAeFw0xNzAxMTMwMzA2MzlaFw00NDA1MzEwMzA2MzlaMB4xHDAaBgNVBAMME3dl
+Yi5uZ2lueC1wcm94eS50bGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQCVVscNSKUrPGVJPybhOCthMFbkktdj4OutrPkzm7Ix8TkTC+VDe8W9ioXI2T3Y
+rHG6FueBlrKrrsbAvb6n0ZaPspvfuvlNoTt+IUrNtkX5bXlQvySPwWvBCRlbYsuW
+6AQUIOjUFmJq8jfBluKdUwULUh3naJLbizZozY1bAv8S8KxdDMTgelWiSWCf/0cf
+UnNVTdTy0WKi9FCdyfbxQ7PcV+ExdrTgpGl+8m00rrmNdCZ72fYHAO9LNmGz73qh
+Njq20J74uKkNTDCi7eura+su4gsovvcEsenghNZdMXd83NIf1B1xb29sbRu/MeJb
+w1LQFPyL+0XqQezKxztnEsTfAgMBAAGjIjAgMB4GA1UdEQQXMBWCE3dlYi5uZ2lu
+eC1wcm94eS50bGQwDQYJKoZIhvcNAQELBQADggEBAE5IfYFmui9QPSRCYT8f3s/s
+Gxu9Cme2Ysh5nTGg/alhzv9pvw709+YVK7Dw5PL00o90ArEeSqhvJgp3MinP3LVh
+gj5YR2GS8AwgJfhBTTQJRLw5nqqCBoMTix4sPc/NGvd3OTjgo6fzCdoCjXN1OLTd
+JKf5A9uYxohUh9zgZUyVxTmcADDc8NMsGcrx9GzG2bXESse8oS6Ie7Uz0P/7SF4+
+KfpY5QPe2Bfe7Zb8fh/+mPa+mTiHUcDTt5oPJpLlUxvWJUysSPMp/HRknQdqJVck
+qqdw+o9spyu3nYFGEDKTuUVtDxYYsiEf8zAkYj/hbAcdcSjLTLv1OQX5slugBRs=
+-----END CERTIFICATE-----

+ 27 - 0
test/test_headers/certs/web.nginx-proxy.tld.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAlVbHDUilKzxlST8m4TgrYTBW5JLXY+Drraz5M5uyMfE5Ewvl
+Q3vFvYqFyNk92KxxuhbngZayq67GwL2+p9GWj7Kb37r5TaE7fiFKzbZF+W15UL8k
+j8FrwQkZW2LLlugEFCDo1BZiavI3wZbinVMFC1Id52iS24s2aM2NWwL/EvCsXQzE
+4HpVoklgn/9HH1JzVU3U8tFiovRQncn28UOz3FfhMXa04KRpfvJtNK65jXQme9n2
+BwDvSzZhs+96oTY6ttCe+LipDUwwou3rq2vrLuILKL73BLHp4ITWXTF3fNzSH9Qd
+cW9vbG0bvzHiW8NS0BT8i/tF6kHsysc7ZxLE3wIDAQABAoIBAEmK7IecKMq7+V0y
+3mC3GpXICmKR9cRX9XgX4LkLiZuSoXrBtuuevmhzGSMp6I0VjwQHV4a3wdFORs6Q
+Ip3eVvj5Ck4Jc9BJAFVC6+WWR6tnwACFwOmSZRAw/O3GH2B3bdrDwiT/yQPFuLN7
+LKoxQiCrFdLp6rh3PBosb9pMBXU7k/HUazIdgmSKg6/JIoo/4Gwyid04TF/4MI2l
+RscxtP5/ANtS8VgwBEqhgdafRJ4KnLEpgvswgIQvUKmduVhZQlzd0LMY8FbhKVqz
+Utg8gsXaTyH6df/nmgUIInxLMz/MKPnMkv99fS6Sp/hvYlGpLZFWBJ6unMq3lKEr
+LMbHfIECgYEAxB+5QWdVqG2r9loJlf8eeuNeMPml4P8Jmi5RKyJC7Cww6DMlMxOS
+78ZJfl4b3ZrWuyvhjOfX/aTq7kQaF1BI9o3KJBH8k6EtO4gI8KeNmDONyQk9zsrn
+ru8Zwr7hVbAo8fCXxCnmPzhDLsYg6f3BVOsQWoX2SFYKZ1GvkPfIReECgYEAwu6G
+qtgFb57Vim10ecfWGM6vrPxvyfqP+zlH/p4nR+aQ+2sFbt27D0B1byWBRZe4KQyw
+Vq6XiQ09Fk6MJr8E8iAr9GXPPHcqlYI6bbNc6YOP3jVSKut0tQdTUOHll4kYIY+h
+RS3VA3+BA//ADpWpywu+7RZRbaIECA+U2a224r8CgYB5PCMIixgoRaNHZeEHF+1/
+iY1wOOKRcxY8eOU0BLnZxHd3EiasrCzoi2pi80nGczDKAxYqRCcAZDHVl8OJJdf0
+kTGjmnrHx5pucmkUWn7s1vGOlGfgrQ0K1kLWX6hrj7m/1Tn7yOrLqbvd7hvqiTI5
+jBVP3/+eN5G2zIf61TC4AQKBgCX2Q92jojNhsF58AHHy+/vqzIWYx8CC/mVDe4TX
+kfjLqzJ7XhyAK/zFZdlWaX1/FYtRAEpxR+uV226rr1mgW7s3jrfS1/ADmRRyvyQ8
+CP0k9PCmW7EmF51lptEanRbMyRlIGnUZfuFmhF6eAO4WMXHsgKs1bHg4VCapuihG
+T1aLAoGACRGn1UxFuBGqtsh2zhhsBZE7GvXKJSk/eP7QJeEXUNpNjCpgm8kIZM5K
+GorpL7PSB8mwVlDl18TpMm3P7nz6YkJYte+HdjO7pg59H39Uvtg3tZnIrFxNxVNb
+YF62/yHfk2AyTgjQZQUSmDS84jq1zUK4oS90lxr+u8qwELTniMs=
+-----END RSA PRIVATE KEY-----

+ 81 - 0
test/test_headers/test_http.py

@@ -0,0 +1,81 @@
+import pytest
+
+def test_arbitrary_headers_are_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'Foo': 'Bar'})
+    assert r.status_code == 200
+    assert "Foo: Bar\n" in r.text
+
+
+##### Testing the handling of X-Forwarded-For #####
+
+def test_X_Forwarded_For_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-For:" in r.text
+
+def test_X_Forwarded_For_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'X-Forwarded-For': '1.2.3.4'})
+    assert r.status_code == 200
+    assert "X-Forwarded-For: 1.2.3.4, " in r.text
+
+
+##### Testing the handling of X-Forwarded-Proto #####
+
+def test_X_Forwarded_Proto_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-Proto: http" in r.text
+
+def test_X_Forwarded_Proto_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Proto': 'f00'})
+    assert r.status_code == 200
+    assert "X-Forwarded-Proto: f00\n" in r.text
+
+
+##### Testing the handling of X-Forwarded-Port #####
+
+def test_X_Forwarded_Port_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-Port: 80\n" in r.text
+
+def test_X_Forwarded_Port_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Port': '1234'})
+    assert r.status_code == 200
+    assert "X-Forwarded-Port: 1234\n" in r.text
+
+
+##### Testing the handling of X-Forwarded-Ssl #####
+
+def test_X_Forwarded_Ssl_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-Ssl: off\n" in r.text
+
+def test_X_Forwarded_Ssl_is_overwritten(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Ssl': 'f00'})
+    assert r.status_code == 200
+    assert "X-Forwarded-Ssl: off\n" in r.text
+
+
+##### Other headers
+
+def test_X_Real_IP_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Real-IP: " in r.text
+
+def test_Host_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "Host: web.nginx-proxy.tld" in r.text
+
+def test_httpoxy_safe(docker_compose, nginxproxy):
+    """
+    See https://httpoxy.org/
+    nginx-proxy should suppress the `Proxy` header
+    """
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'Proxy': 'tcp://some.hacker.com'})
+    assert r.status_code == 200
+    assert "Proxy:" not in r.text
+    

+ 13 - 0
test/test_headers/test_http.yml

@@ -0,0 +1,13 @@
+web:
+  image: web
+  expose:
+    - "80"
+  environment:
+    WEB_PORTS: 80
+    VIRTUAL_HOST: web.nginx-proxy.tld
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro

+ 82 - 0
test/test_headers/test_https.py

@@ -0,0 +1,82 @@
+import pytest
+
+
+def test_arbitrary_headers_are_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'Foo': 'Bar'})
+    assert r.status_code == 200
+    assert "Foo: Bar\n" in r.text
+
+
+##### Testing the handling of X-Forwarded-For #####
+
+def test_X_Forwarded_For_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-For:" in r.text
+
+def test_X_Forwarded_For_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'X-Forwarded-For': '1.2.3.4'})
+    assert r.status_code == 200
+    assert "X-Forwarded-For: 1.2.3.4, " in r.text
+
+
+##### Testing the handling of X-Forwarded-Proto #####
+
+def test_X_Forwarded_Proto_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-Proto: https" in r.text
+
+def test_X_Forwarded_Proto_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Proto': 'f00'})
+    assert r.status_code == 200
+    assert "X-Forwarded-Proto: f00\n" in r.text
+
+
+##### Testing the handling of X-Forwarded-Port #####
+
+def test_X_Forwarded_Port_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-Port: 443\n" in r.text
+
+def test_X_Forwarded_Port_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Port': '1234'})
+    assert r.status_code == 200
+    assert "X-Forwarded-Port: 1234\n" in r.text
+
+
+##### Testing the handling of X-Forwarded-Ssl #####
+
+def test_X_Forwarded_Ssl_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-Ssl: on\n" in r.text
+
+def test_X_Forwarded_Ssl_is_overwritten(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Ssl': 'f00'})
+    assert r.status_code == 200
+    assert "X-Forwarded-Ssl: on\n" in r.text
+
+
+##### Other headers
+
+def test_X_Real_IP_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Real-IP: " in r.text
+
+def test_Host_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "Host: web.nginx-proxy.tld" in r.text
+
+def test_httpoxy_safe(docker_compose, nginxproxy):
+    """
+    See https://httpoxy.org/
+    nginx-proxy should suppress the `Proxy` header
+    """
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'Proxy': 'tcp://some.hacker.com'})
+    assert r.status_code == 200
+    assert "Proxy:" not in r.text
+    

+ 15 - 0
test/test_headers/test_https.yml

@@ -0,0 +1,15 @@
+web:
+  image: web
+  expose:
+    - "80"
+  environment:
+    WEB_PORTS: 80
+    VIRTUAL_HOST: web.nginx-proxy.tld
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ./certs/web.nginx-proxy.tld.crt:/etc/nginx/certs/web.nginx-proxy.tld.crt:ro
+    - ./certs/web.nginx-proxy.tld.key:/etc/nginx/certs/web.nginx-proxy.tld.key:ro

+ 0 - 183
test/test_helpers.bash

@@ -1,183 +0,0 @@
-# Test if requirements are met
-(
-	type docker &>/dev/null || ( echo "docker is not available"; exit 1 )
-)>&2
-
-
-# set a few global variables
-SUT_IMAGE=jwilder/nginx-proxy:bats
-TEST_FILE=$(basename $BATS_TEST_FILENAME .bats)
-
-
-# load the Bats stdlib (see https://github.com/sstephenson/bats/pull/110)
-DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
-export BATS_LIB="${DIR}/lib/bats"
-load "${BATS_LIB}/batslib.bash"
-
-
-# load additional bats helpers
-load ${DIR}/lib/helpers.bash
-load ${DIR}/lib/docker_helpers.bash
-
-
-# Define functions specific to our test suite
-
-# run the SUT docker container 
-# and makes sure it remains started
-# and displays the nginx-proxy start logs
-#
-# $1 container name
-# $@ other options for the `docker run` command
-function nginxproxy {
-	local -r container_name=$1
-	shift
-	docker_clean $container_name \
-	&& docker run -d \
-		--label bats-type="nginx-proxy" \
-		--name $container_name \
-		-v $DIR/lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro \
-		"$@" \
-		$SUT_IMAGE \
-	&& wait_for_nginxproxy_container_to_start $container_name \
-	&& docker logs $container_name
-}
-
-
-# wait until the nginx-proxy container is ready to operate
-#
-# $1 container name
-function wait_for_nginxproxy_container_to_start {
-	local -r container_name=$1
-	sleep .5s  # give time to eventually fail to initialize
-
-	function is_running {
-		run docker_running_state $container_name
-		assert_output "true"
-	}
-	retry 3 1 is_running
-}
-
-
-# Send a HTTP request to container $1 for path $2 and 
-# Additional curl options can be passed as $@
-#
-# $1 container name
-# $2 HTTP path to query
-# $@ additional options to pass to the curl command
-function curl_container {
-	local -r container=$1
-	local -r path=$2
-	shift 2
-	docker run --label bats-type="curl" appropriate/curl --silent \
-		--connect-timeout 5 \
-		--max-time 20 \
-		"$@" \
-		http://$(docker_ip $container)${path}
-}
-
-# Send a HTTPS request to container $1 for path $2 and 
-# Additional curl options can be passed as $@
-#
-# $1 container name
-# $2 HTTPS path to query
-# $@ additional options to pass to the curl command
-function curl_container_https {
-	local -r container=$1
-	local -r path=$2
-	shift 2
-	docker run --label bats-type="curl" appropriate/curl --silent \
-		--connect-timeout 5 \
-		--max-time 20 \
-		--insecure \
-		"$@" \
-		https://$(docker_ip $container)${path}
-}
-
-# start a container running (one or multiple) webservers listening on given ports
-#
-# $1 container name
-# $2 container port(s). If multiple ports, provide them as a string: "80 90" with a space as a separator
-# $@ `docker run` additional options
-function prepare_web_container {
-	local -r container_name=$1
-	local -r ports=$2
-	shift 2
-	local -r options="$@"
-
-	local expose_option=""
-	IFS=$' \t\n' # See https://github.com/sstephenson/bats/issues/89
-	for port in $ports; do
-		expose_option="${expose_option}--expose=$port "
-	done
-
-	(	# used for debugging purpose. Will be display if test fails
-		echo "container_name: $container_name"
-		echo "ports: $ports"
-		echo "options: $options"
-		echo "expose_option: $expose_option"
-	)>&2
-	
-	docker_clean $container_name
-
-	# GIVEN a container exposing 1 webserver on ports 1234
-	run docker run -d \
-		--label bats-type="web" \
-		--name $container_name \
-		$expose_option \
-		-w /var/www/ \
-		-v $DIR/web_helpers:/var/www:ro \
-		$options \
-		-e PYTHON_PORTS="$ports" \
-		python:3 bash -c "
-			trap '[ \${#PIDS[@]} -gt 0 ] && kill -TERM \${PIDS[@]}' TERM
-			declare -a PIDS
-			for port in \$PYTHON_PORTS; do
-				echo starting a web server listening on port \$port;
-				./webserver.py \$port &
-				PIDS+=(\$!)
-			done
-			wait \${PIDS[@]}
-			trap - TERM
-			wait \${PIDS[@]}
-		"
-	assert_success
-
-	# THEN querying directly port works
-	IFS=$' \t\n' # See https://github.com/sstephenson/bats/issues/89
-	for port in $ports; do
-		run retry 5 1s docker run --label bats-type="curl" appropriate/curl --silent --fail http://$(docker_ip $container_name):$port/port
-		assert_output "answer from port $port"
-	done
-}
-
-# stop all containers with the "bats-type" label (matching the optionally supplied value)
-#
-# $1 optional label value
-function stop_bats_containers {
-	local -r value=$1
-
-	if [ -z "$value" ]; then
-		CIDS=( $(docker ps -q --filter "label=bats-type") )
-	else
-		CIDS=( $(docker ps -q --filter "label=bats-type=$value") )
-	fi
-
-	if [ ${#CIDS[@]} -gt 0 ]; then
-		docker stop ${CIDS[@]} >&2
-	fi
-}
-
-# wait for a docker-gen container to receive a specified event from a
-# container with the specified ID/name
-#
-# $1 docker-gen container name
-# $2 event
-# $3 ID/name of container to receive event from
-function dockergen_wait_for_event {
-	local -r container=$1
-	local -r event=$2
-	local -r other=$3
-	local -r did=$(docker_id "$other")
-	docker_wait_for_log "$container" 9 "Received event $event for container ${did:0:12}"
-}
-

+ 35 - 0
test/test_ipv6.py

@@ -0,0 +1,35 @@
+import pytest
+
+
+def test_unknown_virtual_host_ipv4(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/port")
+    assert r.status_code == 503
+
+
+def test_forwards_to_web1_ipv4(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.tld/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+
+
+def test_forwards_to_web2_ipv4(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert r.text == "answer from port 82\n" 
+
+
+def test_unknown_virtual_host_ipv6(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/port", ipv6=True)
+    assert r.status_code == 503
+
+
+def test_forwards_to_web1_ipv6(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.tld/port", ipv6=True)
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+
+
+def test_forwards_to_web2_ipv6(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.tld/port", ipv6=True)
+    assert r.status_code == 200
+    assert r.text == "answer from port 82\n" 

+ 23 - 0
test/test_ipv6.yml

@@ -0,0 +1,23 @@
+web1:
+  image: web 
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: 81
+    VIRTUAL_HOST: web1.nginx-proxy.tld
+
+web2:
+  image: web
+  expose: 
+    - "82"
+  environment:
+    WEB_PORTS: 82
+    VIRTUAL_HOST: web2.nginx-proxy.tld
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+  environment:
+    ENABLE_IPV6: "true"

+ 16 - 0
test/test_multiple-hosts.py

@@ -0,0 +1,16 @@
+import pytest
+
+
+def test_unknown_virtual_host_is_503(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://unknown.nginx-proxy.tld/port")
+    assert r.status_code == 503
+
+def test_webA_is_forwarded(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://webA.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert r.text == "answer from port 81\n"
+
+def test_webB_is_forwarded(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://webB.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert r.text == "answer from port 81\n"

+ 13 - 0
test/test_multiple-hosts.yml

@@ -0,0 +1,13 @@
+web:
+  image: web
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: 81
+    VIRTUAL_HOST: webA.nginx-proxy.tld,webB.nginx-proxy.tld
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro

+ 15 - 0
test/test_multiple-networks.py

@@ -0,0 +1,15 @@
+import pytest
+
+def test_unknown_virtual_host(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+
+def test_forwards_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+
+def test_forwards_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 82\n"

+ 34 - 0
test/test_multiple-networks.yml

@@ -0,0 +1,34 @@
+version: '2'
+
+networks:
+  net1: {}
+  net2: {}
+
+services:
+  nginx-proxy:
+    image: jwilder/nginx-proxy:test
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+    networks:
+      - net1
+      - net2
+
+  web1:
+    image: web 
+    expose:
+      - "81"
+    environment:
+      WEB_PORTS: 81
+      VIRTUAL_HOST: web1.nginx-proxy.local
+    networks:
+      - net1
+
+  web2:
+    image: web 
+    expose:
+      - "82"
+    environment:
+      WEB_PORTS: 82
+      VIRTUAL_HOST: web2.nginx-proxy.local
+    networks:
+      - net2

+ 7 - 0
test/test_multiple-ports/test_VIRTUAL_PORT.py

@@ -0,0 +1,7 @@
+import pytest
+
+
+def test_answer_is_served_from_chosen_port(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert "answer from port 90\n" in r.text

+ 14 - 0
test/test_multiple-ports/test_VIRTUAL_PORT.yml

@@ -0,0 +1,14 @@
+web:
+  image: web
+  expose:
+    - "80"
+    - "90"
+  environment:
+    WEB_PORTS: "80 90"
+    VIRTUAL_HOST: "web.nginx-proxy.tld"
+    VIRTUAL_PORT: 90
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro

+ 7 - 0
test/test_multiple-ports/test_default-80.py

@@ -0,0 +1,7 @@
+import pytest
+
+
+def test_answer_is_served_from_port_80_by_default(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert "answer from port 80\n" in r.text

+ 13 - 0
test/test_multiple-ports/test_default-80.yml

@@ -0,0 +1,13 @@
+web:
+  image: web
+  expose:
+    - "80"
+    - "81"
+  environment:
+    WEB_PORTS: "80 81"
+    VIRTUAL_HOST: "web.nginx-proxy.tld"
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro

+ 7 - 0
test/test_multiple-ports/test_single-port-not-80.py

@@ -0,0 +1,7 @@
+import pytest
+
+
+def test_answer_is_served_from_exposed_port_even_if_not_80(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert "answer from port 81\n" in r.text

+ 13 - 0
test/test_multiple-ports/test_single-port-not-80.yml

@@ -0,0 +1,13 @@
+web:
+  image: web
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: "81"
+    VIRTUAL_HOST: "web.nginx-proxy.tld"
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro

+ 24 - 0
test/test_nominal.py

@@ -0,0 +1,24 @@
+import pytest
+from requests import ConnectionError
+
+
+def test_unknown_virtual_host(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/port")
+    assert r.status_code == 503
+
+
+def test_forwards_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.tld/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+
+
+def test_forwards_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert r.text == "answer from port 82\n" 
+
+
+def test_ipv6_is_disabled_by_default(docker_compose, nginxproxy):
+    with pytest.raises(ConnectionError):
+        nginxproxy.get("http://nginx-proxy/port", ipv6=True)

+ 21 - 0
test/test_nominal.yml

@@ -0,0 +1,21 @@
+web1:
+  image: web 
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: 81
+    VIRTUAL_HOST: web1.nginx-proxy.tld
+
+web2:
+  image: web
+  expose: 
+    - "82"
+  environment:
+    WEB_PORTS: 82
+    VIRTUAL_HOST: web2.nginx-proxy.tld
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro

+ 70 - 0
test/test_ssl/certs/nginx-proxy.tld.crt

@@ -0,0 +1,70 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 4096 (0x1000)
+    Signature Algorithm: sha256WithRSAEncryption
+        Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
+        Validity
+            Not Before: Jan 10 00:08:52 2017 GMT
+            Not After : May 28 00:08:52 2044 GMT
+        Subject: CN=*.nginx-proxy.tld
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:cb:45:f4:14:9b:fe:64:85:79:4a:36:8d:3d:d1:
+                    27:d0:7c:36:28:30:e6:73:80:6f:7c:49:23:d0:6c:
+                    17:e4:44:c0:77:4d:9a:c2:bc:24:84:e3:a5:4d:ba:
+                    d2:da:51:7b:a1:2a:12:d4:c0:19:55:69:2c:22:27:
+                    2d:1a:f6:fc:4b:7f:e9:cb:a8:3c:e8:69:b8:d2:4f:
+                    de:4e:50:e2:d0:74:30:7c:42:5a:ae:aa:85:a5:b1:
+                    71:4d:c9:7e:86:8b:62:8c:3e:0d:e3:3b:c3:f5:81:
+                    0b:8c:68:79:fe:bf:10:fb:ae:ec:11:49:6d:64:5e:
+                    1a:7d:b3:92:93:4e:96:19:3a:98:04:a7:66:b2:74:
+                    61:2d:41:13:0c:a4:54:0d:2c:78:fd:b4:a3:e8:37:
+                    78:9a:de:fa:bc:2e:a8:0f:67:14:58:ce:c3:87:d5:
+                    14:0e:8b:29:7d:48:19:b2:a9:f5:b4:e8:af:32:21:
+                    67:15:7e:43:52:8b:20:cf:9f:38:43:bf:fd:c8:24:
+                    7f:52:a3:88:f2:f1:4a:14:91:2a:6e:91:6f:fb:7d:
+                    6a:78:c6:6d:2e:dd:1e:4c:2b:63:bb:3a:43:9c:91:
+                    f9:df:d3:08:13:63:86:7d:ce:e8:46:cf:f1:6c:1f:
+                    ca:f7:4c:de:d8:4b:e0:da:bc:06:d9:87:0f:ff:96:
+                    45:85
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Subject Alternative Name: 
+                DNS:*.nginx-proxy.tld
+    Signature Algorithm: sha256WithRSAEncryption
+         6e:a5:0e:e4:d3:cc:d5:b7:fc:34:75:89:4e:98:8c:e7:08:06:
+         a8:5b:ec:13:7d:83:99:a2:61:b8:d5:12:6e:c5:b4:53:4e:9a:
+         22:cd:ad:14:30:6a:7d:58:d7:23:d9:a4:2a:96:a0:40:9e:50:
+         9f:ce:f2:fe:8c:dd:9a:ac:99:39:5b:89:2d:ca:e5:3e:c3:bc:
+         03:04:1c:12:d9:6e:b8:9f:f0:3a:be:12:44:7e:a4:21:86:73:
+         af:d5:00:51:3f:2c:56:70:34:8f:26:b0:7f:b0:cf:cf:7f:f9:
+         40:6f:00:29:c4:cf:c3:b7:c2:49:3d:3f:b0:26:78:87:b9:c7:
+         6c:1b:aa:6a:1a:dd:c5:eb:f2:69:ba:6d:46:0b:92:49:b5:11:
+         3c:eb:48:c7:2f:fb:33:a6:6a:82:a2:ab:f8:1e:5f:7d:e3:b7:
+         f2:fd:f5:88:a5:09:4d:a0:bc:f4:3b:cd:d2:8b:d7:57:1f:86:
+         3b:d2:3e:a4:92:21:b0:02:0b:e9:e0:c4:1c:f1:78:e2:58:a7:
+         26:5f:4c:29:c8:23:f0:6e:12:3f:bd:ad:44:7b:0b:bd:db:ba:
+         63:8d:07:c6:9d:dc:46:cc:63:40:ba:5e:45:82:dd:9a:e5:50:
+         e8:e7:d7:27:88:fc:6f:1d:8a:e7:5c:49:28:aa:10:29:75:28:
+         c7:52:de:f9
+-----BEGIN CERTIFICATE-----
+MIIC9zCCAd+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
+bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
+ZDAeFw0xNzAxMTAwMDA4NTJaFw00NDA1MjgwMDA4NTJaMBwxGjAYBgNVBAMMESou
+bmdpbngtcHJveHkudGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+y0X0FJv+ZIV5SjaNPdEn0Hw2KDDmc4BvfEkj0GwX5ETAd02awrwkhOOlTbrS2lF7
+oSoS1MAZVWksIictGvb8S3/py6g86Gm40k/eTlDi0HQwfEJarqqFpbFxTcl+hoti
+jD4N4zvD9YELjGh5/r8Q+67sEUltZF4afbOSk06WGTqYBKdmsnRhLUETDKRUDSx4
+/bSj6Dd4mt76vC6oD2cUWM7Dh9UUDospfUgZsqn1tOivMiFnFX5DUosgz584Q7/9
+yCR/UqOI8vFKFJEqbpFv+31qeMZtLt0eTCtjuzpDnJH539MIE2OGfc7oRs/xbB/K
+90ze2Evg2rwG2YcP/5ZFhQIDAQABoyAwHjAcBgNVHREEFTATghEqLm5naW54LXBy
+b3h5LnRsZDANBgkqhkiG9w0BAQsFAAOCAQEAbqUO5NPM1bf8NHWJTpiM5wgGqFvs
+E32DmaJhuNUSbsW0U06aIs2tFDBqfVjXI9mkKpagQJ5Qn87y/ozdmqyZOVuJLcrl
+PsO8AwQcEtluuJ/wOr4SRH6kIYZzr9UAUT8sVnA0jyawf7DPz3/5QG8AKcTPw7fC
+ST0/sCZ4h7nHbBuqahrdxevyabptRguSSbURPOtIxy/7M6ZqgqKr+B5ffeO38v31
+iKUJTaC89DvN0ovXVx+GO9I+pJIhsAIL6eDEHPF44linJl9MKcgj8G4SP72tRHsL
+vdu6Y40Hxp3cRsxjQLpeRYLdmuVQ6OfXJ4j8bx2K51xJKKoQKXUox1Le+Q==
+-----END CERTIFICATE-----

+ 27 - 0
test/test_ssl/certs/nginx-proxy.tld.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAy0X0FJv+ZIV5SjaNPdEn0Hw2KDDmc4BvfEkj0GwX5ETAd02a
+wrwkhOOlTbrS2lF7oSoS1MAZVWksIictGvb8S3/py6g86Gm40k/eTlDi0HQwfEJa
+rqqFpbFxTcl+hotijD4N4zvD9YELjGh5/r8Q+67sEUltZF4afbOSk06WGTqYBKdm
+snRhLUETDKRUDSx4/bSj6Dd4mt76vC6oD2cUWM7Dh9UUDospfUgZsqn1tOivMiFn
+FX5DUosgz584Q7/9yCR/UqOI8vFKFJEqbpFv+31qeMZtLt0eTCtjuzpDnJH539MI
+E2OGfc7oRs/xbB/K90ze2Evg2rwG2YcP/5ZFhQIDAQABAoIBAQCjAro2PNLJMfCO
+fyjNRgmzu6iCmpR0U68T8GN0JPsT576g7e8J828l0pkhuIyW33lRSThIvLSUNf9a
+dChL032H3lBTLduKVh4NKleQXnVFzaeEPoISSFVdButiAhAhPW4OIUVp0OfY3V+x
+fac3j2nDLAfL5SKAtqZv363Py9m66EBYm5BmGTQqT/frQWeCEBvlErQef5RIaU8p
+e2zMWgSNNojVai8U3nKNRvYHWeWXM6Ck7lCvkHhMF+RpbmCZuqhbEARVnehU/Jdn
+QHJ3nxeA2OWpoWKXvAHtSnno49yxq1UIstiQvY+ng5C5i56UlB60UiU2NJ6doZkB
+uQ7/1MaBAoGBAORdcFtgdgRALjXngFWhpCp0CseyUehn1KhxDCG+D1pJ142/ymcf
+oJOzKJPMRNDdDUBMnR1GBfy7rmwvYevI/SMNy2Qs7ofcXPbdtwwvTCToZ1V9/54k
+VfuPBFT+3QzWRvG1tjTV3E4L2VV3nrl2qNPhE5DlfIaU3nQq5Fl0HprJAoGBAOPf
+MWOTGev61CdODO5KN3pLAoamiPs5lEUlz3kM3L1Q52YLITxNDjRj9hWBUATJZOS2
+pLOoYRwmhD7vrnimMc41+NuuFX+4T7hWPc8uSuOxX0VijYtULyNRK57mncG1Fq9M
+RMLbOJ7FD+8jdXNsSMqpQ+pxLJRX/A10O2fOQnbdAoGAL5hV4YWSM0KZHvz332EI
+ER0MXiCJN7HkPZMKH0I4eu3m8hEmAyYxVndBnsQ1F37q0xrkqAQ/HTSUntGlS/og
+4Bxw5pkCwegoq/77tpto+ExDtSrEitYx4XMmSPyxX4qNULU5m3tzJgUML+b1etwD
+Rd2kMU/TC02dq4KBAy/TbRkCgYAl1xN5iJz+XenLGR/2liZ+TWR+/bqzlU006mF4
+pZUmbv/uJxz+yYD5XDwqOA4UrWjuvhG9r9FoflDprp2XdWnB556KxG7XhcDfSJr9
+A5/2DadXe1Ur9O/a+oi2228JEsxQkea9QPA3FVxfBtFjOHEiDlez39VaUP4PMeUH
+iO3qlQKBgFQhdTb7HeYnApYIDHLmd1PvjRvp8XKR1CpEN0nkw8HpHcT1q1MUjQCr
+iT6FQupULEvGmO3frQsgVeRIQDbEdZK3C5xCtn6qOw70sYATVf361BbTtidmU9yV
+THFxwDSVLiVZgFryoY/NtAc27sVdJnGsPRjjaeVgALAsLbmZ1K/H
+-----END RSA PRIVATE KEY-----

+ 71 - 0
test/test_ssl/certs/web2.nginx-proxy.tld.crt

@@ -0,0 +1,71 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 4096 (0x1000)
+    Signature Algorithm: sha256WithRSAEncryption
+        Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
+        Validity
+            Not Before: Jan 10 00:37:02 2017 GMT
+            Not After : May 28 00:37:02 2044 GMT
+        Subject: CN=web2.nginx-proxy.tld
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:da:ee:46:2d:44:7c:f1:e6:91:11:bf:34:d6:02:
+                    4e:fe:43:23:fb:6d:20:f7:8d:1b:c6:9c:cd:1c:1a:
+                    07:95:c2:ed:b9:23:73:c1:02:2b:50:51:3f:33:cf:
+                    8e:aa:f1:13:58:4c:ff:7f:7d:4a:87:fc:f0:0f:54:
+                    af:8c:eb:ba:b4:0f:71:5e:12:1f:64:b1:3d:83:88:
+                    dd:9c:09:50:2d:37:1d:03:3b:18:30:36:f4:82:94:
+                    87:7f:31:27:28:84:0c:99:6d:77:b5:b8:4f:ca:83:
+                    58:d5:d8:4e:36:73:1c:1a:5c:ed:05:ef:95:60:03:
+                    28:0c:9f:d8:6f:98:a8:cd:08:be:af:b1:95:5a:60:
+                    96:fe:2a:d0:98:74:9b:4a:c0:48:66:73:67:54:33:
+                    11:22:20:ea:11:05:ba:a6:ed:74:12:05:d8:de:4f:
+                    01:46:39:74:d8:34:1a:f2:2c:c2:df:6d:94:57:52:
+                    c1:e4:2e:1b:8e:12:0e:43:e7:6f:1f:da:51:80:35:
+                    c2:8a:9b:b6:2a:30:39:6b:a0:77:fa:37:11:b7:ec:
+                    de:6e:8c:6f:93:81:5e:2d:90:69:1b:4b:a4:80:ca:
+                    f4:e5:5b:c0:13:45:b9:76:70:ed:d3:4e:dd:66:98:
+                    99:9f:9d:f0:1e:fd:dd:04:4f:9a:55:bc:38:ad:42:
+                    b9:dd
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Subject Alternative Name: 
+                DNS:web2.nginx-proxy.tld
+    Signature Algorithm: sha256WithRSAEncryption
+         38:d6:8c:be:3c:5e:5d:61:02:77:eb:5b:6e:a7:1d:4f:69:0d:
+         54:bd:dd:3a:1a:8e:9d:a0:c2:f3:a5:31:91:3e:ec:7a:69:48:
+         40:27:45:a5:c6:b9:af:6d:d9:76:24:97:ec:c5:cf:4d:cc:49:
+         93:ab:bc:4f:01:7e:7a:57:73:4d:27:62:a6:68:bf:4c:00:2c:
+         c0:f3:7b:b3:32:81:6b:96:20:0a:73:a0:85:b5:f8:07:10:88:
+         e8:62:85:41:63:df:43:c5:f9:4b:90:89:6a:16:d9:a6:85:4a:
+         04:1b:5e:75:d8:84:ae:69:c7:62:8f:f1:53:c8:c6:31:71:6d:
+         0c:05:2d:bf:6e:b8:b8:7a:8c:73:6f:17:bb:5c:5a:67:51:12:
+         af:e2:17:9c:40:0e:35:f6:59:93:69:45:14:74:33:c6:aa:04:
+         76:8e:3c:a6:ac:f4:60:cb:53:eb:d6:63:1a:47:7b:be:11:8d:
+         74:de:e8:e5:bc:de:1b:09:db:1b:87:89:b7:6a:20:0a:5e:fb:
+         35:04:ab:b2:f7:4d:a1:86:65:1d:c7:c3:aa:30:15:3a:6e:66:
+         f8:43:33:63:ac:fc:c1:58:48:5b:ec:a0:00:bf:d4:f1:06:03:
+         79:ca:d5:b6:f2:39:0b:62:b4:fd:27:27:e9:37:52:21:ce:a4:
+         32:8a:bb:c7
+-----BEGIN CERTIFICATE-----
+MIIC/TCCAeWgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
+bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
+ZDAeFw0xNzAxMTAwMDM3MDJaFw00NDA1MjgwMDM3MDJaMB8xHTAbBgNVBAMMFHdl
+YjIubmdpbngtcHJveHkudGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEA2u5GLUR88eaREb801gJO/kMj+20g940bxpzNHBoHlcLtuSNzwQIrUFE/M8+O
+qvETWEz/f31Kh/zwD1SvjOu6tA9xXhIfZLE9g4jdnAlQLTcdAzsYMDb0gpSHfzEn
+KIQMmW13tbhPyoNY1dhONnMcGlztBe+VYAMoDJ/Yb5iozQi+r7GVWmCW/irQmHSb
+SsBIZnNnVDMRIiDqEQW6pu10EgXY3k8BRjl02DQa8izC322UV1LB5C4bjhIOQ+dv
+H9pRgDXCipu2KjA5a6B3+jcRt+zeboxvk4FeLZBpG0ukgMr05VvAE0W5dnDt007d
+ZpiZn53wHv3dBE+aVbw4rUK53QIDAQABoyMwITAfBgNVHREEGDAWghR3ZWIyLm5n
+aW54LXByb3h5LnRsZDANBgkqhkiG9w0BAQsFAAOCAQEAONaMvjxeXWECd+tbbqcd
+T2kNVL3dOhqOnaDC86UxkT7semlIQCdFpca5r23ZdiSX7MXPTcxJk6u8TwF+eldz
+TSdipmi/TAAswPN7szKBa5YgCnOghbX4BxCI6GKFQWPfQ8X5S5CJahbZpoVKBBte
+ddiErmnHYo/xU8jGMXFtDAUtv264uHqMc28Xu1xaZ1ESr+IXnEAONfZZk2lFFHQz
+xqoEdo48pqz0YMtT69ZjGkd7vhGNdN7o5bzeGwnbG4eJt2ogCl77NQSrsvdNoYZl
+HcfDqjAVOm5m+EMzY6z8wVhIW+ygAL/U8QYDecrVtvI5C2K0/Scn6TdSIc6kMoq7
+xw==
+-----END CERTIFICATE-----

+ 27 - 0
test/test_ssl/certs/web2.nginx-proxy.tld.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEA2u5GLUR88eaREb801gJO/kMj+20g940bxpzNHBoHlcLtuSNz
+wQIrUFE/M8+OqvETWEz/f31Kh/zwD1SvjOu6tA9xXhIfZLE9g4jdnAlQLTcdAzsY
+MDb0gpSHfzEnKIQMmW13tbhPyoNY1dhONnMcGlztBe+VYAMoDJ/Yb5iozQi+r7GV
+WmCW/irQmHSbSsBIZnNnVDMRIiDqEQW6pu10EgXY3k8BRjl02DQa8izC322UV1LB
+5C4bjhIOQ+dvH9pRgDXCipu2KjA5a6B3+jcRt+zeboxvk4FeLZBpG0ukgMr05VvA
+E0W5dnDt007dZpiZn53wHv3dBE+aVbw4rUK53QIDAQABAoIBABeTCsl7E30017Ay
+h6z3yKvGbQx43tDpR/FmFwwMnX555AFImQFSi3l1ljmtAu7TUML0X5rJ0gm8qdjs
+xI6HH66d7xYzG2BLWZVdWoef1RtZUO11IpCmikO5XLHMiCvrtDOdPwO5WhYzeJBm
+X12rnX4VPYyjFNGm5Vwepj62EI8rS9G3NG00pDYPmN/vUQJiV/iTRIlvXgFm4Hl2
+zJhVi+NhyiptFEycdg65JwVfLKtmUXRmwGFiSxQi1FX8YS5EdIV2S8yDwXlWSxmq
+4R1eSID7pKxtzyRtBqZJggzfqtY8cMpoOC12FbLAvzagOavs4ntMgAVy5k2T15G2
+syQyLSECgYEA+1NIRF3CxNUaPvpcR8Y4PWhwDEzqn5ZscnXaFrUp1W72f3bpwSCa
+/t9lXe9O53R5/yt4nCbwvVM0UWYPHGZGQr+5AS7WWDVWVcwkXX1NIjALi0TXQ0Ty
+zeuViXDofUS31yhwFFmgGa52pd+edXaGRvxzGyPVdtwpSLZP/gBLQykCgYEA3wC9
+sHNPKMxi6vI2VBvmBXHoCSDAkuRLmQEGDmjjt0Ve4fmwGRbBJxniOlNtufNQRfRg
+67qaeM4BTrP03cilZ71yXvLN5G2opY5c/I0dYTXRhV3IV6XwlCC+0xmn+ro7kB8x
+J4wAj/h/tJ8T0+0TpnbyjmPH4KTJ9GjRTKwe65UCgYBszIHlbr2JXkONbe6S98GS
+++o9uPJ9Aa6S4mf2Gpkwl2fIiF7rR0Ux/t2wC5AZ7Ld/en8tAkKHg0SL1GXIQpI6
+BSt+0prh9r0YSVaYzkyc9zWYJcYWjfuan1jN9f3/dMctMolKlf4UAA3HAwZjDVtV
+0aW24w1e9jI9EweQCuqJ+QKBgBwZec18GiNn7abxMktS4J8bBUPxLpLT1XrIGD1E
+lj0HrrcGwVvH9Dq7FjiHPrJJqHnIG1ZYwxIp0xxZrKctmzoBMyInsi3wa2nBEJJ6
+LZOMNoR5lr8El9XyclkjSHldchfs9kKnb4K0q1LVIKh5nRpCrrmmdQ8ndJMpigYB
+QjwpAoGAWIhFrN0Acdq7Xc9pScqnAohPMWCTBUbeKrOm0ZwN4Q9D+lLeeggbWlWy
+AqR4WHQMHc6B+p4Ncg3XBCFw0V62PkYhSCdaLNH3CPyFT0qoeY8VuMjmqS7yoKvp
+uMMHfzMmyGg0dyplVGgANafb/it6Dp8T0pnCmzxhe57jf9xsVBo=
+-----END RSA PRIVATE KEY-----

+ 71 - 0
test/test_ssl/certs/web3.nginx-proxy.tld.crt

@@ -0,0 +1,71 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 4096 (0x1000)
+    Signature Algorithm: sha256WithRSAEncryption
+        Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
+        Validity
+            Not Before: Jan 10 00:58:11 2017 GMT
+            Not After : May 28 00:58:11 2044 GMT
+        Subject: CN=web3.nginx-proxy.tld
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:ac:9b:90:dc:2c:0d:c3:ae:f4:a0:cc:40:15:d2:
+                    c4:c2:2c:8c:15:b6:70:28:cf:32:c4:03:ce:b3:87:
+                    30:5d:a6:12:96:69:7a:fe:67:29:1c:8c:24:bb:6a:
+                    c8:86:13:19:91:00:3e:ef:00:67:50:b0:ea:c9:93:
+                    c6:8a:73:82:d8:37:9f:8e:6e:12:13:ec:fa:08:0f:
+                    ac:73:6e:42:96:67:9f:20:c5:1c:a3:b1:4a:83:36:
+                    0e:0a:31:93:76:b1:b6:37:4f:e0:88:3c:46:dc:c1:
+                    53:60:df:28:ae:3e:20:d8:d9:53:a2:be:09:38:f0:
+                    ff:4a:91:45:cb:cb:b5:b3:3d:7d:09:98:47:dc:0d:
+                    5e:83:73:b6:c9:f3:fb:9a:f2:bb:b0:62:ca:aa:af:
+                    6b:42:e5:08:b2:14:87:f4:fc:f1:14:f8:cc:76:b3:
+                    c0:49:df:66:c6:21:a0:bc:5e:0c:bb:de:e9:9c:e7:
+                    fb:31:a1:cf:c4:e9:bb:bd:c3:5a:0d:23:52:c6:b3:
+                    84:77:f1:0c:3d:ca:c3:60:48:f9:7e:a6:dc:4f:f7:
+                    d2:5b:7c:02:4d:38:09:64:33:7e:bb:b1:65:bb:e2:
+                    2b:1d:9a:49:d4:34:21:42:7a:49:3e:11:6c:10:66:
+                    b4:91:db:bd:3a:c2:8d:f4:e4:03:b1:b4:6e:5c:98:
+                    bf:7d
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Subject Alternative Name: 
+                DNS:web3.nginx-proxy.tld
+    Signature Algorithm: sha256WithRSAEncryption
+         9a:f6:b9:c2:08:a4:b4:d7:e4:b2:d3:22:e9:fe:69:4a:e8:76:
+         18:60:11:1b:3b:7c:4b:c3:72:66:95:b7:7c:de:c7:34:32:58:
+         aa:5d:e0:12:f0:df:27:b6:3f:dd:f1:8c:ed:ce:bd:73:50:fc:
+         9b:e1:8c:c2:7f:ac:6b:54:9d:23:0a:d9:a6:25:cc:99:94:73:
+         2b:69:e8:f7:07:40:37:d3:d4:0b:14:86:6a:a1:01:53:4b:ae:
+         85:2d:12:13:bd:23:1e:09:cf:20:50:24:76:a6:5f:b4:d6:d6:
+         e1:34:40:93:4d:42:f7:d0:95:98:77:53:16:e7:ce:cc:4c:35:
+         b8:30:3b:62:95:e2:40:0c:a1:73:84:92:93:63:df:c6:21:d5:
+         eb:1d:a1:d0:f2:ec:a5:cf:d6:10:c9:8f:ac:11:16:39:cd:b0:
+         38:04:29:cf:e1:1c:dd:21:df:1f:95:35:a5:a4:61:2b:3d:8b:
+         8a:76:02:6d:31:a1:e8:6b:c5:3b:eb:90:40:34:16:5a:07:93:
+         96:56:cd:8b:52:ca:65:51:78:d3:f2:af:40:44:43:ec:fe:a2:
+         c8:d4:6d:21:c7:1f:d2:45:28:0d:d2:51:0f:d1:a5:db:00:2a:
+         3a:10:88:9e:5a:76:a2:f7:e2:f0:fe:14:a3:e8:ec:ef:00:f0:
+         35:87:eb:7c
+-----BEGIN CERTIFICATE-----
+MIIC/TCCAeWgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
+bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
+ZDAeFw0xNzAxMTAwMDU4MTFaFw00NDA1MjgwMDU4MTFaMB8xHTAbBgNVBAMMFHdl
+YjMubmdpbngtcHJveHkudGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEArJuQ3CwNw670oMxAFdLEwiyMFbZwKM8yxAPOs4cwXaYSlml6/mcpHIwku2rI
+hhMZkQA+7wBnULDqyZPGinOC2Defjm4SE+z6CA+sc25ClmefIMUco7FKgzYOCjGT
+drG2N0/giDxG3MFTYN8orj4g2NlTor4JOPD/SpFFy8u1sz19CZhH3A1eg3O2yfP7
+mvK7sGLKqq9rQuUIshSH9PzxFPjMdrPASd9mxiGgvF4Mu97pnOf7MaHPxOm7vcNa
+DSNSxrOEd/EMPcrDYEj5fqbcT/fSW3wCTTgJZDN+u7Flu+IrHZpJ1DQhQnpJPhFs
+EGa0kdu9OsKN9OQDsbRuXJi/fQIDAQABoyMwITAfBgNVHREEGDAWghR3ZWIzLm5n
+aW54LXByb3h5LnRsZDANBgkqhkiG9w0BAQsFAAOCAQEAmva5wgiktNfkstMi6f5p
+Suh2GGARGzt8S8NyZpW3fN7HNDJYql3gEvDfJ7Y/3fGM7c69c1D8m+GMwn+sa1Sd
+IwrZpiXMmZRzK2no9wdAN9PUCxSGaqEBU0uuhS0SE70jHgnPIFAkdqZftNbW4TRA
+k01C99CVmHdTFufOzEw1uDA7YpXiQAyhc4SSk2PfxiHV6x2h0PLspc/WEMmPrBEW
+Oc2wOAQpz+Ec3SHfH5U1paRhKz2LinYCbTGh6GvFO+uQQDQWWgeTllbNi1LKZVF4
+0/KvQERD7P6iyNRtIccf0kUoDdJRD9Gl2wAqOhCInlp2ovfi8P4Uo+js7wDwNYfr
+fA==
+-----END CERTIFICATE-----

+ 27 - 0
test/test_ssl/certs/web3.nginx-proxy.tld.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEArJuQ3CwNw670oMxAFdLEwiyMFbZwKM8yxAPOs4cwXaYSlml6
+/mcpHIwku2rIhhMZkQA+7wBnULDqyZPGinOC2Defjm4SE+z6CA+sc25ClmefIMUc
+o7FKgzYOCjGTdrG2N0/giDxG3MFTYN8orj4g2NlTor4JOPD/SpFFy8u1sz19CZhH
+3A1eg3O2yfP7mvK7sGLKqq9rQuUIshSH9PzxFPjMdrPASd9mxiGgvF4Mu97pnOf7
+MaHPxOm7vcNaDSNSxrOEd/EMPcrDYEj5fqbcT/fSW3wCTTgJZDN+u7Flu+IrHZpJ
+1DQhQnpJPhFsEGa0kdu9OsKN9OQDsbRuXJi/fQIDAQABAoIBAA14DjPAFEriyiAK
+EC4jxkrIox3GoLXuhS2ahnSn5fRI00Z9cKWNcz3RCcS+LmuX7fTMqhyIUYeQZqHY
+MDP5k4o/vOmmWS7I3THn1zMitXt7FoW+G+ACI6hdfXb6K2GluGxUhVbcLUNoqpLy
+lwARxQpm2wnl/l49IA63i1S9zq3vy5HSvxBv3jq8xp2PX3Sho33LhsvW+miCJe+R
+etKSV4mAjvR62XVgUGJ40FciVMK3pzwwIKPLI7k9sa7WHZr6fNHeDxIWA6AsVBTH
+O+2l8Ufd79KesOD6VqdHlxg2a79s3NoCOflQQAbOSSR9ioCE7Ykgvw0wVl57xNoB
+WZVAY0ECgYEA2I6+a56NMzkEMxr0w9ZRqgocXCgbqxZx1v7newDyO5J3X4jYhmMJ
+abNZtnVs1Pz0IAgCMCf+N4D+RAi9/ahYxq7jmhIkT/IUHseh93XLgd/x9Q9ZJOGm
+9+NI3aIBgWOsy4orKxfwzRAVEakOCChYUCy/GUDDD1MPcjxC8ma5abUCgYEAzAua
+15Nayr9Sg0QsHqNvgTVkVlA6u+TwN+vfI8cH5nmXIMm5ShwCc9+Pm8mpcFwUo4uE
+vOzQ+NwG9CBbVu6/i1/aR+ZlbctdhpsW51v18B9eFVXSZUvEv1ONGdoJZhq0tW7W
+V4Zjf+UwdTcrSZKVpd5woUbRkByROPdir/3Ie6kCgYBhf5LX3SBxSWBMqfw9F4bY
+6YhvLVeXpZlHVKhfRsPIcl7wUio6Bui8ABWKAkAnfGNk8HYbvEXGM3tGojD3vQ2L
+Fj4+paBXpgPM/9A6G3yuUmcbD/fwlO+Zd2jc8A2BdaDcWq6ozjSJ/o2dz+ETZyar
+ohm/gtrPUXQI2HzDqeAcaQKBgEBMd+LvAHFbkPjkhrKw9fZViOTaK2gCYOB+Z7ay
+hX7PWhxu9QCxiuRQ0sRY7BgILEjNMmsGhWOmklpjx+TBH4MgFX0K0XOj3jkIrlMB
+26JrgA5hGQfqtHlGLvSyjLusNr3ly42RP9GRu490byOkGZxHWF66Hle3aNv2uRaU
+dpThAoGBAMIPpf4E6xGzurhgYdXMit3jGAYD85BbNUIm9jOym2lxS63ipoF08QhH
+NoMVRf/AUoS4VDGXsuABjMffTZbE8L+DaL1cWSuPJAoF9AXUXtz6A8LRc9Mn2WjS
+L8BIs9xZCzrJ5XzY4PSnjjyAU81z6E80azWglkmh8rRDzi/o9O79
+-----END RSA PRIVATE KEY-----

+ 18 - 0
test/test_ssl/test_nohttp.py

@@ -0,0 +1,18 @@
+import pytest
+
+
+def test_web2_http_is_not_forwarded(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.tld/", allow_redirects=False)
+    assert r.status_code == 503
+
+
+def test_web2_https_is_forwarded(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web2.nginx-proxy.tld/port", allow_redirects=False)
+    assert r.status_code == 200
+    assert "answer from port 82\n" in r.text
+
+
+def test_web2_HSTS_policy_is_active(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web2.nginx-proxy.tld/port", allow_redirects=False)
+    assert "answer from port 82\n" in r.text
+    assert "Strict-Transport-Security" in r.headers

+ 15 - 0
test/test_ssl/test_nohttp.yml

@@ -0,0 +1,15 @@
+web2:
+  image: web
+  expose:
+    - "82"
+  environment:
+    WEB_PORTS: "82"
+    VIRTUAL_HOST: "web2.nginx-proxy.tld"
+    HTTPS_METHOD: nohttp
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ./certs:/etc/nginx/certs:ro

+ 12 - 0
test/test_ssl/test_nohttps.py

@@ -0,0 +1,12 @@
+import pytest
+from requests import ConnectionError
+
+def test_http_is_forwarded(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/port", allow_redirects=False)
+    assert r.status_code == 200
+    assert "answer from port 83\n" in r.text
+
+
+def test_https_is_disabled(docker_compose, nginxproxy):
+    with pytest.raises(ConnectionError):
+        nginxproxy.get("https://web.nginx-proxy.tld/", allow_redirects=False)

+ 14 - 0
test/test_ssl/test_nohttps.yml

@@ -0,0 +1,14 @@
+web:
+  image: web
+  expose:
+    - "83"
+  environment:
+    WEB_PORTS: "83"
+    VIRTUAL_HOST: "web.nginx-proxy.tld"
+    HTTPS_METHOD: nohttps
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro

+ 19 - 0
test/test_ssl/test_noredirect.py

@@ -0,0 +1,19 @@
+import pytest
+
+
+def test_web3_http_is_forwarded(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web3.nginx-proxy.tld/port", allow_redirects=False)
+    assert r.status_code == 200
+    assert "answer from port 83\n" in r.text
+
+
+def test_web3_https_is_forwarded(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web3.nginx-proxy.tld/port", allow_redirects=False)
+    assert r.status_code == 200
+    assert "answer from port 83\n" in r.text
+
+
+def test_web2_HSTS_policy_is_inactive(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web3.nginx-proxy.tld/port", allow_redirects=False)
+    assert "answer from port 83\n" in r.text
+    assert "Strict-Transport-Security" not in r.headers

+ 15 - 0
test/test_ssl/test_noredirect.yml

@@ -0,0 +1,15 @@
+web3:
+  image: web
+  expose:
+    - "83"
+  environment:
+    WEB_PORTS: "83"
+    VIRTUAL_HOST: "web3.nginx-proxy.tld"
+    HTTPS_METHOD: noredirect
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ./certs:/etc/nginx/certs:ro

+ 23 - 0
test/test_ssl/test_wildcard.py

@@ -0,0 +1,23 @@
+import pytest
+
+
+@pytest.mark.parametrize("subdomain", ["foo", "bar"])
+def test_web1_http_redirects_to_https(docker_compose, nginxproxy, subdomain):
+    r = nginxproxy.get("http://%s.nginx-proxy.tld/" % subdomain, allow_redirects=False)
+    assert r.status_code == 301
+    assert "Location" in r.headers
+    assert "https://%s.nginx-proxy.tld/" % subdomain == r.headers['Location']
+
+
+@pytest.mark.parametrize("subdomain", ["foo", "bar"])
+def test_web1_https_is_forwarded(docker_compose, nginxproxy, subdomain):
+    r = nginxproxy.get("https://%s.nginx-proxy.tld/port" % subdomain, allow_redirects=False)
+    assert r.status_code == 200
+    assert "answer from port 81\n" in r.text
+
+
+@pytest.mark.parametrize("subdomain", ["foo", "bar"])
+def test_web1_HSTS_policy_is_active(docker_compose, nginxproxy, subdomain):
+    r = nginxproxy.get("https://%s.nginx-proxy.tld/port" % subdomain, allow_redirects=False)
+    assert "answer from port 81\n" in r.text
+    assert "Strict-Transport-Security" in r.headers

+ 13 - 0
test/test_ssl/test_wildcard.yml

@@ -0,0 +1,13 @@
+web1:
+  image: web
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: "81"
+    VIRTUAL_HOST: "*.nginx-proxy.tld"
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ./certs:/etc/nginx/certs:ro

+ 32 - 0
test/test_wildcard_host.py

@@ -0,0 +1,32 @@
+import pytest
+
+
+@pytest.mark.parametrize("host,expected_port", [
+    ("f00.nginx-proxy.test", 81),
+    ("bar.nginx-proxy.test", 81),
+    ("test.nginx-proxy.f00", 82),
+    ("test.nginx-proxy.bar", 82),
+    ("web3.123.nginx-proxy.regexp", 83),
+    ("web3.ABC.nginx-proxy.regexp", 83),
+    ("web3.123.ABC.nginx-proxy.regexp", 83),
+    ("web3.123-ABC.nginx-proxy.regexp", 83),
+    ("web3.whatever.nginx-proxy.regexp-to-infinity-and-beyond", 83),
+    ("web4.123.nginx-proxy.regexp", 84),
+    ("web4.ABC.nginx-proxy.regexp", 84),
+    ("web4.123.ABC.nginx-proxy.regexp", 84),
+    ("web4.123-ABC.nginx-proxy.regexp", 84),
+    ("web4.whatever.nginx-proxy.regexp", 84),
+])
+def test_wildcard_prefix(docker_compose, nginxproxy, host, expected_port):
+    r = nginxproxy.get("http://%s/port" % host)
+    assert r.status_code == 200
+    assert r.text == "answer from port %s\n" % expected_port
+
+
+@pytest.mark.parametrize("host", [
+    "unexpected.nginx-proxy.tld",
+    "web4.whatever.nginx-proxy.regexp-to-infinity-and-beyond"
+])
+def test_non_matching_host_is_503(docker_compose, nginxproxy, host):
+    r = nginxproxy.get("http://%s/port" % host)
+    assert r.status_code == 503, r.text

+ 37 - 0
test/test_wildcard_host.yml

@@ -0,0 +1,37 @@
+web1:
+  image: web
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: "81"
+    VIRTUAL_HOST: "*.nginx-proxy.test"
+
+web2:
+  image: web
+  expose:
+    - "82"
+  environment:
+    WEB_PORTS: "82"
+    VIRTUAL_HOST: "test.nginx-proxy.*"
+
+web3:
+  image: web
+  expose:
+    - "83"
+  environment:
+    WEB_PORTS: "83"
+    VIRTUAL_HOST: ~^web3\..*\.nginx-proxy\.regexp
+
+web4:
+  image: web
+  expose:
+    - "84"
+  environment:
+    WEB_PORTS: "84"
+    VIRTUAL_HOST: ~^web4\..*\.nginx-proxy\.regexp$$ # we need to double the `$` because of docker-compose variable interpolation
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro

+ 0 - 93
test/wildcard-hosts.bats

@@ -1,93 +0,0 @@
-#!/usr/bin/env bats
-load test_helpers
-SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}
-
-function setup {
-	# make sure to stop any web container before each test so we don't
-	# have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set
-	stop_bats_containers web
-}
-
-
-@test "[$TEST_FILE] start a nginx-proxy container" {
-	# GIVEN
-	run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro
-	assert_success
-	docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events"
-}
-
-
-@test "[$TEST_FILE] VIRTUAL_HOST=*.wildcard.bats" {
-	# WHEN
-	prepare_web_container bats-wildcard-hosts-1 80 -e VIRTUAL_HOST=*.wildcard.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-wildcard-hosts-1
-	sleep 1
-
-	# THEN
-	assert_200 f00.wildcard.bats
-	assert_200 bar.wildcard.bats
-	assert_503 unexpected.host.bats
-}
-
-@test "[$TEST_FILE] VIRTUAL_HOST=wildcard.bats.*" {
-	# WHEN
-	prepare_web_container bats-wildcard-hosts-2 80 -e VIRTUAL_HOST=wildcard.bats.*
-	dockergen_wait_for_event $SUT_CONTAINER start bats-wildcard-hosts-2
-	sleep 1
-
-	# THEN
-	assert_200 wildcard.bats.f00
-	assert_200 wildcard.bats.bar
-	assert_503 unexpected.host.bats
-}
-
-@test "[$TEST_FILE] VIRTUAL_HOST=~^foo\.bar\..*\.bats" {
-	# WHEN
-	prepare_web_container bats-wildcard-hosts-3 80 -e VIRTUAL_HOST=~^foo\.bar\..*\.bats
-	dockergen_wait_for_event $SUT_CONTAINER start bats-wildcard-hosts-3
-	sleep 1
-
-	# THEN
-	assert_200 foo.bar.whatever.bats
-	assert_200 foo.bar.why.not.bats
-	assert_200 foo.bar.why.not.bats-to-infinity-and-beyond
-	assert_503 unexpected.host.bats
-
-}
-
-@test "[$TEST_FILE] VIRTUAL_HOST=~^foo\.bar\..*\.bats$" {
-	# WHEN
-	prepare_web_container bats-wildcard-hosts-4 80 -e VIRTUAL_HOST=~^foo\.bar\..*\.bats$
-	dockergen_wait_for_event $SUT_CONTAINER start bats-wildcard-hosts-4
-	sleep 1
-
-	# THEN
-	assert_200 foo.bar.whatever.bats
-	assert_200 foo.bar.why.not.bats
-	assert_503 foo.bar.why.not.bats-to-infinity-and-beyond
-	assert_503 unexpected.host.bats
-
-}
-
-@test "[$TEST_FILE] stop all bats containers" {
-	stop_bats_containers
-}
-
-
-# assert that querying nginx-proxy with the given Host header produces a `HTTP 200` response
-# $1 Host HTTP header to use when querying nginx-proxy
-function assert_200 {
-	local -r host=$1
-
-	run curl_container $SUT_CONTAINER / --head --header "Host: $host"
-	assert_output -l 0 $'HTTP/1.1 200 OK\r'
-}
-
-# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response
-# $1 Host HTTP header to use when querying nginx-proxy
-function assert_503 {
-	local -r host=$1
-
-	run curl_container $SUT_CONTAINER / --head --header "Host: $host"
-	assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r'
-}