"""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_duties_invalid_date_format(client): r = client.get("/api/duties", params={"from": "2025-01-01", "to": "invalid"}) assert r.status_code == 400 assert "YYYY-MM-DD" in r.json()["detail"] 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 assert "from" in r.json()["detail"].lower() or "позже" in r.json()["detail"] @patch("duty_teller.api.dependencies._is_private_client") @patch("duty_teller.api.dependencies.config.MINI_APP_SKIP_AUTH", False) def test_duties_403_without_init_data_from_public_client(mock_private, client): mock_private.return_value = False 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") def test_duties_403_when_init_data_invalid(mock_validate, client): mock_validate.return_value = (None, "hash_mismatch") 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 "авторизации" in detail or "Неверные" in detail or "Неверная" in detail @patch("duty_teller.api.dependencies.validate_init_data_with_reason") @patch("duty_teller.api.dependencies.config.can_access_miniapp") def test_duties_403_when_username_not_allowed(mock_can_access, mock_validate, client): mock_validate.return_value = ("someuser", "ok") 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 assert "Доступ запрещён" in r.json()["detail"] mock_fetch.assert_not_called() @patch("duty_teller.api.dependencies.validate_init_data_with_reason") @patch("duty_teller.api.dependencies.config.can_access_miniapp") def test_duties_200_with_allowed_user(mock_can_access, mock_validate, client): mock_validate.return_value = ("alloweduser", "ok") 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") def test_duties_e2e_auth_real_validation(client, monkeypatch): test_token = "123:ABC" test_username = "e2euser" monkeypatch.setattr(config, "BOT_TOKEN", test_token) monkeypatch.setattr(config, "ALLOWED_USERNAMES", {test_username}) 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()), ) 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): return [(fake_duty, "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"