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
events/tests/__init__.py
Normal file
0
events/tests/__init__.py
Normal file
30
events/tests/conftest.py
Normal file
30
events/tests/conftest.py
Normal 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")
|
||||
82
events/tests/test_calendar_view.py
Normal file
82
events/tests/test_calendar_view.py
Normal 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
|
||||
57
events/tests/test_entry_cost.py
Normal file
57
events/tests/test_entry_cost.py
Normal 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")
|
||||
59
events/tests/test_slots.py
Normal file
59
events/tests/test_slots.py
Normal 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
|
||||
42
events/tests/test_slugify.py
Normal file
42
events/tests/test_slugify.py
Normal 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"
|
||||
Reference in New Issue
Block a user