Test dashboard: full menu system, all-service tests, filtering
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m11s
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:
0
cart/tests/__init__.py
Normal file
0
cart/tests/__init__.py
Normal file
59
cart/tests/test_calendar_cart.py
Normal file
59
cart/tests/test_calendar_cart.py
Normal 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
139
cart/tests/test_checkout.py
Normal 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
|
||||
77
cart/tests/test_ticket_groups.py
Normal file
77
cart/tests/test_ticket_groups.py
Normal 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
47
cart/tests/test_total.py
Normal 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")
|
||||
Reference in New Issue
Block a user