Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 59 additions & 5 deletions tests/test_ansible_events_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ def test_discourse_request_uses_explicit_run_as_identity(self, monkeypatch):
assert ansible_events_lib.DISCOURSE_CLIENT.session.headers["Api-Key"] == "test-api-key"
assert request.call_args.kwargs["headers"]["Api-Username"] == "organiser"

def test_discourse_request_retries_rate_limit_using_wait_seconds(self, monkeypatch):
def test_discourse_request_retries_rate_limit_using_wait_seconds(self, monkeypatch, caplog):
from tools.common import ansible_events_lib

monkeypatch.setattr(ansible_events_lib, "DISCOURSE_API_KEY", "test-api-key")
Expand All @@ -323,6 +323,8 @@ def test_discourse_request_retries_rate_limit_using_wait_seconds(self, monkeypat

assert request.call_count == 2
sleep.assert_called_once_with(31.0)
warning = next(record for record in caplog.records if record.levelname == "WARNING")
assert "Discourse rate limit for GET c/43/show.json" in warning.message

def test_discourse_request_rate_limit_retries_are_bounded(self, monkeypatch):
from tools.common import ansible_events_lib
Expand Down Expand Up @@ -725,7 +727,9 @@ def unexpected_request(*_args, **_kwargs):
]
)

def test_pretix_teams_do_not_require_pretix_two_factor_authentication(self, monkeypatch):
def test_pretix_teams_do_not_require_pretix_two_factor_authentication(self, monkeypatch, caplog):
import logging

from tools.environment import provision_environment

requests = []
Expand Down Expand Up @@ -753,12 +757,17 @@ def request(method, endpoint, payload=None):

monkeypatch.setattr(provision_environment, "pretix_list_all", list_all)
monkeypatch.setattr(provision_environment, "pretix_req", request)
caplog.set_level(logging.INFO, logger=provision_environment.logger.name)

provision_environment.reconcile_pretix_teams()

team_creations = [payload for method, endpoint, payload in requests if method == "POST" and endpoint == "teams"]
assert len(team_creations) == len(provision_environment.CITIES) + 1
assert all(team["require_2fa"] is False for team in team_creations)
messages = [record.getMessage() for record in caplog.records]
for city in provision_environment.CITIES:
assert city.city in messages
assert f" 🟡 {city.team_name}: Created" in messages

def test_template_ticket_reconciles_item_and_quota_drift(self, monkeypatch):
from tools.environment import provision_environment
Expand Down Expand Up @@ -903,29 +912,70 @@ def test_resource_status_lists_changed_and_already_correct_fields(self, caplog):
from tools.environment import provision_environment

caplog.set_level(logging.INFO, logger=provision_environment.logger.name)
provision_environment.set_log_scope("Test City")
provision_environment.log_resource_status(
"meetup-organisers-london",
{"visibility_level": 1, "members_visibility_level": 2},
{"visibility_level": 0, "members_visibility_level": 2},
)

assert "meetup-organisers-london: Updated | visibility: Changed, member visibility: OK" in caplog.text
messages = [record.getMessage() for record in caplog.records]
assert " 🟡 meetup-organisers-london: Updated" in messages
assert " 🟡 visibility: Updated" in messages
assert " ✅ member visibility: Already correct" in messages

def test_resource_status_marks_created_fields_as_verified(self, caplog):
import logging

from tools.environment import provision_environment

caplog.set_level(logging.INFO, logger=provision_environment.logger.name)
provision_environment.set_log_scope("Test City")
provision_environment.log_resource_status(
"meetup-attendee-london",
{"visibility_level": 3, "members_visibility_level": 3},
None,
)

assert "meetup-attendee-london: Created | visibility: OK, member visibility: OK" in caplog.text
messages = [record.getMessage() for record in caplog.records]
assert " 🟡 meetup-attendee-london: Created" in messages
assert " ✅ visibility: Already correct" in messages
assert " ✅ member visibility: Already correct" in messages

def test_resource_status_marks_manual_fields_for_follow_up(self, caplog):
import logging

from tools.environment import provision_environment

caplog.set_level(logging.INFO, logger=provision_environment.logger.name)
provision_environment.set_log_scope("Manual City")
provision_environment.log_resource_status(
"meetup-attendee-manual-city",
{"description": "desired"},
{"description": "custom"},
{"description": "OK:MANUAL"},
)

assert " ❓ description: Manual follow-up" in [record.getMessage() for record in caplog.records]

def test_provisioning_api_failure_has_failure_marker(self, caplog):
import logging

from tools.common import ansible_events_lib

caplog.set_level(logging.ERROR, logger=ansible_events_lib.logger.name)

def fail():
raise ansible_events_lib.ApiError("test failure")

with pytest.raises(SystemExit):
ansible_events_lib.run_cli(fail)

assert "❌ API request failed: test failure" in caplog.text

def test_city_category_uses_reply_only_default_and_city_organiser_access(self, monkeypatch, caplog):
import logging

def test_city_category_uses_reply_only_default_and_city_organiser_access(self, monkeypatch):
from tools.environment import provision_environment
from tools.common.ansible_events_lib import get_city

Expand Down Expand Up @@ -961,6 +1011,7 @@ def request(method, endpoint, payload=None):
raise AssertionError(f"Unexpected Discourse request: {method} {endpoint}")

