Refactor project structure and enhance Docker configuration

- Updated `.dockerignore` to exclude test and development artifacts, optimizing the Docker image size.
- Refactored `main.py` to delegate execution to `duty_teller.run.main()`, simplifying the entry point.
- Introduced a new `duty_teller` package to encapsulate core functionality, improving modularity and organization.
- Enhanced `pyproject.toml` to define a script for running the application, streamlining the execution process.
- Updated README documentation to reflect changes in project structure and usage instructions.
- Improved Alembic environment configuration to utilize the new package structure for database migrations.
This commit is contained in:
2026-02-18 13:03:14 +03:00
parent 5331fac334
commit 28973489a5
42 changed files with 361 additions and 363 deletions

View File

@@ -6,9 +6,9 @@ from unittest.mock import ANY, patch
import pytest
from fastapi.testclient import TestClient
import config
from api.app import app
from tests.test_telegram_auth import _make_init_data
import duty_teller.config as config
from duty_teller.api.app import app
from tests.helpers import make_init_data
@pytest.fixture
@@ -28,11 +28,10 @@ def test_duties_from_after_to(client):
assert "from" in r.json()["detail"].lower() or "позже" in r.json()["detail"]
@patch("api.app._is_private_client")
@patch("api.app.config.MINI_APP_SKIP_AUTH", False)
@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):
"""Without initData and without private IP / skip-auth, should get 403."""
mock_private.return_value = False # simulate public client
mock_private.return_value = False
r = client.get(
"/api/duties",
params={"from": "2025-01-01", "to": "2025-01-31"},
@@ -40,8 +39,8 @@ def test_duties_403_without_init_data_from_public_client(mock_private, client):
assert r.status_code == 403
@patch("api.app.config.MINI_APP_SKIP_AUTH", True)
@patch("api.app._fetch_duties_response")
@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(
@@ -53,7 +52,7 @@ def test_duties_200_when_skip_auth(mock_fetch, client):
mock_fetch.assert_called_once_with(ANY, "2025-01-01", "2025-01-31")
@patch("api.app.validate_init_data_with_reason")
@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(
@@ -66,12 +65,12 @@ def test_duties_403_when_init_data_invalid(mock_validate, client):
assert "авторизации" in detail or "Неверные" in detail or "Неверная" in detail
@patch("api.app.validate_init_data_with_reason")
@patch("api.app.config.can_access_miniapp")
@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("api.app._fetch_duties_response") as mock_fetch:
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"},
@@ -82,12 +81,12 @@ def test_duties_403_when_username_not_allowed(mock_can_access, mock_validate, cl
mock_fetch.assert_not_called()
@patch("api.app.validate_init_data_with_reason")
@patch("api.app.config.can_access_miniapp")
@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("api.app._fetch_duties_response") as mock_fetch:
with patch("duty_teller.api.app.fetch_duties_response") as mock_fetch:
mock_fetch.return_value = [
{
"id": 1,
@@ -109,19 +108,18 @@ def test_duties_200_with_allowed_user(mock_can_access, mock_validate, client):
def test_duties_e2e_auth_real_validation(client, monkeypatch):
"""E2E: valid initData + allowlist, no mocks on validate_init_data_with_reason; full auth path."""
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(
init_data = make_init_data(
{"id": 1, "username": test_username},
test_token,
auth_date=int(time.time()),
)
with patch("api.app._fetch_duties_response") as mock_fetch:
with patch("duty_teller.api.app.fetch_duties_response") as mock_fetch:
mock_fetch.return_value = []
r = client.get(
"/api/duties",
@@ -133,9 +131,8 @@ def test_duties_e2e_auth_real_validation(client, monkeypatch):
mock_fetch.assert_called_once_with(ANY, "2025-01-01", "2025-01-31")
@patch("api.app.config.MINI_APP_SKIP_AUTH", True)
@patch("duty_teller.api.app.config.MINI_APP_SKIP_AUTH", True)
def test_duties_200_with_unknown_event_type_mapped_to_duty(client):
"""When DB returns duty with event_type not in (duty, unavailable, vacation), API returns 200 with event_type='duty'."""
from types import SimpleNamespace
fake_duty = SimpleNamespace(
@@ -149,7 +146,7 @@ def test_duties_200_with_unknown_event_type_mapped_to_duty(client):
def fake_get_duties(session, from_date, to_date):
return [(fake_duty, "User A")]
with patch("api.app.get_duties", side_effect=fake_get_duties):
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"},