Skip to content

Commit 6198719

Browse files
committed
Fix failing tests and workflow warnings
1 parent ce2e1d8 commit 6198719

9 files changed

Lines changed: 172 additions & 37 deletions

File tree

.github/workflows/docker-image.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@ jobs:
1515
contents: read
1616
steps:
1717
- name: Checkout
18-
uses: actions/checkout@v3
18+
uses: actions/checkout@v6
1919

2020
- name: Set up QEMU
21-
uses: docker/setup-qemu-action@v2
21+
uses: docker/setup-qemu-action@v4
2222

2323
- name: Set up Docker Buildx
24-
uses: docker/setup-buildx-action@v2
24+
uses: docker/setup-buildx-action@v4
2525

2626
- name: Login to DockerHub
27-
uses: docker/login-action@v2
27+
uses: docker/login-action@v4
2828
with:
2929
username: mesudip
3030
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -51,7 +51,7 @@ jobs:
5151
5252
- name: Build and push for tags
5353
if: startsWith(github.ref, 'refs/tags/')
54-
uses: docker/build-push-action@v5
54+
uses: docker/build-push-action@v7
5555
with:
5656
file: Dockerfile
5757
context: .
@@ -63,7 +63,7 @@ jobs:
6363

6464
- name: Build and push for main branch
6565
if: github.ref == 'refs/heads/master'
66-
uses: docker/build-push-action@v5
66+
uses: docker/build-push-action@v7
6767
with:
6868
file: Dockerfile
6969
context: .

.github/workflows/publish-python-package.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ jobs:
1515
runs-on: ubuntu-latest
1616
steps:
1717
- name: Checkout code
18-
uses: actions/checkout@v4
18+
uses: actions/checkout@v6
1919

2020
- name: Set up Python
21-
uses: actions/setup-python@v5
21+
uses: actions/setup-python@v6
2222
with:
2323
python-version: "3.12"
2424

.github/workflows/run-tests.yml

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,18 @@ jobs:
2222
contents: read
2323
steps:
2424
- name: Checkout code
25-
uses: actions/checkout@v3
25+
uses: actions/checkout@v6
2626

2727
- name: Set up Python
28-
uses: actions/setup-python@v4
28+
uses: actions/setup-python@v6
2929
with:
3030
python-version: '3.12'
3131

3232
- name: Set up QEMU
33-
uses: docker/setup-qemu-action@v2
33+
uses: docker/setup-qemu-action@v4
3434

3535
- name: Set up Docker Buildx
36-
uses: docker/setup-buildx-action@v2
36+
uses: docker/setup-buildx-action@v4
3737

