- Store subscriber count from YouTube statistics and show it on channel page - Sync 50 videos per channel with playlistItems pagination support - Show per-channel and per-category new-videos counters (2-day window) - Replace hourly videos sync with activity trigger (2h idle) and incremental backfill (hard cap 200 per channel) - Clicking the sidebar new-videos count filters the category feed to recent videos only (new_only) - Update agent-team docs: deploy after green checks
197 lines
7.8 KiB
Python
197 lines
7.8 KiB
Python
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
|