"""Catalog's service and queries, one use case each, below the HTTP layer. The routes are thin callers now, and their own tests stay the contract a client sees. These hold what each write does and leaves on its own: that it flushes and leaves the commit to its caller, that a row of someone else's reads as a missing one, and the rule each write carries (an explored channel flagged only while nobody follows it, a subscription that keeps it, a cleared search that spares what anyone holds). DB-backed (real Postgres). """ from datetime import datetime, timedelta, timezone from sqlalchemy import select from app.catalog import queries, search, service from app.catalog.models import ( BlockedChannel, Channel, ExploredChannel, SearchFind, Subscription, Video, ) from app.reader import queries as reader_queries from app.reader import service as reader_service def _channel(db, cid="UCc", **kw): ch = Channel(id=cid, title=kw.pop("title", cid), **kw) db.add(ch) db.commit() return ch def _video(db, vid, cid="UCc", **kw): v = Video(id=vid, channel_id=cid, published_at=datetime.now(timezone.utc), **kw) db.add(v) db.commit() return v def _sub(db, user, cid="UCc", **kw): s = Subscription(user_id=user.id, channel_id=cid, **kw) db.add(s) db.commit() return s def _uncommitted(db): return bool(db.new or db.dirty or db.deleted) or db.in_transaction() # --- whose a row is ------------------------------------------------------------------------ def test_a_users_subscriptions_blocks_and_explorations_are_theirs_alone(db, make_user): owner, stranger = make_user(), make_user() ch = _channel(db) _sub(db, owner) service.explore(db, owner, ch) service.block(db, owner, "UCc") db.commit() assert queries.subscription(db, owner, "UCc") is not None assert queries.subscription(db, stranger, "UCc") is None assert queries.exploration(db, stranger, "UCc") is None assert queries.is_blocked(db, stranger, "UCc") is False assert queries.blocked_ids(db, stranger) == set() assert [s.channel_id for s, _ in queries.subscriptions(db, stranger, "en")] == [] # --- subscription settings ------------------------------------------------------------------- def test_full_history_reports_only_the_switch_on(db, make_user): user = make_user() _channel(db) sub = _sub(db, user) assert service.update_subscription(db, sub, priority=3) is False assert service.update_subscription(db, sub, deep_requested=True) is True assert service.update_subscription(db, sub, deep_requested=True) is False # already on assert _uncommitted(db) db.commit() assert (sub.priority, sub.hidden, sub.deep_requested) == (3, False, True) def test_full_history_for_all_moves_only_the_callers_rows(db, make_user): user, other = make_user(), make_user() _channel(db) _sub(db, user) theirs = _sub(db, other) assert service.set_deep_all(db, user, True) == 1 db.commit() db.expire_all() assert theirs.deep_requested is False def test_a_reset_asks_every_subscriber_for_full_history(db, make_user): a, b = make_user(), make_user() ch = _channel(db, backfill_done=True, recent_synced_at=datetime.now(timezone.utc)) _sub(db, a) _sub(db, b) service.reset_backfill(db, ch) db.commit() db.expire_all() assert (ch.backfill_done, ch.recent_synced_at) == (False, None) assert set(db.scalars(select(Subscription.deep_requested))) == {True} # --- explore, subscribe, block --------------------------------------------------------------- def test_an_exploration_is_one_row_and_flags_the_channel_only_while_nobody_follows_it( db, make_user ): explorer, follower = make_user(), make_user() lone = _channel(db, "UClone") kept = _channel(db, "UCkept") _sub(db, follower, "UCkept") service.explore(db, explorer, lone) service.explore(db, explorer, kept) db.commit() first = queries.exploration(db, explorer, "UClone").created_at service.explore(db, explorer, lone) # refreshes the grace clock db.commit() assert db.scalar(select(ExploredChannel.id).where(ExploredChannel.channel_id == "UClone")) assert len(db.scalars(select(ExploredChannel.id)).all()) == 2 assert queries.exploration(db, explorer, "UClone").created_at >= first assert (lone.from_explore, kept.from_explore) == (True, False) def test_subscribing_keeps_an_explored_channel_and_its_videos(db, make_user): user = make_user() ch = _channel(db, from_explore=True) v = _video(db, "v1", via_explore=True) service.record_subscription(db, user, ch, "") db.commit() db.expire_all() sub = queries.subscription(db, user, "UCc") assert (sub.yt_subscription_id, ch.from_explore, v.via_explore) == (None, False, False) service.drop_subscription(db, sub) db.commit() assert queries.subscription(db, user, "UCc") is None def test_a_block_is_once_and_ends_the_exploration(db, make_user): user = make_user() ch = _channel(db) service.explore(db, user, ch) db.commit() service.block(db, user, "UCc") service.block(db, user, "UCc") db.commit() assert len(db.scalars(select(BlockedChannel.id)).all()) == 1 assert queries.exploration(db, user, "UCc") is None assert [c.id for c in queries.blocked_channels(db, user, "en")] == ["UCc"] service.unblock(db, user, "UCc") db.commit() assert queries.is_blocked(db, user, "UCc") is False # --- what the status cards count ------------------------------------------------------------- def test_my_progress_counts_only_my_channels_and_my_unhidden_videos(db, make_user): user, other = make_user(), make_user() now = datetime.now(timezone.utc) _channel(db, "UCa", details_synced_at=now, recent_synced_at=now, backfill_done=True) _channel(db, "UCb") _channel(db, "UCtheirs") _video(db, "a1", "UCa") _video(db, "b1", "UCb") _video(db, "t1", "UCtheirs") _sub(db, user, "UCa", deep_requested=True) _sub(db, user, "UCb", hidden=True) _sub(db, other, "UCtheirs") assert queries.my_progress(db, user) == (2, 1, 1, 1) assert [c.id for c in queries.my_deep_requested(db, user)] == ["UCa"] assert queries.my_video_count(db, user) == 1 assert queries.video_total(db) == 3 assert queries.catalog_totals(db, user)["subscriptions"] == 2 def test_a_channel_with_no_title_is_left_out_of_the_titles(db): _channel(db, "UCt", title="Titled") db.add(Channel(id="UCn", title=None)) db.commit() assert queries.channel_titles(db, {"UCt", "UCn", "UCgone"}) == {"UCt": "Titled"} assert queries.channel_title(db, "UCt") == "Titled" def test_the_deep_backlog_is_the_unfinished_channels_someone_asked_for(db, make_user): user = make_user() _channel(db, "UCwant") _channel(db, "UCdone", backfill_done=True) _channel(db, "UCnobody") _sub(db, user, "UCwant", deep_requested=True) _sub(db, user, "UCdone", deep_requested=True) _sub(db, user, "UCnobody") assert [c.id for c in queries.deep_backlog(db)] == ["UCwant"] assert queries.deep_queued(db, ["UCwant", "UCdone", "UCnobody"]) == {"UCwant", "UCdone"} # --- live search ----------------------------------------------------------------------------- def test_a_cleared_search_spares_what_anyone_holds(db, make_user): user, other, follower = make_user(), make_user(), make_user() _channel(db, "UCs") _channel(db, "UCfollowed") _sub(db, follower, "UCfollowed") for vid in ("loose", "watched", "found_by_other"): _video(db, vid, "UCs", via_search=True) _video(db, "followed", "UCfollowed", via_search=True) _video(db, "organic", "UCs") ids = ["loose", "watched", "found_by_other", "followed", "organic"] search.record_finds(db, user, ids, "cat") search.record_finds(db, user, ids, "cat") # idempotent search.record_finds(db, other, ["found_by_other"], "dog") reader_service.mark(db, other, "watched", "watched") db.commit() assert search.clear(db, user, ids) == 1 db.commit() left = set(db.scalars(select(Video.id))) assert left == {"watched", "found_by_other", "followed", "organic"} assert db.scalars(select(SearchFind.user_id)).all() == [other.id] def test_a_search_puts_what_the_user_already_has_last(db, make_user): user = make_user() _channel(db, "UCs") _channel(db, "UCmine") _sub(db, user, "UCmine") for vid, cid in (("new1", "UCs"), ("mine", "UCmine"), ("seen", "UCs"), ("new2", "UCs")): _video(db, vid, cid) reader_service.mark(db, user, "seen", "hidden") db.commit() assert search.new_first(db, user, ["mine", "new1", "seen", "new2"]) == [ "new1", "new2", "mine", "seen", ] # --- what Catalog asks Reader ---------------------------------------------------------------- def test_a_users_tag_links_are_made_once_and_only_theirs_are_read(db, make_user): user, other = make_user(), make_user() _channel(db) mine = reader_service.create_tag(db, user, "Mine", "other", None) theirs = reader_service.create_tag(db, other, "Theirs", "other", None) reader_service.attach_user_tag(db, user, "UCc", mine.id) reader_service.attach_user_tag(db, user, "UCc", mine.id) reader_service.attach_user_tag(db, other, "UCc", theirs.id) db.commit() assert reader_queries.user_tag_links(db, user) == {"UCc": [mine.id]} assert reader_queries.user_tag_links(db, user, "UCelse") == {} reader_service.detach_user_tag(db, user, "UCc", mine.id) db.commit() assert reader_queries.user_tag_links(db, user) == {} assert reader_queries.user_tag_links(db, other) == {"UCc": [theirs.id]} def test_discovery_offers_the_playlist_channels_not_followed_and_not_ones_own(db, make_user): user = make_user() for cid in ("UCnew", "UCfollowed", "UCown"): _channel(db, cid) for vid, cid in (("n1", "UCnew"), ("n2", "UCnew"), ("f1", "UCfollowed"), ("o1", "UCown")): _video(db, vid, cid) _sub(db, user, "UCfollowed") pl = reader_service.create_playlist(db, user, "L") for vid in ("n1", "n2", "f1", "o1"): reader_service.add_item(db, pl, vid, mark_dirty=False) db.commit() rows = reader_queries.playlist_channels(db, user, "UCown", "en") assert [(ch.id, n, lists) for ch, n, lists in rows] == [("UCnew", 2, 1)] def test_an_exploration_older_than_its_grace_is_still_one_row(db, make_user): """The grace clock is refreshed in place: re-exploring after the cutoff does not add a row.""" user = make_user() ch = _channel(db) db.add( ExploredChannel( user_id=user.id, channel_id="UCc", created_at=datetime.now(timezone.utc) - timedelta(days=30), ) ) db.commit() service.explore(db, user, ch) db.commit() rows = db.scalars(select(ExploredChannel)).all() assert len(rows) == 1 assert rows[0].created_at > datetime.now(timezone.utc) - timedelta(minutes=1)