3838
- name: Install dependencies
3939
run: |
@@ -44,16 +44,113 @@ jobs:
4444
4545
- name: Run tests
4646
run: |
47-
pytest --cov --cov-branch --junitxml=junit.xml -o junit_family=legacy
47+
pytest --cov --cov-branch --cov-report=xml:coverage.xml --junitxml=junit.xml -o junit_family=legacy
48+
49+
- name: Write test summary
50+
if: ${{ always() }}
51+
run: |
52+
python - <<'PY'
53+
import os
54+
import sys
55+
import xml.etree.ElementTree as ET
56+
from pathlib import Path
57+
58+
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
59+
if not summary_path:
60+
sys.exit(0)
61+
62+
junit_path = Path("junit.xml")
63+
64+
def md_escape(value):
65+
return str(value).replace("|", "\\|").replace("\n", "<br>")
66+
67+
lines = ["## Test summary", ""]
68+
69+
if not junit_path.exists():
70+
lines.extend([
71+
"No `junit.xml` test report was generated.",
72+
"",
73+
])
74+
else:
75+
root = ET.parse(junit_path).getroot()
76+
suites = [root] if root.tag == "testsuite" else list(root.findall("testsuite"))
77+
if not suites:
78+
suites = [root]
79+
80+
totals = {"tests": 0, "failures": 0, "errors": 0, "skipped": 0, "time": 0.0}
81+
for suite in suites:
82+
totals["tests"] += int(suite.attrib.get("tests", 0))
83+
totals["failures"] += int(suite.attrib.get("failures", 0))
84+
totals["errors"] += int(suite.attrib.get("errors", 0))
85+
totals["skipped"] += int(suite.attrib.get("skipped", 0))
86+
totals["time"] += float(suite.attrib.get("time", 0.0))
87+
88+
passed = totals["tests"] - totals["failures"] - totals["errors"] - totals["skipped"]
89+
lines.extend([
90+
"| Total | Passed | Failed | Errors | Skipped | Duration |",
91+
"| ---: | ---: | ---: | ---: | ---: | ---: |",
92+
f"| {totals['tests']} | {passed} | {totals['failures']} | {totals['errors']} | {totals['skipped']} | {totals['time']:.2f}s |",
93+
"",
94+
"### Metadata",
95+
"",
96+
"| Key | Value |",
97+
"| --- | --- |",
98+
f"| Python | {md_escape(sys.version.split()[0])} |",
99+
f"| Runner OS | {md_escape(os.environ.get('RUNNER_OS', 'unknown'))} |",
100+
f"| Event | {md_escape(os.environ.get('GITHUB_EVENT_NAME', 'unknown'))} |",
101+
f"| Ref | {md_escape(os.environ.get('GITHUB_REF_NAME', 'unknown'))} |",
102+
f"| SHA | `{md_escape(os.environ.get('GITHUB_SHA', 'unknown'))}` |",
103+
f"| Actor | {md_escape(os.environ.get('GITHUB_ACTOR', 'unknown'))} |",
104+
"",
105+
])
106+
107+
failed_cases = []
108+
for case in root.iter("testcase"):
109+
issue = case.find("failure")
110+
if issue is None:
111+
issue = case.find("error")
112+
if issue is not None:
113+
failed_cases.append((case.attrib, issue))
114+
115+
if failed_cases:
116+
lines.extend([
117+
"### Failed tests",
118+
"",
119+
"| Test | Type | Message |",
120+
"| --- | --- | --- |",
121+
])
122+
for attrs, issue in failed_cases[:25]:
123+
classname = attrs.get("classname", "")
124+
name = attrs.get("name", "unknown")
125+
test_name = f"{classname}.{name}" if classname else name
126+
message = issue.attrib.get("message", "").strip() or (issue.text or "").strip().splitlines()[0:1]
127+
if isinstance(message, list):
128+
message = message[0] if message else ""
129+
lines.append(f"| `{md_escape(test_name)}` | {md_escape(issue.tag)} | {md_escape(message[:300])} |")
130+
if len(failed_cases) > 25:
131+
lines.append(f"| ... | ... | {len(failed_cases) - 25} more failing tests omitted from summary |")
132+
lines.append("")
133+
134+
with open(summary_path, "a", encoding="utf-8") as summary:
135+
summary.write("\n".join(lines))
136+
summary.write("\n")
137+
PY
48138
49139
- name: Upload coverage reports to Codecov
140+
if: ${{ !cancelled() }}
50141
uses: codecov/codecov-action@v5
51142
with:
52143
token: ${{ secrets.CODECOV_TOKEN }}
144+
files: coverage.xml
145+
disable_search: true
53146
fail_ci_if_error: false
54147

55148
- name: Upload test results to Codecov
56149
if: ${{ !cancelled() }}
57-
uses: codecov/test-results-action@v1
150+
uses: codecov/codecov-action@v5
58151
with:
59152
token: ${{ secrets.CODECOV_TOKEN }}
153+
files: junit.xml
154+
disable_search: true
155+
report_type: test_results
156+
fail_ci_if_error: false

nginx_proxy/pre_processors/redirect_processor.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ def _is_certificate_redirect_target(target: Url):
1212
return "https" in target.scheme or "wss" in target.scheme or int(target.port or 80) == 443
1313

