Test dashboard: full menu system, all-service tests, filtering
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m11s

- Run tests for all 10 services via per-service pytest subprocesses
- Group results by service with section headers
- Clickable summary cards filter by outcome (passed/failed/errors/skipped)
- Service filter nav using ~nav-link buttons in menu bar
- Full menu integration: ~header-row + ~header-child + ~menu-row
- Show logo image via cart-mini rendering
- Mount full service directories in docker-compose for test access
- Add 24 unit test files across 9 services

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 22:54:25 +00:00
parent 81e51ae7bc
commit 3809affcab
41 changed files with 2484 additions and 110 deletions

0
cart/tests/__init__.py Normal file
View File

View File

@@ -0,0 +1,59 @@
"""Unit tests for calendar/ticket total functions."""
from __future__ import annotations
from decimal import Decimal
from types import SimpleNamespace
import pytest
from cart.bp.cart.services.calendar_cart import calendar_total, ticket_total
def _entry(cost):
return SimpleNamespace(cost=cost)
def _ticket(price):
return SimpleNamespace(price=price)
class TestCalendarTotal:
def test_empty(self):
assert calendar_total([]) == 0
def test_single_entry(self):
assert calendar_total([_entry(25.0)]) == Decimal("25.0")
def test_none_cost_excluded(self):
result = calendar_total([_entry(None)])
assert result == 0
def test_zero_cost(self):
# cost=0 is falsy, so it produces Decimal(0) via the else branch
result = calendar_total([_entry(0)])
assert result == Decimal("0")
def test_multiple(self):
result = calendar_total([_entry(10.0), _entry(20.0)])
assert result == Decimal("30.0")
class TestTicketTotal:
def test_empty(self):
assert ticket_total([]) == Decimal("0")
def test_single_ticket(self):
assert ticket_total([_ticket(15.0)]) == Decimal("15.0")
def test_none_price_treated_as_zero(self):
# ticket_total includes all tickets, None → Decimal(0)
result = ticket_total([_ticket(None)])
assert result == Decimal("0")
def test_multiple(self):
result = ticket_total([_ticket(5.0), _ticket(10.0)])
assert result == Decimal("15.0")
def test_mixed_with_none(self):
result = ticket_total([_ticket(10.0), _ticket(None), _ticket(5.0)])
assert result == Decimal("15.0")

139
cart/tests/test_checkout.py Normal file
View File

@@ -0,0 +1,139 @@
"""Unit tests for cart checkout helpers."""
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from cart.bp.cart.services.checkout import (
build_sumup_description,
build_sumup_reference,
build_webhook_url,
validate_webhook_secret,
)
def _ci(title=None, qty=1):
return SimpleNamespace(product_title=title, quantity=qty)
# ---------------------------------------------------------------------------
# build_sumup_description
# ---------------------------------------------------------------------------
class TestBuildSumupDescription:
def test_empty_cart_no_tickets(self):
result = build_sumup_description([], 1)
assert "Order 1" in result
assert "0 items" in result
assert "order items" in result
def test_single_item(self):
result = build_sumup_description([_ci("Widget", 1)], 42)
assert "Order 42" in result
assert "1 item)" in result
assert "Widget" in result
def test_quantity_counted(self):
result = build_sumup_description([_ci("Widget", 3)], 1)
assert "3 items" in result
def test_three_titles(self):
items = [_ci("A"), _ci("B"), _ci("C")]
result = build_sumup_description(items, 1)
assert "A, B, C" in result
assert "more" not in result
def test_four_titles_truncated(self):
items = [_ci("A"), _ci("B"), _ci("C"), _ci("D")]
result = build_sumup_description(items, 1)
assert "A, B, C" in result
assert "+ 1 more" in result
def test_none_titles_excluded(self):
items = [_ci(None), _ci("Visible")]
result = build_sumup_description(items, 1)
assert "Visible" in result
def test_tickets_singular(self):
result = build_sumup_description([], 1, ticket_count=1)
assert "1 ticket" in result
assert "tickets" not in result
def test_tickets_plural(self):
result = build_sumup_description([], 1, ticket_count=3)
assert "3 tickets" in result
def test_mixed_items_and_tickets(self):
result = build_sumup_description([_ci("A", 2)], 1, ticket_count=1)
assert "3 items" in result # 2 + 1
# ---------------------------------------------------------------------------
# build_sumup_reference
# ---------------------------------------------------------------------------
class TestBuildSumupReference:
def test_with_page_config(self):
pc = SimpleNamespace(sumup_checkout_prefix="SHOP-")
assert build_sumup_reference(42, pc) == "SHOP-42"
def test_without_page_config(self):
with patch("cart.bp.cart.services.checkout.config",
return_value={"sumup": {"checkout_reference_prefix": "RA-"}}):
assert build_sumup_reference(99) == "RA-99"
def test_no_prefix(self):
with patch("cart.bp.cart.services.checkout.config",
return_value={"sumup": {}}):
assert build_sumup_reference(1) == "1"
# ---------------------------------------------------------------------------
# build_webhook_url
# ---------------------------------------------------------------------------
class TestBuildWebhookUrl:
def test_no_secret(self):
with patch("cart.bp.cart.services.checkout.config",
return_value={"sumup": {}}):
assert build_webhook_url("https://x.com/hook") == "https://x.com/hook"
def test_with_secret_no_query(self):
with patch("cart.bp.cart.services.checkout.config",
return_value={"sumup": {"webhook_secret": "s3cret"}}):
result = build_webhook_url("https://x.com/hook")
assert "?token=s3cret" in result
def test_with_secret_existing_query(self):
with patch("cart.bp.cart.services.checkout.config",
return_value={"sumup": {"webhook_secret": "s3cret"}}):
result = build_webhook_url("https://x.com/hook?a=1")
assert "&token=s3cret" in result
# ---------------------------------------------------------------------------
# validate_webhook_secret
# ---------------------------------------------------------------------------
class TestValidateWebhookSecret:
def test_no_secret_configured(self):
with patch("cart.bp.cart.services.checkout.config",
return_value={"sumup": {}}):
assert validate_webhook_secret(None) is True
def test_correct_token(self):
with patch("cart.bp.cart.services.checkout.config",
return_value={"sumup": {"webhook_secret": "abc"}}):
assert validate_webhook_secret("abc") is True
def test_wrong_token(self):
with patch("cart.bp.cart.services.checkout.config",
return_value={"sumup": {"webhook_secret": "abc"}}):
assert validate_webhook_secret("wrong") is False
def test_none_token_with_secret(self):
with patch("cart.bp.cart.services.checkout.config",
return_value={"sumup": {"webhook_secret": "abc"}}):
assert validate_webhook_secret(None) is False

