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
events/tests/__init__.py Normal file
View File

30
events/tests/conftest.py Normal file
View File

@@ -0,0 +1,30 @@
"""Events test fixtures — direct module loading to avoid bp __init__ chains."""
from __future__ import annotations
import importlib.util
import sys
def _load(name: str, path: str):
"""Import a .py file directly, bypassing package __init__ chains."""
if name in sys.modules:
return sys.modules[name]
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
# Ensure events/models is importable as 'models'
sys.path.insert(0, "/app/events")
# Pre-load target modules that would fail via normal package import
_load("events.bp.calendar.services.calendar_view",
"/app/events/bp/calendar/services/calendar_view.py")
_load("events.bp.calendar.services.slots",
"/app/events/bp/calendar/services/slots.py")
_load("events.bp.calendars.services.calendars",
"/app/events/bp/calendars/services/calendars.py")
_load("events.bp.calendar_entries.routes",
"/app/events/bp/calendar_entries/routes.py")

View File

@@ -0,0 +1,82 @@
"""Unit tests for calendar view helpers."""
from __future__ import annotations
from datetime import date
import pytest
from events.bp.calendar.services.calendar_view import add_months, build_calendar_weeks
class TestAddMonths:
def test_same_month(self):
assert add_months(2025, 6, 0) == (2025, 6)
def test_forward_one(self):
assert add_months(2025, 6, 1) == (2025, 7)
def test_december_to_january(self):
assert add_months(2025, 12, 1) == (2026, 1)
def test_january_to_december(self):
assert add_months(2025, 1, -1) == (2024, 12)
def test_multi_year_forward(self):
assert add_months(2025, 1, 24) == (2027, 1)
def test_multi_year_backward(self):
assert add_months(2025, 3, -15) == (2023, 12)
def test_negative_large(self):
y, m = add_months(2025, 6, -30)
assert m >= 1 and m <= 12
assert y == 2022 # 2025-06 minus 30 months = 2022-12
def test_forward_eleven(self):
assert add_months(2025, 1, 11) == (2025, 12)
def test_forward_twelve(self):
assert add_months(2025, 1, 12) == (2026, 1)
class TestBuildCalendarWeeks:
def test_returns_weeks(self):
weeks = build_calendar_weeks(2025, 6)
assert len(weeks) >= 4
assert len(weeks) <= 6
def test_seven_days_per_week(self):
weeks = build_calendar_weeks(2025, 1)
for week in weeks:
assert len(week) == 7
def test_day_structure(self):
weeks = build_calendar_weeks(2025, 6)
day = weeks[0][0]
assert "date" in day
assert "in_month" in day
assert "is_today" in day
assert isinstance(day["date"], date)
def test_in_month_flag(self):
weeks = build_calendar_weeks(2025, 6)
# First day of first week might be in May
june_days = [
d for week in weeks for d in week if d["in_month"]
]
assert len(june_days) == 30 # June has 30 days
def test_february_leap_year(self):
weeks = build_calendar_weeks(2024, 2)
feb_days = [d for week in weeks for d in week if d["in_month"]]
assert len(feb_days) == 29
def test_february_non_leap(self):
weeks = build_calendar_weeks(2025, 2)
feb_days = [d for week in weeks for d in week if d["in_month"]]
assert len(feb_days) == 28
def test_starts_on_monday(self):
weeks = build_calendar_weeks(2025, 6)
# First day of first week should be a Monday
assert weeks[0][0]["date"].weekday() == 0 # Monday

View File

