import pytest from datetime import datetime, timedelta, timezone from fastapi.testclient import TestClient from app.core.auth_dependency import require_session from app.db import get_db from app.main import app from app.models.channel import Channel from app.models.video import Video @pytest.fixture def client(db_session): def _get_db_override(): yield db_session app.dependency_overrides[get_db] = _get_db_override app.dependency_overrides[require_session] = lambda: None # Deliberately not using `with TestClient(app)`: that runs the app's # lifespan, which would try to reach the real MeTube instance and DB. yield TestClient(app) del app.dependency_overrides[get_db] del app.dependency_overrides[require_session] def _create_channel(db_session, youtube_channel_id="chanA", title="Channel A"): channel = Channel(youtube_channel_id=youtube_channel_id, title=title, subscribed=True) db_session.add(channel) db_session.commit() db_session.refresh(channel) return channel def _seed_video(db_session, channel, video_id, published_at): video = Video( youtube_video_id=video_id, channel_id=channel.id, title=f"Video {video_id}", published_at=published_at, youtube_url=f"https://www.youtube.com/watch?v={video_id}", ) db_session.add(video) db_session.commit() return video def test_category_crud(client): created = client.post("/api/categories", json={"name": "Linux"}).json() assert created["name"] == "Linux" assert created["slug"] == "linux" assert created["channel_count"] == 0 listed = client.get("/api/categories").json() assert len(listed) == 1 updated = client.patch(f"/api/categories/{created['id']}", json={"name": "Linux/BSD"}).json() assert updated["name"] == "Linux/BSD" resp = client.delete(f"/api/categories/{created['id']}") assert resp.status_code == 204 assert client.get("/api/categories").json() == [] def test_category_name_must_be_unique(client): client.post("/api/categories", json={"name": "Шахматы"}) resp = client.post("/api/categories", json={"name": "шахматы"}) assert resp.status_code == 409 def test_cyrillic_slug_transliterated(client): created = client.post("/api/categories", json={"name": "Научпоп"}).json() assert created["slug"] == "nauchpop" def test_delete_category_does_not_delete_channel(client, db_session): channel = _create_channel(db_session) category = client.post("/api/categories", json={"name": "IT"}).json() client.put(f"/api/channels/{channel.id}/categories", json={"category_ids": [category["id"]]}) client.delete(f"/api/categories/{category['id']}") resp = client.get(f"/api/channels/{channel.id}").json() assert resp["id"] == channel.id assert resp["category_ids"] == [] def test_channel_can_belong_to_multiple_categories(client, db_session): channel = _create_channel(db_session) cat1 = client.post("/api/categories", json={"name": "Linux"}).json() cat2 = client.post("/api/categories", json={"name": "IT"}).json() resp = client.put( f"/api/channels/{channel.id}/categories", json={"category_ids": [cat1["id"], cat2["id"]]}, ).json() assert sorted(resp["category_ids"]) == sorted([cat1["id"], cat2["id"]]) def test_assign_unknown_category_rejected(client, db_session): channel = _create_channel(db_session) resp = client.put(f"/api/channels/{channel.id}/categories", json={"category_ids": [9999]}) assert resp.status_code == 400 def test_channels_filter_by_category_and_uncategorized(client, db_session): channel_a = _create_channel(db_session, "chanA", "Channel A") channel_b = _create_channel(db_session, "chanB", "Channel B") category = client.post("/api/categories", json={"name": "Linux"}).json() client.put(f"/api/channels/{channel_a.id}/categories", json={"category_ids": [category["id"]]}) in_category = client.get(f"/api/channels?category_id={category['id']}").json() assert [c["id"] for c in in_category] == [channel_a.id] uncategorized = client.get("/api/channels?uncategorized=true").json() assert [c["id"] for c in uncategorized] == [channel_b.id] def test_reorder_categories(client): cat1 = client.post("/api/categories", json={"name": "A"}).json() cat2 = client.post("/api/categories", json={"name": "B"}).json() cat3 = client.post("/api/categories", json={"name": "C"}).json() reordered = client.post( "/api/categories/reorder", json={"category_ids": [cat3["id"], cat1["id"], cat2["id"]]}, ).json() assert [c["id"] for c in reordered] == [cat3["id"], cat1["id"], cat2["id"]] def test_reorder_rejects_mismatched_ids(client): client.post("/api/categories", json={"name": "A"}) resp = client.post("/api/categories/reorder", json={"category_ids": [9999]}) assert resp.status_code == 400 def test_category_new_videos_count_sums_recent_videos_of_subscribed_channels(client, db_session): now = datetime.now(timezone.utc) category = client.post("/api/categories", json={"name": "Linux"}).json() assert category["new_videos_count"] == 0 channel_a = _create_channel(db_session, "chanA", "Channel A") channel_b = _create_channel(db_session, "chanB", "Channel B") channel_c = _create_channel(db_session, "chanC", "Channel C") channel_c.subscribed = False db_session.commit() client.put(f"/api/channels/{channel_a.id}/categories", json={"category_ids": [category["id"]]}) client.put(f"/api/channels/{channel_b.id}/categories", json={"category_ids": [category["id"]]}) client.put(f"/api/channels/{channel_c.id}/categories", json={"category_ids": [category["id"]]}) _seed_video(db_session, channel_a, "vidARecent1", now - timedelta(days=1)) _seed_video(db_session, channel_a, "vidARecent2", now - timedelta(hours=2)) _seed_video(db_session, channel_a, "vidAOld", now - timedelta(days=30)) _seed_video(db_session, channel_b, "vidBRecent", now - timedelta(days=1)) # Unsubscribed channel: its videos must not count towards the category. _seed_video(db_session, channel_c, "vidCUnsub", now - timedelta(days=1)) listed = client.get("/api/categories").json() assert len(listed) == 1 assert listed[0]["channel_count"] == 3 assert listed[0]["new_videos_count"] == 3 def test_category_new_videos_count_excludes_other_categories(client, db_session): now = datetime.now(timezone.utc) cat1 = client.post("/api/categories", json={"name": "Linux"}).json() cat2 = client.post("/api/categories", json={"name": "IT"}).json() channel_a = _create_channel(db_session, "chanA", "Channel A") channel_b = _create_channel(db_session, "chanB", "Channel B") client.put(f"/api/channels/{channel_a.id}/categories", json={"category_ids": [cat1["id"]]}) client.put(f"/api/channels/{channel_b.id}/categories", json={"category_ids": [cat2["id"]]}) # Only an old video in cat1, a recent one in cat2: counts must not leak. _seed_video(db_session, channel_a, "vidAOld", now - timedelta(days=30)) _seed_video(db_session, channel_b, "vidBRecent", now - timedelta(days=1)) listed = {c["id"]: c for c in client.get("/api/categories").json()} assert listed[cat1["id"]]["new_videos_count"] == 0 assert listed[cat2["id"]]["new_videos_count"] == 1 def test_category_new_videos_count_zero_without_recent_videos(client, db_session): now = datetime.now(timezone.utc) category = client.post("/api/categories", json={"name": "Linux"}).json() channel = _create_channel(db_session) client.put(f"/api/channels/{channel.id}/categories", json={"category_ids": [category["id"]]}) _seed_video(db_session, channel, "vidOld", now - timedelta(days=30)) listed = client.get("/api/categories").json() assert len(listed) == 1 assert listed[0]["channel_count"] == 1 assert listed[0]["new_videos_count"] == 0