View File

@@ -0,0 +1,77 @@
"""Unit tests for ticket grouping logic."""
from __future__ import annotations
from types import SimpleNamespace
from datetime import datetime
import pytest
from cart.bp.cart.services.ticket_groups import group_tickets
def _ticket(entry_id=1, entry_name="Event", ticket_type_id=None,
ticket_type_name=None, price=10.0,
entry_start_at=None, entry_end_at=None):
return SimpleNamespace(
entry_id=entry_id,
entry_name=entry_name,
entry_start_at=entry_start_at or datetime(2025, 6, 1, 10, 0),
entry_end_at=entry_end_at,
ticket_type_id=ticket_type_id,
ticket_type_name=ticket_type_name,
price=price,
)
class TestGroupTickets:
def test_empty(self):
assert group_tickets([]) == []
def test_single_ticket(self):
result = group_tickets([_ticket()])
assert len(result) == 1
assert result[0]["quantity"] == 1
assert result[0]["line_total"] == 10.0
def test_same_group_merged(self):
tickets = [_ticket(entry_id=1), _ticket(entry_id=1)]
result = group_tickets(tickets)
assert len(result) == 1
assert result[0]["quantity"] == 2
assert result[0]["line_total"] == 20.0
def test_different_entries_separate(self):
tickets = [_ticket(entry_id=1), _ticket(entry_id=2)]
result = group_tickets(tickets)
assert len(result) == 2
def test_different_ticket_types_separate(self):
tickets = [
_ticket(entry_id=1, ticket_type_id=1, ticket_type_name="Adult"),
_ticket(entry_id=1, ticket_type_id=2, ticket_type_name="Child"),
]
result = group_tickets(tickets)
assert len(result) == 2
def test_none_price(self):
result = group_tickets([_ticket(price=None)])
assert result[0]["line_total"] == 0.0
def test_ordering_preserved(self):
tickets = [
_ticket(entry_id=2, entry_name="Second"),
_ticket(entry_id=1, entry_name="First"),
]
result = group_tickets(tickets)
assert result[0]["entry_name"] == "Second"
assert result[1]["entry_name"] == "First"
def test_metadata_from_first_ticket(self):
tickets = [
_ticket(entry_id=1, entry_name="A", price=5.0),
_ticket(entry_id=1, entry_name="B", price=10.0),
]
result = group_tickets(tickets)
assert result[0]["entry_name"] == "A" # from first
assert result[0]["price"] == 5.0 # from first
assert result[0]["line_total"] == 15.0 # accumulated

47
cart/tests/test_total.py Normal file
View File

@@ -0,0 +1,47 @@
"""Unit tests for cart total calculations."""
from __future__ import annotations
from decimal import Decimal
from types import SimpleNamespace
import pytest
from cart.bp.cart.services.total import total
def _item(special=None, regular=None, qty=1):
return SimpleNamespace(
product_special_price=special,
product_regular_price=regular,
quantity=qty,
)
class TestTotal:
def test_empty_cart(self):
assert total([]) == 0
def test_regular_price_only(self):
result = total([_item(regular=10.0, qty=2)])
assert result == Decimal("20.0")
def test_special_price_preferred(self):
result = total([_item(special=5.0, regular=10.0, qty=1)])
assert result == Decimal("5.0")
def test_none_prices_excluded(self):
result = total([_item(special=None, regular=None, qty=1)])
assert result == 0
def test_mixed_items(self):
items = [
_item(special=5.0, qty=2), # 10
_item(regular=3.0, qty=3), # 9
_item(), # excluded
]
result = total(items)
assert result == Decimal("19.0")
def test_quantity_multiplication(self):
result = total([_item(regular=7.5, qty=4)])
assert result == Decimal("30.0")