"""Уведомления: сервис (create/list/mark_read/purge) и триггеры (инвайт, старт/финиш партии).""" from __future__ import annotations from datetime import datetime, timedelta, timezone from fastapi.testclient import TestClient from sqlmodel import Session from app.models import Notification, User from app.services import notification_service from tests.conftest import ( add_group_member, csrf_headers, finish_match, login, start_match, ) def _group(client: TestClient, name: str = "Группа") -> int: return client.post( "/api/groups", json={"name": name, "expansion_ids": []}, headers=csrf_headers(client) ).json()["id"] def _dev_user(client: TestClient, nick: str) -> None: client.post("/api/auth/dev/users", json={"nickname": nick}, headers=csrf_headers(client)) def _make_user(engine, nick: str) -> int: with Session(engine) as s: u = User(nickname=nick, role="player", auth_provider="stub") s.add(u) s.commit() s.refresh(u) return u.id # type: ignore[return-value] # ─── Сервис ──────────────────────────────────────────────────────────────────── def test_create_list_mark_read(engine): uid = _make_user(engine, "Алиса") with Session(engine) as s: notification_service.create(s, uid, type="test", title="Привет", link="/account") data = notification_service.list_for_user(s, uid) assert data["unread_count"] == 1 assert len(data["items"]) == 1 assert data["items"][0]["title"] == "Привет" assert data["items"][0]["link"] == "/account" assert data["items"][0]["read_at"] is None marked = notification_service.mark_read(s, uid) assert marked == 1 after = notification_service.list_for_user(s, uid) assert after["unread_count"] == 0 assert after["items"][0]["read_at"] is not None def test_purge_expired(engine): uid = _make_user(engine, "Боб") with Session(engine) as s: n = Notification(user_id=uid, type="test", title="Старое") s.add(n) s.commit() s.refresh(n) # Состарим запись за пределы окна хранения. n.created_at = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta( hours=notification_service.RETENTION_HOURS + 1 ) s.add(n) s.commit() removed = notification_service.purge_expired(s) assert removed == 1 assert notification_service.list_for_user(s, uid)["items"] == [] def test_notifications_require_auth(client: TestClient): assert client.get("/api/notifications").status_code == 401 # ─── Триггеры через HTTP ───────────────────────────────────────────────────────── def test_invitation_creates_notification(client: TestClient): login(client, "Овнер") gid = _group(client) _dev_user(client, "Гость") r = client.post( f"/api/groups/{gid}/invitations", json={"nickname": "Гость"}, headers=csrf_headers(client) ) assert r.status_code == 200, r.text login(client, "Гость") data = client.get("/api/notifications").json() invites = [n for n in data["items"] if n["type"] == "invite"] assert len(invites) == 1 assert invites[0]["link"] == "/group" assert "Группа" in invites[0]["title"] assert data["unread_count"] >= 1 def test_match_notifications_exclude_actor(client: TestClient, engine): me = login(client, "Капитан") gid = _group(client) member_id = add_group_member(engine, gid, "Боец") fids = [f["id"] for f in client.get(f"/api/groups/{gid}/factions").json()] started = start_match( client, gid, [ {"user_id": me["id"], "faction_id": fids[0]}, {"user_id": member_id, "faction_id": fids[1]}, ], ) assert started.status_code == 200, started.text mid = started.json()["id"] # Инициатор (Капитан) себе уведомление не шлёт. mine = client.get("/api/notifications").json() assert all(n["type"] != "match_started" for n in mine["items"]) # Участник получает уведомление о старте со ссылкой на партию. login(client, "Боец") his = client.get("/api/notifications").json() started_notifs = [n for n in his["items"] if n["type"] == "match_started"] assert len(started_notifs) == 1 assert started_notifs[0]["link"] == f"/match/{mid}" # Финиш (инициатор снова Капитан) → участник получает уведомление о завершении. login(client, "Капитан") fin = finish_match( client, mid, [{"user_id": me["id"], "place": 1}, {"user_id": member_id, "place": 2}], ) assert fin.status_code == 200, fin.text login(client, "Боец") his2 = client.get("/api/notifications").json() assert any( n["type"] == "match_finished" and n["link"] == f"/match/{mid}" for n in his2["items"] ) def test_mark_read_endpoint(client: TestClient): login(client, "Овнер2") gid = _group(client) _dev_user(client, "Гость2") client.post( f"/api/groups/{gid}/invitations", json={"nickname": "Гость2"}, headers=csrf_headers(client) ) login(client, "Гость2") assert client.get("/api/notifications").json()["unread_count"] >= 1 r = client.post("/api/notifications/read", json={}, headers=csrf_headers(client)) assert r.status_code == 200, r.text assert client.get("/api/notifications").json()["unread_count"] == 0