1414

15+
def _hostname_exceeds_certificate_limit(hostname: str) -> bool:
16+
return bool(hostname) and len(hostname.rstrip(".")) > 64
17+
18+
1519
def process_redirection(backend: BackendTarget, environments: map, vhost_map: Dict[str, Dict[int, Host]]):
1620
redirect_env = [e[1] for e in environments.items() if e[0].startswith("PROXY_FULL_REDIRECT")]
1721
hosts = []
@@ -46,9 +50,7 @@ def process_redirection(backend: BackendTarget, environments: map, vhost_map: Di
4650
if not Url.is_valid_hostname(target.hostname, allow_wildcard=True):
4751
print("Invalid PROXY_FULL_REDIRECT target hostname: " + target.hostname)
4852
continue
49-
if _is_certificate_redirect_target(target) and not Url.is_valid_hostname(
50-
target.hostname, allow_wildcard=True, max_length=64
51-
):
53+
if _is_certificate_redirect_target(target) and _hostname_exceeds_certificate_limit(target.hostname):
5254
print("Invalid PROXY_FULL_REDIRECT target certificate hostname: " + target.hostname)
5355
continue
5456
for source in sources:

nginx_proxy/pre_processors/virtual_host_processor.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@ def _requires_certificate(host: Host) -> bool:
2525
return "https" in host.scheme or "wss" in host.scheme or int(host.port or 80) == 443
2626

2727

28+
def _hostname_exceeds_certificate_limit(hostname: str) -> bool:
29+
return bool(hostname) and len(hostname.rstrip(".")) > 64
30+
31+
2832
def _validate_external_host(host: Host):
2933
if not Url.is_valid_hostname(host.hostname, allow_wildcard=True):
3034
raise InvalidHostConfiguration(host.hostname, "invalid hostname")
31-
if _requires_certificate(host) and not Url.is_valid_hostname(host.hostname, allow_wildcard=True, max_length=64):
35+
if _requires_certificate(host) and _hostname_exceeds_certificate_limit(host.hostname):
3236
raise InvalidHostConfiguration(host.hostname, "certificate hostnames must be 64 characters or fewer")
3337

3438

tests/integration/test_prefer_local_swarm.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
from tests.helpers.integration_helpers import get_nginx_config_from_container
99

1010

11-
@pytest.fixture(scope="session")
12-
def swarm_mode():
13-
return "prefer-local"
11+
@pytest.fixture(scope="session", params=["prefer-local"], ids=["swarm_prefer_local"])
12+
def swarm_mode(request):
13+
return request.param
1414

1515

1616
def test_prefer_local_uses_local_swarm_task_primary_and_service_vip_backup(
@@ -25,7 +25,8 @@ def test_prefer_local_uses_local_swarm_task_primary_and_service_vip_backup(
2525
try:
2626
upstream = None
2727
config_str = ""
28-
for _ in range(40):
28+
deadline = time.monotonic() + 60
29+
while time.monotonic() < deadline:
2930
config_str = get_nginx_config_from_container(nginx_proxy_container[0])
3031
config = HttpBlock.parse(config_str)
3132
upstream = next((u for u in config.upstreams if virtual_host in u.parameters), None)

tests/unit/test_backend_target.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,18 @@ def test_http_virtual_host_allows_dns_valid_hostname_longer_than_64_chars(self):
258258
assert len(hosts) == 1
259259
assert hosts[0].hostname == long_hostname
260260

261+
def test_virtual_host_rejects_invalid_hostname(self):
262+
bt = BackendTarget(
263+
id="invalid-host-id",
264+
name="invalid-host-test",
265+
env={"VIRTUAL_HOST": "bad_host.example.com"},
266+
network_settings={"my-net": {"NetworkID": "my-net-id", "IPAddress": "10.0.0.12"}},
267+
)
268+
269+
config_data = process_virtual_hosts(bt, {"my-net-id"})
270+
271+
assert len(list(config_data.host_list())) == 0
272+
261273
def test_https_virtual_host_allows_certificate_hostname_at_64_chars(self):
262274
hostname = f"{'a' * 52}.example.com"
263275
assert len(hostname) == 64
@@ -274,18 +286,6 @@ def test_https_virtual_host_allows_certificate_hostname_at_64_chars(self):
274286
assert len(hosts) == 1
275287
assert hosts[0].hostname == hostname
276288

277-
def test_virtual_host_rejects_invalid_hostname(self):
278-
bt = BackendTarget(
279-
id="invalid-host-id",
280-
name="invalid-host-test",
281-
env={"VIRTUAL_HOST": "bad_host.example.com"},
282-
network_settings={"my-net": {"NetworkID": "my-net-id", "IPAddress": "10.0.0.12"}},
283-
)
284-
285-
config_data = process_virtual_hosts(bt, {"my-net-id"})
286-
287-
assert len(list(config_data.host_list())) == 0
288-
289289
def test_virtual_host_allows_wildcard_hostname(self):
290290
bt = BackendTarget(
291291
id="wildcard-host-id",

tests/unit/test_nginx_config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
@pytest.fixture
77
def loaded_config():
8-
CONFIG = """
8+
CONFIG = r"""
99
user www www;
1010
1111
worker_processes 2;
@@ -239,7 +239,7 @@ def test_location_blocks(loaded_config):
239239
loc3 = server.locations[3]
240240
assert loc3.path == "/download/"
241241
assert loc3.valid_referers == ["none", "blocked", "server_names", "*.example.com"]
242-
assert loc3.rewrite == "^/(download/.*)/mp3/(.*)\..*$ /$1/mp3/$2.mp3 break"
242+
assert loc3.rewrite == r"^/(download/.*)/mp3/(.*)\..*$ /$1/mp3/$2.mp3 break"
243243
assert loc3.root == "/spool/www"
244244
assert loc3.access_log == "/var/log/nginx-download.access_log download"
245245

@@ -251,7 +251,7 @@ def test_location_blocks(loaded_config):
251251

252252
# Location ~* \.(jpg|jpeg|gif)$
253253
loc4 = server.locations[4]
254-
assert loc4.path == "~* \.(jpg|jpeg|gif)$"
254+
assert loc4.path == r"~* \.(jpg|jpeg|gif)$"
255255
assert loc4.root == "/spool/www"
256256
assert loc4.access_log == "off"
257257
assert loc4.expires == "30d"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from nginx_proxy.BackendTarget import BackendTarget
2+
from nginx_proxy.Host import Host
3+
from nginx_proxy.pre_processors.redirect_processor import process_redirection
4+
5+
6+
def test_proxy_full_redirect_rejects_invalid_target_hostname():
7+
vhost_map = {"target.example.com": {80: Host("target.example.com", 80)}}
8+
backend = BackendTarget(id="redirect-id", name="redirect-test")
9+
10+
process_redirection(
11+
backend,
12+
{"PROXY_FULL_REDIRECT": "source.example.com -> bad_target.example.com"},
13+
vhost_map,
14+
)
15+
16+
assert "source.example.com" not in vhost_map
17+
assert "bad_target.example.com" not in vhost_map
18+
19+
20+
def test_proxy_full_redirect_skips_invalid_source_hostname():
21+
vhost_map = {"target.example.com": {80: Host("target.example.com", 80)}}
22+
backend = BackendTarget(id="redirect-id", name="redirect-test")
23+
24+
process_redirection(
25+
backend,
26+
{"PROXY_FULL_REDIRECT": "bad_source.example.com,valid-source.example.com -> target.example.com"},
27+
vhost_map,
28+
)
29+
30+
assert "bad_source.example.com" not in vhost_map
31+
assert "valid-source.example.com" in vhost_map

0 commit comments

Comments
 (0)