monkeypatch.setattr(provision_environment, "discourse_req", request)
caplog.set_level(logging.INFO, logger=provision_environment.logger.name)
provision_environment.reconcile_city_category_access(
[current.copy()],
{51: plan},
Expand All @@ -974,6 +1025,9 @@ def request(method, endpoint, payload=None):
}
assert update["moderating_group_ids"] == [9]
assert sum(method == "GET" for method, _, _ in requests) == 2
messages = [record.getMessage() for record in caplog.records]
assert "London" in messages
assert " 🟡 Events > London: Updated" in messages

def test_organizer_access_is_removed_everywhere_except_own_subcategory(self, monkeypatch):
from tools.environment import provision_environment
Expand Down
22 changes: 17 additions & 5 deletions tests/test_check_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,15 +203,27 @@ def test_local_configuration_never_displays_secret_values(monkeypatch: pytest.Mo
assert "forum-secret" not in checker.results[0].detail


def test_print_results_fails_only_for_fail_status(capsys: pytest.CaptureFixture[str]) -> None:
def test_print_results_uses_hierarchical_status_markers_and_fails_only_on_fail(
capsys: pytest.CaptureFixture[str],
) -> None:
manual = check_setup.CheckResult("MANUAL", "server check", "operator", "unavailable", "inspect")
city = check_setup.CheckResult(
"PASS",
f"Forum/Pretix mapping: {CITIES[0].city}",
f"{CITIES[0].organiser_group} → {CITIES[0].team_name}",
"mapping is correct",
"None.",
)
failure = check_setup.CheckResult("FAIL", "resource", "GET settings", "missing", "reconcile")

assert check_setup.print_results([manual]) is False
assert check_setup.print_results([manual, city]) is False
manual_output = capsys.readouterr().out
assert "1 MANUAL" in manual_output
assert "🔴" not in manual_output
assert check_setup.print_results([manual, failure]) is True
assert "Global\n ❓ MANUAL: server check" in manual_output
assert f"{CITIES[0].city}\n ✅ PASS: Forum/Pretix mapping: {CITIES[0].city}" in manual_output
assert " Target: operator" in manual_output
assert "❌ FAIL:" not in manual_output
assert check_setup.print_results([manual, city, failure]) is True
failed_output = capsys.readouterr().out
assert "🔴 FAIL: resource" in failed_output
assert " ❌ FAIL: resource" in failed_output
assert "1 FAIL" in failed_output
6 changes: 3 additions & 3 deletions tools/common/ansible_events_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def run_cli(action: Callable[[], None]) -> None:
try:
action()
except ApiError as exc:
logger.error("API request failed: %s", exc)
logger.error("❌ API request failed: %s", exc)
raise SystemExit(1) from exc


Expand Down Expand Up @@ -318,10 +318,10 @@ def pre_flight_checks(*, require_discourse: bool = True, require_pretix: bool =
if require_pretix:
parsed = urlparse(PRETIX_URL)
if parsed.scheme != "https" and parsed.hostname not in {"localhost", "127.0.0.1", "::1"}:
logger.error("PRETIX_URL must use HTTPS outside localhost/loopback")
logger.error("❌ PRETIX_URL must use HTTPS outside localhost/loopback")
missing.append("secure PRETIX_URL")
if missing:
logger.error("Missing required environment variables: %s", ", ".join(missing))
logger.error("❌ Missing required environment variables: %s", ", ".join(missing))
sys.exit(1)


Expand Down
40 changes: 33 additions & 7 deletions tools/environment/check_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,16 +775,42 @@ def run(self) -> list[CheckResult]:
return self.results


def result_scope(result: CheckResult) -> str:
"""Return the city section for a city-specific check, or Global."""
searchable = f"{result.name} {result.target}".casefold()
for city in CITIES:
if any(
identifier.casefold() in searchable
for identifier in (city.city, city.slug, city.organiser_group, city.team_name)
):
return city.city
return "Global"


def print_results(results: list[CheckResult]) -> bool:
"""Print sanitized results and return whether any diagnostic failed."""
"""Print sanitized, hierarchically grouped results and report whether any failed."""
grouped: dict[str, list[CheckResult]] = {"Global": []}
grouped.update({city.city: [] for city in CITIES})
for result in results:
marker = "🔴 " if result.status == "FAIL" else ""
print(f"{marker}{result.status}: {result.name}")
print(f" Target: {result.target}")
print(f" Result: {result.detail}")
print(f" Next: {result.next_step}")
grouped[result_scope(result)].append(result)

for scope in ("Global", *(city.city for city in CITIES)):
scoped_results = grouped[scope]
if not scoped_results:
continue
print(scope)
for result in scoped_results:
marker = {"PASS": "✅", "FAIL": "❌", "MANUAL": "❓"}.get(result.status, "❌")
print(f" {marker} {result.status}: {result.name}")
print(f" Target: {result.target}")
print(f" Result: {result.detail}")
print(f" Next: {result.next_step}")
counts = {status: sum(result.status == status for result in results) for status in ("PASS", "FAIL", "MANUAL")}
print(f"Summary: {counts['PASS']} PASS, {counts['FAIL']} FAIL, {counts['MANUAL']} MANUAL")
print(
f"Summary: ✅ {counts['PASS']} PASS, "
f"{('❌ ' if counts['FAIL'] else '')}{counts['FAIL']} FAIL, "
f"❓ {counts['MANUAL']} MANUAL"
)
return counts["FAIL"] > 0


Expand Down
Loading
Loading