- Updated the language configuration to use a single source of truth from `DEFAULT_LANGUAGE` for the bot, API, and Mini App, eliminating auto-detection from user settings. - Refactored the `get_lang` function to always return `DEFAULT_LANGUAGE`, ensuring consistent language usage throughout the application. - Modified the handling of language in various components, including API responses and UI elements, to reflect the new language management approach. - Enhanced documentation and comments to clarify the changes in language handling. - Added unit tests to verify the new language handling behavior and ensure coverage for the updated functionality.
528 lines
20 KiB
Python
528 lines
20 KiB
Python
"""Tests for FastAPI app /api/duties."""
|
||
|
||
import time
|
||
from unittest.mock import ANY, patch
|
||
|
||
import pytest
|
||
from fastapi.testclient import TestClient
|
||
|
||
import duty_teller.config as config
|
||
from duty_teller.api.app import app
|
||
from tests.helpers import make_init_data
|
||
|
||
|
||
@pytest.fixture
|
||
def client():
|
||
return TestClient(app)
|
||
|
||
|
||
def test_health(client):
|
||
"""Health endpoint returns 200 and status ok for Docker HEALTHCHECK."""
|
||
r = client.get("/health")
|
||
assert r.status_code == 200
|
||
assert r.json() == {"status": "ok"}
|
||
|
||
|
||
def test_health_has_vary_accept_language(client):
|
||
"""NoCacheStaticMiddleware adds Vary: Accept-Language to all responses."""
|
||
r = client.get("/health")
|
||
assert r.status_code == 200
|
||
assert "accept-language" in r.headers.get("vary", "").lower()
|
||
|
||
|
||
def test_app_static_has_no_store_and_vary(client):
|
||
"""Static files under /app get Cache-Control: no-store and Vary: Accept-Language."""
|
||
r = client.get("/app/")
|
||
if r.status_code != 200:
|
||
r = client.get("/app")
|
||
assert r.status_code == 200, "webapp static mount should serve index at /app or /app/"
|
||
assert r.headers.get("cache-control") == "no-store"
|
||
assert "accept-language" in r.headers.get("vary", "").lower()
|
||
|
||
|
||
def test_app_js_has_no_store(client):
|
||
"""JS and all static under /app get Cache-Control: no-store."""
|
||
r = client.get("/app/js/main.js")
|
||
assert r.status_code == 200
|
||
assert r.headers.get("cache-control") == "no-store"
|
||
|
||
|
||
def test_app_config_js_returns_lang_from_default_language(client):
|
||
"""GET /app/config.js returns JS setting window.__DT_LANG from config.DEFAULT_LANGUAGE."""
|
||
r = client.get("/app/config.js")
|
||
assert r.status_code == 200
|
||
assert r.headers.get("content-type", "").startswith("application/javascript")
|
||
assert r.headers.get("cache-control") == "no-store"
|
||
body = r.text
|
||
assert "window.__DT_LANG" in body
|
||
assert config.DEFAULT_LANGUAGE in body
|
||
|
||
|
||
def test_duties_invalid_date_format(client):
|
||
r = client.get("/api/duties", params={"from": "2025-01-01", "to": "invalid"})
|
||
assert r.status_code == 400
|
||
detail = r.json()["detail"]
|
||
assert "from" in detail.lower() and "to" in detail.lower()
|
||
|
||
|
||
def test_duties_from_after_to(client):
|
||
r = client.get("/api/duties", params={"from": "2025-02-01", "to": "2025-01-01"})
|
||
assert r.status_code == 400
|
||
detail = r.json()["detail"].lower()
|
||
assert "from" in detail or "to" in detail or "after" in detail or "позже" in detail
|
||
|
||
|
||
@patch("duty_teller.api.dependencies.config.MINI_APP_SKIP_AUTH", False)
|
||
def test_duties_403_without_init_data(client):
|
||
"""Without X-Telegram-Init-Data and without MINI_APP_SKIP_AUTH → 403 (any client)."""
|
||
r = client.get(
|
||
"/api/duties",
|
||
params={"from": "2025-01-01", "to": "2025-01-31"},
|
||
)
|
||
assert r.status_code == 403
|
||
|
||
|
||
@patch("duty_teller.api.app.config.MINI_APP_SKIP_AUTH", True)
|
||
@patch("duty_teller.api.app.fetch_duties_response")
|
||
def test_duties_200_when_skip_auth(mock_fetch, client):
|
||
mock_fetch.return_value = []
|
||
r = client.get(
|
||
"/api/duties",
|
||
params={"from": "2025-01-01", "to": "2025-01-31"},
|
||
)
|
||
assert r.status_code == 200
|
||
assert r.json() == []
|
||
mock_fetch.assert_called_once_with(ANY, "2025-01-01", "2025-01-31")
|
||
|
||
|
||
@patch("duty_teller.api.dependencies.validate_init_data_with_reason")
|
||
@patch("duty_teller.api.dependencies.config.MINI_APP_SKIP_AUTH", False)
|
||
def test_duties_403_when_init_data_invalid(mock_validate, client):
|
||
mock_validate.return_value = (None, None, "hash_mismatch", "en")
|
||
r = client.get(
|
||
"/api/duties",
|
||
params={"from": "2025-01-01", "to": "2025-01-31"},
|
||
headers={"X-Telegram-Init-Data": "some=data&hash=abc"},
|
||
)
|
||
assert r.status_code == 403
|
||
detail = r.json()["detail"]
|
||
assert (
|
||
"signature" in detail.lower()
|
||
or "авторизации" in detail
|
||
or "Неверные" in detail
|
||
or "Неверная" in detail
|
||
)
|
||
|
||
|
||
@patch("duty_teller.api.dependencies.get_user_by_telegram_id")
|
||
@patch("duty_teller.api.dependencies.validate_init_data_with_reason")
|
||
@patch("duty_teller.api.dependencies.config.MINI_APP_SKIP_AUTH", False)
|
||
def test_duties_403_when_user_not_in_db(mock_validate, mock_get_user, client):
|
||
"""User not in DB -> 403 (access only for users with role or env-admin fallback)."""
|
||
mock_validate.return_value = (123, "someuser", "ok", "en")
|
||
mock_get_user.return_value = None
|
||
with patch("duty_teller.api.app.fetch_duties_response") as mock_fetch:
|
||
r = client.get(
|
||
"/api/duties",
|
||
params={"from": "2025-01-01", "to": "2025-01-31"},
|
||
headers={"X-Telegram-Init-Data": "user=xxx&hash=yyy"},
|
||
)
|
||
assert r.status_code == 403
|
||
assert (
|
||
"Access denied" in r.json()["detail"] or "Доступ запрещён" in r.json()["detail"]
|
||
)
|
||
mock_fetch.assert_not_called()
|
||
|
||
|
||
@patch("duty_teller.api.dependencies.can_access_miniapp_for_telegram_user")
|
||
@patch("duty_teller.api.dependencies.get_user_by_telegram_id")
|
||
@patch("duty_teller.api.dependencies.validate_init_data_with_reason")
|
||
@patch("duty_teller.api.dependencies.config.MINI_APP_SKIP_AUTH", False)
|
||
def test_duties_403_when_no_username_and_no_access(
|
||
mock_validate, mock_get_user, mock_can_access, client
|
||
):
|
||
"""User in DB but no miniapp access (no role, not env admin) -> 403."""
|
||
from types import SimpleNamespace
|
||
|
||
mock_validate.return_value = (456, None, "ok", "en")
|
||
mock_get_user.return_value = SimpleNamespace(
|
||
phone="+79001111111", full_name="User", username=None
|
||
)
|
||
mock_can_access.return_value = False
|
||
with patch("duty_teller.api.app.fetch_duties_response") as mock_fetch:
|
||
r = client.get(
|
||
"/api/duties",
|
||
params={"from": "2025-01-01", "to": "2025-01-31"},
|
||
headers={"X-Telegram-Init-Data": "user=xxx&hash=yyy"},
|
||
)
|
||
assert r.status_code == 403
|
||
mock_fetch.assert_not_called()
|
||
|
||
|
||
@patch("duty_teller.api.dependencies.can_access_miniapp_for_telegram_user")
|
||
@patch("duty_teller.api.dependencies.get_user_by_telegram_id")
|
||
@patch("duty_teller.api.dependencies.validate_init_data_with_reason")
|
||
@patch("duty_teller.api.dependencies.config.MINI_APP_SKIP_AUTH", False)
|
||
def test_duties_200_when_user_has_access(
|
||
mock_validate, mock_get_user, mock_can_access, client
|
||
):
|
||
"""User in DB with miniapp access (role or env fallback) -> 200."""
|
||
from types import SimpleNamespace
|
||
|
||
mock_validate.return_value = (789, None, "ok", "en")
|
||
mock_get_user.return_value = SimpleNamespace(
|
||
phone="+7 900 123-45-67", full_name="Иван", username=None
|
||
)
|
||
mock_can_access.return_value = True
|
||
with patch("duty_teller.api.app.fetch_duties_response") as mock_fetch:
|
||
mock_fetch.return_value = []
|
||
r = client.get(
|
||
"/api/duties",
|
||
params={"from": "2025-01-01", "to": "2025-01-31"},
|
||
headers={"X-Telegram-Init-Data": "user=xxx&hash=yyy"},
|
||
)
|
||
assert r.status_code == 200
|
||
assert r.json() == []
|
||
mock_fetch.assert_called_once_with(ANY, "2025-01-01", "2025-01-31")
|
||
|
||
|
||
@patch("duty_teller.api.dependencies.validate_init_data_with_reason")
|
||
@patch("duty_teller.api.dependencies.config.can_access_miniapp")
|
||
@patch("duty_teller.api.dependencies.config.MINI_APP_SKIP_AUTH", True)
|
||
def test_duties_200_with_valid_init_data_and_skip_auth_not_in_allowlist(
|
||
mock_can_access, mock_validate, client
|
||
):
|
||
"""With MINI_APP_SKIP_AUTH=True, valid initData grants access without allowlist check."""
|
||
mock_validate.return_value = ("outsider_user", "ok", "en")
|
||
mock_can_access.return_value = False
|
||
with patch("duty_teller.api.app.fetch_duties_response") as mock_fetch:
|
||
mock_fetch.return_value = []
|
||
r = client.get(
|
||
"/api/duties",
|
||
params={"from": "2025-01-01", "to": "2025-01-31"},
|
||
headers={"X-Telegram-Init-Data": "user=xxx&hash=yyy"},
|
||
)
|
||
assert r.status_code == 200
|
||
assert r.json() == []
|
||
mock_fetch.assert_called_once_with(ANY, "2025-01-01", "2025-01-31")
|
||
mock_can_access.assert_not_called()
|
||
mock_validate.assert_not_called()
|
||
|
||
|
||
@patch("duty_teller.api.dependencies.can_access_miniapp_for_telegram_user")
|
||
@patch("duty_teller.api.dependencies.get_user_by_telegram_id")
|
||
@patch("duty_teller.api.dependencies.validate_init_data_with_reason")
|
||
def test_duties_200_with_allowed_user(
|
||
mock_validate, mock_get_user, mock_can_access, client
|
||
):
|
||
from types import SimpleNamespace
|
||
|
||
mock_validate.return_value = (1, "alloweduser", "ok", "en")
|
||
mock_get_user.return_value = SimpleNamespace(
|
||
full_name="Иван Иванов", username="alloweduser", phone=None
|
||
)
|
||
mock_can_access.return_value = True
|
||
with patch("duty_teller.api.app.fetch_duties_response") as mock_fetch:
|
||
mock_fetch.return_value = [
|
||
{
|
||
"id": 1,
|
||
"user_id": 10,
|
||
"start_at": "2025-01-15T09:00:00Z",
|
||
"end_at": "2025-01-15T18:00:00Z",
|
||
"full_name": "Иван Иванов",
|
||
}
|
||
]
|
||
r = client.get(
|
||
"/api/duties",
|
||
params={"from": "2025-01-01", "to": "2025-01-31"},
|
||
headers={"X-Telegram-Init-Data": "user=xxx&hash=yyy"},
|
||
)
|
||
assert r.status_code == 200
|
||
assert len(r.json()) == 1
|
||
assert r.json()[0]["full_name"] == "Иван Иванов"
|
||
mock_fetch.assert_called_once_with(ANY, "2025-01-01", "2025-01-31")
|
||
|
||
|
||
@patch("duty_teller.api.dependencies.can_access_miniapp_for_telegram_user")
|
||
@patch("duty_teller.api.dependencies.get_user_by_telegram_id")
|
||
def test_duties_e2e_auth_real_validation(
|
||
mock_get_user, mock_can_access, client, monkeypatch
|
||
):
|
||
from types import SimpleNamespace
|
||
|
||
test_token = "123:ABC"
|
||
test_username = "e2euser"
|
||
monkeypatch.setattr(config, "BOT_TOKEN", test_token)
|
||
monkeypatch.setattr(config, "ADMIN_USERNAMES", set())
|
||
monkeypatch.setattr(config, "INIT_DATA_MAX_AGE_SECONDS", 0)
|
||
init_data = make_init_data(
|
||
{"id": 1, "username": test_username},
|
||
test_token,
|
||
auth_date=int(time.time()),
|
||
)
|
||
mock_get_user.return_value = SimpleNamespace(
|
||
full_name="E2E User", username=test_username, phone=None
|
||
)
|
||
mock_can_access.return_value = True
|
||
with patch("duty_teller.api.app.fetch_duties_response") as mock_fetch:
|
||
mock_fetch.return_value = []
|
||
r = client.get(
|
||
"/api/duties",
|
||
params={"from": "2025-01-01", "to": "2025-01-31"},
|
||
headers={"X-Telegram-Init-Data": init_data},
|
||
)
|
||
assert r.status_code == 200
|
||
assert r.json() == []
|
||
mock_fetch.assert_called_once_with(ANY, "2025-01-01", "2025-01-31")
|
||
|
||
|
||
@patch("duty_teller.api.app.config.MINI_APP_SKIP_AUTH", True)
|
||
def test_duties_200_with_unknown_event_type_mapped_to_duty(client):
|
||
from types import SimpleNamespace
|
||
|
||
fake_duty = SimpleNamespace(
|
||
id=1,
|
||
user_id=10,
|
||
start_at="2025-01-15T09:00:00Z",
|
||
end_at="2025-01-15T18:00:00Z",
|
||
event_type="unknown",
|
||
)
|
||
|
||
def fake_get_duties(session, from_date, to_date):
|
||
# get_duties returns (Duty, full_name, phone, username) tuples.
|
||
return [(fake_duty, "User A", "+79001234567", "user_a")]
|
||
|
||
with patch("duty_teller.api.dependencies.get_duties", side_effect=fake_get_duties):
|
||
r = client.get(
|
||
"/api/duties",
|
||
params={"from": "2025-01-01", "to": "2025-01-31"},
|
||
)
|
||
assert r.status_code == 200
|
||
data = r.json()
|
||
assert len(data) == 1
|
||
assert data[0]["event_type"] == "duty"
|
||
assert data[0]["full_name"] == "User A"
|
||
assert data[0].get("phone") == "+79001234567"
|
||
assert data[0].get("username") == "user_a"
|
||
|
||
|
||
def test_calendar_ical_team_404_invalid_token_format(client):
|
||
"""GET /api/calendar/ical/team/{token}.ics with invalid token format returns 404 without DB."""
|
||
r = client.get("/api/calendar/ical/team/short.ics")
|
||
assert r.status_code == 404
|
||
assert "not found" in r.text.lower()
|
||
|
||
|
||
@patch("duty_teller.api.app.get_user_by_calendar_token")
|
||
def test_calendar_ical_team_404_unknown_token(mock_get_user, client):
|
||
"""GET /api/calendar/ical/team/{token}.ics with unknown token returns 404."""
|
||
mock_get_user.return_value = None
|
||
valid_format_token = "B" * 43
|
||
r = client.get(f"/api/calendar/ical/team/{valid_format_token}.ics")
|
||
assert r.status_code == 404
|
||
assert "not found" in r.text.lower()
|
||
mock_get_user.assert_called_once()
|
||
|
||
|
||
@patch("duty_teller.api.app.build_team_ics")
|
||
@patch("duty_teller.api.app.get_duties")
|
||
@patch("duty_teller.api.app.get_user_by_calendar_token")
|
||
def test_calendar_ical_team_200_only_duty_and_description(
|
||
mock_get_user, mock_get_duties, mock_build_team_ics, client
|
||
):
|
||
"""GET /api/calendar/ical/team/{token}.ics returns ICS with duty-only events and DESCRIPTION."""
|
||
from types import SimpleNamespace
|
||
|
||
mock_user = SimpleNamespace(id=1, full_name="User A")
|
||
mock_get_user.return_value = mock_user
|
||
duty = SimpleNamespace(
|
||
id=10,
|
||
user_id=1,
|
||
start_at="2026-06-15T09:00:00Z",
|
||
end_at="2026-06-15T18:00:00Z",
|
||
event_type="duty",
|
||
)
|
||
non_duty = SimpleNamespace(
|
||
id=11,
|
||
user_id=2,
|
||
start_at="2026-06-16T09:00:00Z",
|
||
end_at="2026-06-16T18:00:00Z",
|
||
event_type="vacation",
|
||
)
|
||
# get_duties returns (Duty, full_name, phone, username) tuples.
|
||
mock_get_duties.return_value = [
|
||
(duty, "User A", None, None),
|
||
(non_duty, "User B", None, None),
|
||
]
|
||
mock_build_team_ics.return_value = b"BEGIN:VCALENDAR\r\nPRODID:Team\r\nVEVENT\r\nDESCRIPTION:User A\r\nEND:VCALENDAR"
|
||
token = "y" * 43
|
||
|
||
r = client.get(f"/api/calendar/ical/team/{token}.ics")
|
||
assert r.status_code == 200
|
||
assert r.headers.get("content-type", "").startswith("text/calendar")
|
||
assert b"BEGIN:VCALENDAR" in r.content
|
||
mock_get_user.assert_called_once()
|
||
mock_get_duties.assert_called_once()
|
||
# build_team_ics called with only duty (event_type duty), not vacation
|
||
mock_build_team_ics.assert_called_once()
|
||
duties_arg = mock_build_team_ics.call_args[0][0]
|
||
assert len(duties_arg) == 1
|
||
assert duties_arg[0][0].event_type == "duty"
|
||
assert duties_arg[0][1] == "User A"
|
||
|
||
|
||
def test_calendar_ical_404_invalid_token_format(client):
|
||
"""GET /api/calendar/ical/{token}.ics with invalid token format returns 404 without DB call."""
|
||
# Token format must be base64url, 40–50 chars; short or invalid chars → 404
|
||
r = client.get("/api/calendar/ical/short.ics")
|
||
assert r.status_code == 404
|
||
assert "not found" in r.text.lower()
|
||
r2 = client.get("/api/calendar/ical/" + "x" * 60 + ".ics")
|
||
assert r2.status_code == 404
|
||
r3 = client.get("/api/calendar/ical/../../../etc/passwd.ics")
|
||
assert r3.status_code == 404
|
||
|
||
|
||
@patch("duty_teller.api.app.get_user_by_calendar_token")
|
||
def test_calendar_ical_404_unknown_token(mock_get_user, client):
|
||
"""GET /api/calendar/ical/{token}.ics with unknown token returns 404."""
|
||
mock_get_user.return_value = None
|
||
# Use a token that passes format validation (base64url, 40–50 chars)
|
||
valid_format_token = "A" * 43
|
||
r = client.get(f"/api/calendar/ical/{valid_format_token}.ics")
|
||
assert r.status_code == 404
|
||
assert "not found" in r.text.lower()
|
||
mock_get_user.assert_called_once()
|
||
|
||
|
||
@patch("duty_teller.api.app.build_personal_ics")
|
||
@patch("duty_teller.api.app.get_duties_for_user")
|
||
@patch("duty_teller.api.app.get_user_by_calendar_token")
|
||
def test_calendar_ical_200_returns_only_that_users_duties(
|
||
mock_get_user, mock_get_duties, mock_build_ics, client
|
||
):
|
||
"""GET /api/calendar/ical/{token}.ics returns ICS with only the token owner's duties."""
|
||
from types import SimpleNamespace
|
||
|
||
mock_user = SimpleNamespace(id=1, full_name="User A")
|
||
mock_get_user.return_value = mock_user
|
||
duty = SimpleNamespace(
|
||
id=10,
|
||
user_id=1,
|
||
start_at="2026-06-15T09:00:00Z",
|
||
end_at="2026-06-15T18:00:00Z",
|
||
event_type="duty",
|
||
)
|
||
# get_duties_for_user returns (Duty, full_name, phone, username) tuples.
|
||
mock_get_duties.return_value = [(duty, "User A", None, None)]
|
||
mock_build_ics.return_value = (
|
||
b"BEGIN:VCALENDAR\r\nVEVENT\r\n2026-06-15\r\nEND:VCALENDAR"
|
||
)
|
||
# Token must pass format validation (base64url, 40–50 chars)
|
||
token = "x" * 43
|
||
|
||
r = client.get(f"/api/calendar/ical/{token}.ics")
|
||
assert r.status_code == 200
|
||
assert r.headers.get("content-type", "").startswith("text/calendar")
|
||
assert b"BEGIN:VCALENDAR" in r.content
|
||
mock_get_user.assert_called_once()
|
||
mock_get_duties.assert_called_once_with(
|
||
ANY, 1, from_date=ANY, to_date=ANY, event_types=["duty"]
|
||
)
|
||
mock_build_ics.assert_called_once()
|
||
# Only User A's duty was passed to build_personal_ics
|
||
duties_arg = mock_build_ics.call_args[0][0]
|
||
assert len(duties_arg) == 1
|
||
assert duties_arg[0][0].user_id == 1
|
||
assert duties_arg[0][1] == "User A"
|
||
|
||
|
||
@patch("duty_teller.api.app.build_personal_ics")
|
||
@patch("duty_teller.api.app.get_duties_for_user")
|
||
@patch("duty_teller.api.app.get_user_by_calendar_token")
|
||
def test_calendar_ical_ignores_unknown_query_params(
|
||
mock_get_user, mock_get_duties, mock_build_ics, client
|
||
):
|
||
"""Unknown query params (e.g. events=all) are ignored; response is duty-only."""
|
||
from types import SimpleNamespace
|
||
|
||
from duty_teller.cache import ics_calendar_cache
|
||
|
||
ics_calendar_cache.invalidate(("personal_ics", 1))
|
||
mock_user = SimpleNamespace(id=1, full_name="User A")
|
||
mock_get_user.return_value = mock_user
|
||
duty = SimpleNamespace(
|
||
id=10,
|
||
user_id=1,
|
||
start_at="2026-06-15T09:00:00Z",
|
||
end_at="2026-06-15T18:00:00Z",
|
||
event_type="duty",
|
||
)
|
||
# get_duties_for_user returns (Duty, full_name, phone, username) tuples.
|
||
mock_get_duties.return_value = [(duty, "User A", None, None)]
|
||
mock_build_ics.return_value = b"BEGIN:VCALENDAR\r\nEND:VCALENDAR"
|
||
token = "z" * 43
|
||
|
||
r = client.get(f"/api/calendar/ical/{token}.ics", params={"events": "all"})
|
||
assert r.status_code == 200
|
||
mock_get_duties.assert_called_once_with(
|
||
ANY, 1, from_date=ANY, to_date=ANY, event_types=["duty"]
|
||
)
|
||
|
||
|
||
# --- /api/calendar-events ---
|
||
|
||
|
||
@patch("duty_teller.api.dependencies.config.MINI_APP_SKIP_AUTH", False)
|
||
def test_calendar_events_403_without_init_data(client):
|
||
"""Without X-Telegram-Init-Data and without MINI_APP_SKIP_AUTH → 403."""
|
||
r = client.get(
|
||
"/api/calendar-events",
|
||
params={"from": "2025-01-01", "to": "2025-01-31"},
|
||
)
|
||
assert r.status_code == 403
|
||
|
||
|
||
@patch("duty_teller.api.app.config.EXTERNAL_CALENDAR_ICS_URL", "")
|
||
@patch("duty_teller.api.app.config.MINI_APP_SKIP_AUTH", True)
|
||
def test_calendar_events_empty_url_returns_empty_list(client):
|
||
"""When EXTERNAL_CALENDAR_ICS_URL is empty, GET /api/calendar-events returns [] without fetch."""
|
||
with patch("duty_teller.api.app.get_calendar_events") as mock_get:
|
||
r = client.get(
|
||
"/api/calendar-events",
|
||
params={"from": "2025-01-01", "to": "2025-01-31"},
|
||
)
|
||
assert r.status_code == 200
|
||
assert r.json() == []
|
||
mock_get.assert_not_called()
|
||
|
||
|
||
@patch(
|
||
"duty_teller.api.app.config.EXTERNAL_CALENDAR_ICS_URL",
|
||
"https://example.com/cal.ics",
|
||
)
|
||
@patch("duty_teller.api.app.config.MINI_APP_SKIP_AUTH", True)
|
||
def test_calendar_events_200_returns_list_with_date_summary(client):
|
||
"""GET /api/calendar-events with auth and URL set returns list of {date, summary}."""
|
||
with patch("duty_teller.api.app.get_calendar_events") as mock_get:
|
||
mock_get.return_value = [
|
||
{"date": "2025-01-15", "summary": "Team meeting"},
|
||
{"date": "2025-01-20", "summary": "Review"},
|
||
]
|
||
r = client.get(
|
||
"/api/calendar-events",
|
||
params={"from": "2025-01-01", "to": "2025-01-31"},
|
||
)
|
||
assert r.status_code == 200
|
||
data = r.json()
|
||
assert len(data) == 2
|
||
assert data[0]["date"] == "2025-01-15"
|
||
assert data[0]["summary"] == "Team meeting"
|
||
assert data[1]["date"] == "2025-01-20"
|
||
assert data[1]["summary"] == "Review"
|
||
mock_get.assert_called_once_with(
|
||
"https://example.com/cal.ics",
|
||
from_date="2025-01-01",
|
||
to_date="2025-01-31",
|
||
)
|