chore: add coverage reporting and improve documentation
All checks were successful
CI / lint-and-test (push) Successful in 19s

- Added `pytest-cov` as a development dependency for coverage reporting.
- Configured pytest to include coverage options, ensuring code coverage is reported and enforced.
- Updated the README to include contributing guidelines and logging policies, enhancing clarity for developers.
- Added a new section in the configuration documentation emphasizing the necessity of serving the application over HTTPS in production for security purposes.
- Introduced a new `.coverage` file to track test coverage metrics.
This commit is contained in:
2026-02-20 16:18:59 +03:00
parent 86f6d66865
commit 0ecbda67f9
11 changed files with 101 additions and 11 deletions

View File

@@ -1,6 +1,7 @@
"""FastAPI app: /api/duties, /api/calendar-events, personal ICS, and static webapp at /app."""
import logging
import re
from datetime import date, timedelta
import duty_teller.config as config
@@ -23,6 +24,15 @@ from duty_teller.db.schemas import CalendarEvent, DutyWithUser
log = logging.getLogger(__name__)
# Calendar tokens are secrets.token_urlsafe(32) → base64url, length 43
_CALENDAR_TOKEN_RE = re.compile(r"^[A-Za-z0-9_-]{40,50}$")
def _is_valid_calendar_token(token: str) -> bool:
"""Return True if token matches expected format (length and alphabet). Rejects invalid before DB."""
return bool(token and _CALENDAR_TOKEN_RE.match(token))
app = FastAPI(title="Duty Teller API")
app.add_middleware(
CORSMiddleware,
@@ -37,7 +47,10 @@ app.add_middleware(
"/api/duties",
response_model=list[DutyWithUser],
summary="List duties",
description="Returns duties for the given date range. Requires Telegram Mini App initData (or MINI_APP_SKIP_AUTH / private IP in dev).",
description=(
"Returns duties for the given date range. Requires Telegram Mini App initData "
"(or MINI_APP_SKIP_AUTH / private IP in dev)."
),
)
def list_duties(
request: Request,
@@ -57,7 +70,10 @@ def list_duties(
"/api/calendar-events",
response_model=list[CalendarEvent],
summary="List calendar events",
description="Returns calendar events for the date range, including external ICS when EXTERNAL_CALENDAR_ICS_URL is set. Auth same as /api/duties.",
description=(
"Returns calendar events for the date range, including external ICS when "
"EXTERNAL_CALENDAR_ICS_URL is set. Auth same as /api/duties."
),
)
def list_calendar_events(
dates: tuple[str, str] = Depends(get_validated_dates),
@@ -74,7 +90,10 @@ def list_calendar_events(
@app.get(
"/api/calendar/ical/{token}.ics",
summary="Personal calendar ICS",
description="Returns an ICS calendar with only the subscribing user's duties. No Telegram auth; access is by secret token in the URL.",
description=(
"Returns an ICS calendar with only the subscribing user's duties. "
"No Telegram auth; access is by secret token in the URL."
),
)
def get_personal_calendar_ical(
token: str,
@@ -84,6 +103,8 @@ def get_personal_calendar_ical(
Return ICS calendar with only the subscribing user's duties.
No Telegram auth; access is by secret token in the URL.
"""
if not _is_valid_calendar_token(token):
return Response(status_code=404, content="Not found")
user = get_user_by_calendar_token(session, token)
if user is None:
return Response(status_code=404, content="Not found")

View File

@@ -105,10 +105,22 @@ def require_miniapp_username(
def _is_private_client(client_host: str | None) -> bool:
"""Return True if client_host is localhost or RFC 1918 private IPv4.
Used to allow /api/duties without initData when opened from local/private
network (e.g. dev). IPv4 only; IPv6 only 127/::1 checked.
Args:
client_host: Client IP or hostname from request.
Returns:
True if loopback or 10.x, 172.1631.x, 192.168.x.x.
"""
if not client_host:
return False
if client_host in ("127.0.0.1", "::1"):
return True
# RFC 1918 private ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
parts = client_host.split(".")
if len(parts) == 4:
try: