1
0
mirror of https://github.com/thib8956/nginx-proxy synced 2026-04-21 03:19:39 +00:00

tests: cleanup test code

- remove unused imports in test cases
- fix code smells and code style in conftest.py
This commit is contained in:
Nicolas Duchon
2024-12-24 13:53:09 +01:00
parent c60eff5d16
commit b5dea1cf50
67 changed files with 83 additions and 156 deletions
+48 -30
View File
@@ -1,4 +1,5 @@
import contextlib
import errno
import logging
import os
import re
@@ -9,13 +10,12 @@ import time
from typing import List
import backoff
import docker
import docker.errors
import pytest
import requests
from _pytest._code.code import ReprExceptionInfo
from packaging.version import Version
from docker.models.containers import Container
from requests.packages.urllib3.util.connection import HAS_IPV6
logging.basicConfig(level=logging.INFO)
logging.getLogger('backoff').setLevel(logging.INFO)
@@ -40,6 +40,24 @@ test_container = 'nginx-proxy-pytest'
#
###############################################################################
def system_has_ipv6() -> bool:
# See https://stackoverflow.com/a/66249915
_ADDR_NOT_AVAIL = {errno.EADDRNOTAVAIL, errno.EAFNOSUPPORT}
_ADDR_IN_USE = {errno.EADDRINUSE}
if not socket.has_ipv6:
return False
try:
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock:
sock.bind(("::1", 0))
return True
except OSError as e:
if e.errno in _ADDR_NOT_AVAIL:
return False
if e.errno in _ADDR_IN_USE:
return True
raise
@contextlib.contextmanager
def ipv6(force_ipv6=True):
"""
@@ -59,10 +77,10 @@ def ipv6(force_ipv6=True):
FORCE_CONTAINER_IPV6 = False
class requests_for_docker(object):
class RequestsForDocker(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.
When an 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):
@@ -99,43 +117,43 @@ class requests_for_docker(object):
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)
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)
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)
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)
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)
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)
def _options(*_args, **_kwargs):
return self.session.options(*_args, **_kwargs)
return _options(*args, **kwargs)
def __getattr__(self, name):
@@ -150,7 +168,7 @@ def container_ip(container: Container):
"""
global FORCE_CONTAINER_IPV6
if FORCE_CONTAINER_IPV6:
if not HAS_IPV6:
if not system_has_ipv6():
pytest.skip("This system does not support IPv6")
ip = container_ipv6(container)
if ip == '':
@@ -200,11 +218,11 @@ def nginx_proxy_dns_resolver(domain_name):
if 'nginx-proxy' in domain_name:
nginxproxy_containers = docker_client.containers.list(filters={"status": "running", "ancestor": "nginxproxy/nginx-proxy:test"})
if len(nginxproxy_containers) == 0:
log.warn(f"no container found from image nginxproxy/nginx-proxy:test while resolving {domain_name!r}")
log.warning(f"no container found from image nginxproxy/nginx-proxy:test while resolving {domain_name!r}")
exited_nginxproxy_containers = docker_client.containers.list(filters={"status": "exited", "ancestor": "nginxproxy/nginx-proxy:test"})
if len(exited_nginxproxy_containers) > 0:
exited_nginxproxy_container_logs = exited_nginxproxy_containers[0].logs()
log.warn(f"nginxproxy/nginx-proxy:test container might have exited unexpectedly. Container logs: " + "\n" + exited_nginxproxy_container_logs.decode())
log.warning(f"nginxproxy/nginx-proxy:test container might have exited unexpectedly. Container logs: " + "\n" + exited_nginxproxy_container_logs.decode())
return
nginxproxy_container = nginxproxy_containers[0]
ip = container_ip(nginxproxy_container)
@@ -231,7 +249,7 @@ def docker_container_dns_resolver(domain_name):
try:
container = docker_client.containers.get(container_name)
except docker.errors.NotFound:
log.warn(f"container named {container_name!r} not found while resolving {domain_name!r}")
log.warning(f"container named {container_name!r} not found while resolving {domain_name!r}")
return
log.debug(f"container {container.name!r} found ({container.short_id})")
@@ -252,9 +270,9 @@ def monkey_patch_urllib_dns_resolver():
logging.getLogger('DNS').debug(f"resolving domain name {repr(args)}")
_args = list(args)
# Fail early when querying IP directly and it is forced ipv6 when not supported,
# Fail early when querying IP directly, and it is forced ipv6 when not supported,
# Otherwise a pytest container not using the host network fails to pass `test_raw-ip-vhost`.
if FORCE_CONTAINER_IPV6 and not HAS_IPV6:
if FORCE_CONTAINER_IPV6 and not system_has_ipv6():
pytest.skip("This system does not support IPv6")
# custom DNS resolvers
@@ -371,7 +389,7 @@ def connect_to_network(network):
try:
my_container = docker_client.containers.get(test_container)
except docker.errors.NotFound:
logging.warn(f"container {test_container} not found")
logging.warning(f"container {test_container} not found")
return
# figure out our container networks
@@ -399,7 +417,7 @@ def disconnect_from_network(network=None):
try:
my_container = docker_client.containers.get(test_container)
except docker.errors.NotFound:
logging.warn(f"container {test_container} not found")
logging.warning(f"container {test_container} not found")
return
# figure out our container networks
@@ -427,6 +445,7 @@ def connect_to_all_networks():
class DockerComposer(contextlib.AbstractContextManager):
def __init__(self):
self._networks = None
self._docker_compose_file = None
def __exit__(self, *exc_info):
@@ -498,7 +517,7 @@ def nginxproxy():
"""
Provides the `nginxproxy` object that can be used in the same way the requests module is:
r = nginxproxy.get("http://foo.com")
r = nginxproxy.get("https://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.
@@ -507,7 +526,7 @@ def nginxproxy():
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()
yield RequestsForDocker()
@pytest.fixture()
@@ -526,11 +545,10 @@ def acme_challenge_path():
# 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": "nginxproxy/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))
report.longrepr.addsection('nginx-proxy logs', container.logs().decode())
report.longrepr.addsection('nginx-proxy conf', get_nginx_conf_from_container(container).decode())
# Py.test `incremental` marker, see http://stackoverflow.com/a/12579625/107049
@@ -1,4 +1,5 @@
backoff==2.2.1
docker==7.1.0
packaging==24.2
pytest==8.3.4
requests==2.32.3
-2
View File
@@ -1,5 +1,3 @@
import pytest
def test_unknown_virtual_host(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/port")
assert r.status_code == 503
@@ -1,6 +1,3 @@
import pytest
def test_redirect_acme_challenge_location_disabled(docker_compose, nginxproxy, acme_challenge_path):
r = nginxproxy.get(
f"http://web1.nginx-proxy.tld/{acme_challenge_path}",
@@ -1,6 +1,3 @@
import pytest
def test_redirect_acme_challenge_location_enabled(docker_compose, nginxproxy, acme_challenge_path):
r = nginxproxy.get(
f"http://web1.nginx-proxy.tld/{acme_challenge_path}",
@@ -1,6 +1,3 @@
import pytest
def test_redirect_acme_challenge_location_legacy(docker_compose, nginxproxy, acme_challenge_path):
r = nginxproxy.get(
f"http://web1.nginx-proxy.tld/{acme_challenge_path}",
+4 -2
View File
@@ -1,11 +1,13 @@
"""
Test that nginx-proxy-tester can build successfully
"""
import pytest
import docker
import re
import os
import docker
import pytest
client = docker.from_env()
@pytest.fixture(scope = "session")
@@ -1,4 +1,3 @@
import pytest
import re
@@ -1,5 +1,3 @@
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
-2
View File
@@ -1,5 +1,3 @@
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
@@ -1,5 +1,3 @@
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
-2
View File
@@ -1,5 +1,3 @@
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
-2
View File
@@ -1,5 +1,3 @@
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
+2
View File
@@ -1,6 +1,8 @@
import json
import pytest
def test_debug_endpoint_is_enabled_globally(docker_compose, nginxproxy):
r = nginxproxy.get("http://enabled.debug.nginx-proxy.example/nginx-proxy-debug")
assert r.status_code == 200
@@ -1,6 +1,8 @@
import json
import pytest
def test_debug_endpoint_is_disabled_globally(docker_compose, nginxproxy):
r = nginxproxy.get("http://disabled1.debug.nginx-proxy.example/nginx-proxy-debug")
assert r.status_code == 404
-3
View File
@@ -1,6 +1,3 @@
import pytest
def test_fallback_on_default(docker_compose, nginxproxy):
r = nginxproxy.get("http://unknown.nginx-proxy.tld/port")
assert r.status_code == 200
-3
View File
@@ -1,6 +1,3 @@
import pytest
def test_nohttp_missing_cert_disabled(docker_compose, nginxproxy):
r = nginxproxy.get("http://nohttp-missing-cert-disabled.nginx-proxy.tld/", allow_redirects=False)
assert r.status_code == 503
@@ -1,6 +1,3 @@
import pytest
def test_forwards_to_bridge_network_container(docker_compose, nginxproxy):
r = nginxproxy.get("http://bridge-network.nginx-proxy.tld/port")
assert r.status_code == 200
@@ -1,6 +1,3 @@
import pytest
def test_forwards_to_host_network_container_1(docker_compose, nginxproxy):
r = nginxproxy.get("http://host-network-1.nginx-proxy.tld:8888/port")
assert r.status_code == 200
@@ -1,5 +1,3 @@
import pytest
def test_htpasswd_regex_virtual_host_is_restricted(docker_compose, nginxproxy):
r = nginxproxy.get("http://regex.htpasswd.nginx-proxy.example/port")
assert r.status_code == 401
@@ -1,5 +1,3 @@
import pytest
def test_htpasswd_virtual_host_is_restricted(docker_compose, nginxproxy):
r = nginxproxy.get("http://htpasswd.nginx-proxy.tld/port")
assert r.status_code == 401
@@ -1,5 +1,3 @@
import pytest
def test_htpasswd_virtual_path_is_restricted(docker_compose, nginxproxy):
r = nginxproxy.get("http://htpasswd.nginx-proxy.tld/foo/port")
assert r.status_code == 401
@@ -1,6 +1,6 @@
import pytest
import re
def test_http2_global_disabled_config(docker_compose, nginxproxy):
conf = nginxproxy.get_conf().decode('ASCII')
r = nginxproxy.get("http://http2-global-disabled.nginx-proxy.tld")
@@ -1,8 +1,8 @@
import pytest
import re
#Python Requests is not able to do native http3 requests.
#We only check for directives which should enable http3.
# Python Requests is not able to do native http3 requests.
# We only check for directives which should enable http3.
def test_http3_global_disabled_ALTSVC_header(docker_compose, nginxproxy):
r = nginxproxy.get("http://http3-global-disabled.nginx-proxy.tld/headers")
+3 -3
View File
@@ -1,8 +1,8 @@
import pytest
import re
#Python Requests is not able to do native http3 requests.
#We only check for directives which should enable http3.
# Python Requests is not able to do native http3 requests.
# We only check for directives which should enable http3.
def test_http3_global_enabled_ALTSVC_header(docker_compose, nginxproxy):
r = nginxproxy.get("http://http3-global-enabled.nginx-proxy.tld/headers")
+3 -3
View File
@@ -1,8 +1,8 @@
import pytest
import re
#Python Requests is not able to do native http3 requests.
#We only check for directives which should enable http3.
# Python Requests is not able to do native http3 requests.
# We only check for directives which should enable http3.
def test_http3_vhost_enabled_ALTSVC_header(docker_compose, nginxproxy):
r = nginxproxy.get("http://http3-vhost-enabled.nginx-proxy.tld/headers")
@@ -1,5 +1,3 @@
import pytest
def test_network_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.example/port")
assert r.status_code == 200
@@ -1,5 +1,3 @@
import pytest
def test_network_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy.example/web1/port")
assert r.status_code == 200
-3
View File
@@ -1,6 +1,3 @@
import pytest
def test_unknown_virtual_host_ipv4(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/port")
assert r.status_code == 503
@@ -1,6 +1,3 @@
import pytest
def test_forwards_to_ipv4_only_network(docker_compose, nginxproxy):
r = nginxproxy.get("http://ipv4only.nginx-proxy.tld/port")
assert r.status_code == 200
@@ -1,6 +1,3 @@
import pytest
def test_forwards_to_ipv4_only_network(docker_compose, nginxproxy):
r = nginxproxy.get("http://ipv4only.nginx-proxy.tld/port")
assert r.status_code == 200
+1 -1
View File
@@ -1,6 +1,6 @@
import pytest
import re
def test_loadbalance_hash(docker_compose, nginxproxy):
conf = nginxproxy.get_conf().decode('ASCII')
r1 = nginxproxy.get("http://loadbalance-enabled.nginx-proxy.tld")
-2
View File
@@ -1,5 +1,3 @@
import pytest
def test_log_disabled(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy.test/port")
assert r.status_code == 200
-2
View File
@@ -1,5 +1,3 @@
import pytest
def test_log_format(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy.test/port")
assert r.status_code == 200
-2
View File
@@ -1,5 +1,3 @@
import pytest
def test_log_json(docker_compose, nginxproxy):
log_conf = [line for line in nginxproxy.get_conf().decode('ASCII').splitlines() if "log_format vhost escape=" in line]
assert "{\"time_local\":\"$time_iso8601\"," in log_conf[0]
-2
View File
@@ -1,5 +1,3 @@
import pytest
def test_log_json_format(docker_compose, nginxproxy):
log_conf = [line for line in nginxproxy.get_conf().decode('ASCII').splitlines() if "log_format vhost escape=" in line]
assert "{\"time_local\":\"$time_iso8601\"," in log_conf[0]
-3
View File
@@ -1,6 +1,3 @@
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
-2
View File
@@ -1,7 +1,5 @@
import re
import pytest
def test_unknown_virtual_host(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy/")
@@ -1,6 +1,3 @@
import pytest
def test_virtual_host_is_dropped_when_using_multiports(docker_compose, nginxproxy):
r = nginxproxy.get("http://notskipped.nginx-proxy.tld/port")
assert r.status_code == 200
@@ -1,6 +1,3 @@
import pytest
def test_virtual_host_is_dropped_when_using_multiports(docker_compose, nginxproxy):
r = nginxproxy.get("http://notskipped.nginx-proxy.tld/port")
assert r.status_code == 200
@@ -1,4 +1,3 @@
import pytest
import re
@@ -1,5 +1,4 @@
import backoff
import pytest
def test_multiports_and_legacy_configs_should_be_merged(docker_compose, nginxproxy):
@@ -1,4 +1,3 @@
import pytest
import re
-3
View File
@@ -1,6 +1,3 @@
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
-3
View File
@@ -1,6 +1,3 @@
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
@@ -1,6 +1,3 @@
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
-3
View File
@@ -1,6 +1,3 @@
import pytest
def test_raw_ipv4_vhost_forwards_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://172.20.0.4")
assert r.status_code == 200
@@ -1,5 +1,3 @@
import pytest
def test_web_has_no_server_down(docker_compose, nginxproxy):
conf = nginxproxy.get_conf().decode('ASCII')
r = nginxproxy.get("http://web.nginx-proxy.tld/port")
@@ -1,5 +1,3 @@
import pytest
def test_web_has_no_server_down(docker_compose, nginxproxy):
conf = nginxproxy.get_conf().decode('ASCII')
r = nginxproxy.get("http://web.nginx-proxy.tld/port")
@@ -1,5 +1,3 @@
import pytest
def test_web_has_server_down(docker_compose, nginxproxy):
conf = nginxproxy.get_conf().decode('ASCII')
r = nginxproxy.get("http://web.nginx-proxy.tld/port")
+1
View File
@@ -1,4 +1,5 @@
import json
import pytest
-3
View File
@@ -1,6 +1,3 @@
import pytest
def test_web1_HSTS_default(docker_compose, nginxproxy):
r = nginxproxy.get("https://web1.nginx-proxy.tld/port", allow_redirects=False)
assert "answer from port 81\n" in r.text
+1
View File
@@ -1,5 +1,6 @@
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:8080/" % subdomain, allow_redirects=False)
-4
View File
@@ -1,7 +1,3 @@
import pytest
import requests
def test_web2_http_is_connection_refused(docker_compose, nginxproxy):
r = nginxproxy.get("http://web2.nginx-proxy.tld/", allow_redirects=False)
assert r.status_code == 503
+1
View File
@@ -1,6 +1,7 @@
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
-3
View File
@@ -1,6 +1,3 @@
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
+1
View File
@@ -1,6 +1,7 @@
import pytest
from requests import ConnectionError
@pytest.mark.parametrize("path", ["web1", "web2"])
def test_web1_http_redirects_to_https(docker_compose, nginxproxy, path):
r = nginxproxy.get("http://www.nginx-proxy.tld/%s/port" % path, allow_redirects=False)
@@ -1,6 +1,7 @@
import pytest
import re
import pytest
@pytest.mark.parametrize('url,header,input,want', [
('http://web.nginx-proxy.tld/headers', 'X-Forwarded-Proto', None, 'http'),
@@ -1,6 +1,7 @@
import pytest
import re
import pytest
@pytest.mark.parametrize('url,header,input,want', [
('http://web.nginx-proxy.tld/headers', 'X-Forwarded-Proto', None, 'http'),
@@ -1,6 +1,7 @@
import pytest
import re
import pytest
@pytest.mark.parametrize('url,header,input,want', [
('http://web.nginx-proxy.tld/headers', 'X-Forwarded-Proto', None, 'http'),
@@ -1,4 +1,3 @@
import pytest
import re
@@ -1,4 +1,3 @@
import pytest
import re
+3 -1
View File
@@ -1,7 +1,9 @@
import pytest
import logging
import time
import pytest
def test_forwards_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://web1.nginx-proxy.example/port")
assert r.status_code == 200
@@ -1,5 +1,6 @@
import pytest
def test_default_root_response(docker_compose, nginxproxy):
r = nginxproxy.get("http://nginx-proxy.test/")
assert r.status_code == 418
@@ -1,5 +1,3 @@
import pytest
def test_root_redirects_to_web1(docker_compose, nginxproxy):
r = nginxproxy.get("http://www.nginx-proxy.tld/port", allow_redirects=False)
assert r.status_code == 301
@@ -1,5 +1,3 @@
import pytest
def test_location_precedence_case1(docker_compose, nginxproxy):
r = nginxproxy.get(f"http://foo.nginx-proxy.test/web1/port")
assert r.status_code == 200
@@ -3,6 +3,7 @@ from time import sleep
import pytest
from docker.errors import NotFound
@pytest.mark.parametrize("stub,expected_port", [
("nginx-proxy.test/web1", 81),
("nginx-proxy.test/web2", 82),