mirror of
https://github.com/taylanbakircioglu/haproxy-openmanager.git
synced 2026-09-21 01:53:24 +00:00
70ebc02e09
Follow-up fixes for the DNS-01 feature reported on #35: - Cloudflare: sanitize the API token (strip surrounding quotes + any non token68 chars) so a pasted token with quotes/spaces no longer fails with "Invalid request headers"; verify-on-save shows a precise hint when it cleaned the input. Covers the automated orchestrator path too. - ZeroSSL/Google EAB: enter the EAB Key ID and HMAC Key per-account in the Register Account dialog (falls back to the global Settings value when blank); base64-validate the HMAC key; humanize the externalAccountRequired failure; and preserve the deliberate 409/422 instead of downgrading them to 400. - Apply Management: cluster ACME enable/disable changes now show under a dedicated "ACME Challenge Routing" section, are counted in the Apply/Reject dialogs, and Apply/Reject All process them (previously "Rejected 0 HA/VIP change(s)") - consistent with every other entity. Reject rolls acme_enabled back to the original via ORDER BY created_at ASC over the snapshot chain. - getErrorMsg surfaces field-level validation messages. Backward compatible (additive / strict superset; HTTP-01 unchanged). Addresses #35.
173 lines
6.9 KiB
Python
173 lines
6.9 KiB
Python
"""
|
|
v1.5.0 Feature A — humanize_error_detail() unit tests.
|
|
|
|
Table-driven coverage of the RFC8555 problem types we humanize, plus
|
|
backwards-compatible behaviour for legacy plain-string error_detail values
|
|
that were written by v1.3.x and earlier.
|
|
"""
|
|
import json
|
|
import pytest
|
|
|
|
from services.acme_diagnostics import humanize_error_detail, _PROBLEM_HUMANIZED
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# 1. Static coverage — we promise >= 11 RFC8555 problem types are humanized.
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
def test_humanizes_at_least_11_rfc8555_problem_types():
|
|
assert len(_PROBLEM_HUMANIZED) >= 11
|
|
|
|
|
|
def test_every_humanized_entry_has_title_and_hint():
|
|
for ptype, body in _PROBLEM_HUMANIZED.items():
|
|
assert "title" in body and body["title"], ptype
|
|
assert "hint" in body, ptype
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# 2. Empty / None handling.
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
def test_none_returns_no_error_marker():
|
|
out = humanize_error_detail(None)
|
|
assert out["title"] == "No error"
|
|
assert out["message"] == ""
|
|
|
|
|
|
def test_empty_string_returns_no_error_marker():
|
|
out = humanize_error_detail("")
|
|
assert out["title"] == "No error"
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# 3. Legacy plain-string fallback (pre-v1.5.0 stored unstructured strings).
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
def test_legacy_plain_string_falls_back_cleanly():
|
|
out = humanize_error_detail("connection refused: agent offline")
|
|
assert out["title"] == "ACME error"
|
|
assert "connection refused" in out["message"]
|
|
# The hint is empty because we don't have a structured problem type.
|
|
assert out["hint"] == ""
|
|
assert out["raw"] == "connection refused: agent offline"
|
|
|
|
|
|
def test_legacy_plain_string_with_brace_but_invalid_json_falls_back():
|
|
# Defensive: '{' prefix is the trigger for JSON parse, but garbled JSON
|
|
# must NOT raise. It should fall through to the legacy string path.
|
|
out = humanize_error_detail("{not valid json{")
|
|
assert out["title"] == "ACME error"
|
|
assert out["raw"] == "{not valid json{"
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# 4. Structured RFC8555 problem types — table-driven across 11+ entries.
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize("problem_type,detail_text,expected_title_contains", [
|
|
("urn:ietf:params:acme:error:rateLimited", "too many orders", "rate limit"),
|
|
("urn:ietf:params:acme:error:dns", "no A record", "DNS"),
|
|
("urn:ietf:params:acme:error:caa", "issuance forbidden", "CAA"),
|
|
("urn:ietf:params:acme:error:connection", "timeout to :80", "could not connect"),
|
|
("urn:ietf:params:acme:error:incorrectResponse", "wrong key auth", "challenge response"),
|
|
("urn:ietf:params:acme:error:unauthorized", "verification failed", "Unauthorized"),
|
|
("urn:ietf:params:acme:error:malformed", "missing field 'csr'", "Malformed"),
|
|
("urn:ietf:params:acme:error:badNonce", "stale nonce", "nonce"),
|
|
("urn:ietf:params:acme:error:rejectedIdentifier", "blacklisted", "rejected"),
|
|
("urn:ietf:params:acme:error:serverInternal", "internal err", "ACME server"),
|
|
("urn:ietf:params:acme:error:userActionRequired", "agree to ToS", "User action"),
|
|
("urn:ietf:params:acme:error:externalAccountRequired", "EAB required", "External Account Binding"),
|
|
])
|
|
def test_known_problem_types_are_humanized(problem_type, detail_text, expected_title_contains):
|
|
payload = json.dumps({"type": problem_type, "detail": detail_text, "status": 400})
|
|
out = humanize_error_detail(payload)
|
|
assert expected_title_contains.lower() in out["title"].lower(), (
|
|
f"Title '{out['title']}' missing expected substring '{expected_title_contains}'"
|
|
)
|
|
assert out["message"] == detail_text
|
|
assert out["status"] == 400
|
|
assert out["type"] == problem_type
|
|
# Hint should be a non-empty operator-targeted string
|
|
assert isinstance(out["hint"], str) and len(out["hint"]) > 10
|
|
|
|
|
|
def test_unknown_problem_type_falls_back_to_generic_title():
|
|
payload = json.dumps({
|
|
"type": "urn:ietf:params:acme:error:notARealType",
|
|
"detail": "future error",
|
|
"status": 500,
|
|
})
|
|
out = humanize_error_detail(payload)
|
|
assert out["title"] == "ACME error"
|
|
assert out["message"] == "future error"
|
|
assert out["hint"] == ""
|
|
assert out["status"] == 500
|
|
assert out["type"] == "urn:ietf:params:acme:error:notARealType"
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# 5. Subproblems (per-domain failures) flatten cleanly.
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
def test_subproblems_are_flattened():
|
|
payload = json.dumps({
|
|
"type": "urn:ietf:params:acme:error:malformed",
|
|
"detail": "multiple validation failures",
|
|
"status": 400,
|
|
"subproblems": [
|
|
{
|
|
"type": "urn:ietf:params:acme:error:dns",
|
|
"detail": "no record for foo",
|
|
"identifier": {"type": "dns", "value": "foo.example.com"},
|
|
},
|
|
{
|
|
"type": "urn:ietf:params:acme:error:caa",
|
|
"detail": "caa forbids",
|
|
"identifier": {"type": "dns", "value": "bar.example.com"},
|
|
},
|
|
],
|
|
})
|
|
out = humanize_error_detail(payload)
|
|
assert "subproblems" in out
|
|
assert len(out["subproblems"]) == 2
|
|
sp = {item["identifier"]: item for item in out["subproblems"]}
|
|
assert sp["foo.example.com"]["type"] == "urn:ietf:params:acme:error:dns"
|
|
assert sp["bar.example.com"]["detail"] == "caa forbids"
|
|
|
|
|
|
def test_subproblems_with_garbage_entries_are_filtered():
|
|
payload = json.dumps({
|
|
"type": "urn:ietf:params:acme:error:malformed",
|
|
"detail": "x",
|
|
"subproblems": [
|
|
"not a dict", # filtered out
|
|
None, # filtered out
|
|
{"type": "urn:ietf:params:acme:error:dns", "detail": "ok"},
|
|
],
|
|
})
|
|
out = humanize_error_detail(payload)
|
|
assert len(out["subproblems"]) == 1
|
|
assert out["subproblems"][0]["detail"] == "ok"
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# 6. Dict input is accepted directly (avoids double-encode in some callers).
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
def test_dict_input_handled_directly():
|
|
out = humanize_error_detail({
|
|
"type": "urn:ietf:params:acme:error:rateLimited",
|
|
"detail": "limit reached",
|
|
"status": 429,
|
|
})
|
|
assert "rate limit" in out["title"].lower()
|
|
assert out["message"] == "limit reached"
|
|
assert out["status"] == 429
|