@@ -0,0 +1,57 @@
"""Unit tests for entry cost calculation."""
from __future__ import annotations
from datetime import datetime, time
from decimal import Decimal
from types import SimpleNamespace
import pytest
from events.bp.calendar_entries.routes import calculate_entry_cost
def _slot(cost=None, flexible=False, time_start=None, time_end=None):
return SimpleNamespace(
cost=cost,
flexible=flexible,
time_start=time_start or time(9, 0),
time_end=time_end or time(17, 0),
)
class TestCalculateEntryCost:
def test_no_cost(self):
assert calculate_entry_cost(_slot(cost=None),
datetime(2025, 1, 1, 9), datetime(2025, 1, 1, 17)) == Decimal("0")
def test_fixed_slot(self):
result = calculate_entry_cost(_slot(cost=100, flexible=False),
datetime(2025, 1, 1, 10), datetime(2025, 1, 1, 12))
assert result == Decimal("100")
def test_flexible_full_duration(self):
slot = _slot(cost=80, flexible=True, time_start=time(9, 0), time_end=time(17, 0))
# 8 hours slot, booking full 8 hours
result = calculate_entry_cost(slot,
datetime(2025, 1, 1, 9, 0), datetime(2025, 1, 1, 17, 0))
assert result == Decimal("80")
def test_flexible_half_duration(self):
slot = _slot(cost=80, flexible=True, time_start=time(9, 0), time_end=time(17, 0))
# 4 hours of 8-hour slot = half
result = calculate_entry_cost(slot,
datetime(2025, 1, 1, 9, 0), datetime(2025, 1, 1, 13, 0))
assert result == Decimal("40")
def test_flexible_no_time_end(self):
slot = _slot(cost=50, flexible=True, time_end=None)
result = calculate_entry_cost(slot,
datetime(2025, 1, 1, 9), datetime(2025, 1, 1, 12))
# When time_end is None, function still calculates based on booking duration
assert isinstance(result, Decimal)
def test_flexible_zero_slot_duration(self):
slot = _slot(cost=50, flexible=True, time_start=time(9, 0), time_end=time(9, 0))
result = calculate_entry_cost(slot,
datetime(2025, 1, 1, 9, 0), datetime(2025, 1, 1, 10, 0))
assert result == Decimal("0")

View File

@@ -0,0 +1,59 @@
"""Unit tests for slot helper functions."""
from __future__ import annotations
import pytest
from events.bp.calendar.services.slots import _b
class TestBoolParse:
def test_true_bool(self):
assert _b(True) is True
def test_false_bool(self):
assert _b(False) is False
def test_string_true(self):
assert _b("true") is True
def test_string_True(self):
assert _b("True") is True
def test_string_1(self):
assert _b("1") is True
def test_string_yes(self):
assert _b("yes") is True
def test_string_y(self):
assert _b("y") is True
def test_string_t(self):
assert _b("t") is True
def test_string_on(self):
assert _b("on") is True
def test_string_false(self):
assert _b("false") is False
def test_string_0(self):
assert _b("0") is False
def test_string_no(self):
assert _b("no") is False
def test_string_off(self):
assert _b("off") is False
def test_string_empty(self):
assert _b("") is False
def test_int_1(self):
assert _b(1) is True
def test_int_0(self):
assert _b(0) is False
def test_random_string(self):
assert _b("maybe") is False

View File

@@ -0,0 +1,42 @@
"""Unit tests for events slugify utility."""
from __future__ import annotations
import pytest
from events.bp.calendars.services.calendars import slugify
class TestSlugify:
def test_basic(self):
assert slugify("My Calendar") == "my-calendar"
def test_unicode(self):
assert slugify("café résumé") == "cafe-resume"
def test_slashes(self):
assert slugify("foo/bar") == "foo-bar"
def test_special_chars(self):
assert slugify("hello!!world") == "hello-world"
def test_collapse_dashes(self):
assert slugify("a---b") == "a-b"
def test_strip_dashes(self):
assert slugify("--hello--") == "hello"
def test_empty_fallback(self):
assert slugify("") == "calendar"
def test_none_fallback(self):
assert slugify(None) == "calendar"
def test_max_len(self):
result = slugify("a" * 300, max_len=10)
assert len(result) <= 10
def test_numbers(self):
assert slugify("event-2025") == "event-2025"
def test_already_clean(self):
assert slugify("my-event") == "my-event"