From 5d0bd8125bec08c7ec92d50d6c59832af9946fd7 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 2 Jul 2026 20:18:53 +0200 Subject: [PATCH] test(capabilities): cover youtube verb schemas, executors, and registry --- .../youtube/comments/test_executor.py | 55 ++++++++++++ .../youtube/comments/test_schemas.py | 35 ++++++++ .../youtube/scrape/test_executor.py | 87 +++++++++++++++++++ .../youtube/scrape/test_schemas.py | 40 +++++++++ .../capabilities/youtube/test_registry.py | 32 +++++++ 5 files changed, 249 insertions(+) create mode 100644 surfsense_backend/tests/unit/capabilities/youtube/comments/test_executor.py create mode 100644 surfsense_backend/tests/unit/capabilities/youtube/comments/test_schemas.py create mode 100644 surfsense_backend/tests/unit/capabilities/youtube/scrape/test_executor.py create mode 100644 surfsense_backend/tests/unit/capabilities/youtube/scrape/test_schemas.py create mode 100644 surfsense_backend/tests/unit/capabilities/youtube/test_registry.py diff --git a/surfsense_backend/tests/unit/capabilities/youtube/comments/test_executor.py b/surfsense_backend/tests/unit/capabilities/youtube/comments/test_executor.py new file mode 100644 index 000000000..94fe941ad --- /dev/null +++ b/surfsense_backend/tests/unit/capabilities/youtube/comments/test_executor.py @@ -0,0 +1,55 @@ +"""``youtube.comments`` executor: verb input → actor input mapping → typed items.""" + +from __future__ import annotations + +import pytest + +from app.capabilities.youtube.comments.executor import build_comments_executor +from app.capabilities.youtube.comments.schemas import CommentsInput, CommentsOutput +from app.proprietary.scrapers.youtube import YouTubeCommentsInput + +pytestmark = pytest.mark.unit + + +class _FakeScraper: + def __init__(self, items: list[dict]): + self._items = items + self.calls: list[YouTubeCommentsInput] = [] + + async def __call__(self, actor_input: YouTubeCommentsInput) -> list[dict]: + self.calls.append(actor_input) + return self._items + + +async def test_maps_urls_and_wraps_comment_items(): + scraper = _FakeScraper([{"cid": "c1", "comment": "nice", "author": "@a"}]) + execute = build_comments_executor(scrape_fn=scraper) + + out = await execute(CommentsInput(urls=["https://www.youtube.com/watch?v=abc"])) + + assert isinstance(out, CommentsOutput) + assert len(out.items) == 1 + assert out.items[0].cid == "c1" + assert out.items[0].comment == "nice" + + (actor_input,) = scraper.calls + assert [u.url for u in actor_input.startUrls] == [ + "https://www.youtube.com/watch?v=abc" + ] + + +async def test_forwards_max_comments_and_sort(): + scraper = _FakeScraper([]) + execute = build_comments_executor(scrape_fn=scraper) + + await execute( + CommentsInput( + urls=["https://youtu.be/abc"], + max_comments=50, + sort_by="TOP_COMMENTS", + ) + ) + + (actor_input,) = scraper.calls + assert actor_input.maxComments == 50 + assert actor_input.sortCommentsBy == "TOP_COMMENTS" diff --git a/surfsense_backend/tests/unit/capabilities/youtube/comments/test_schemas.py b/surfsense_backend/tests/unit/capabilities/youtube/comments/test_schemas.py new file mode 100644 index 000000000..129473f7b --- /dev/null +++ b/surfsense_backend/tests/unit/capabilities/youtube/comments/test_schemas.py @@ -0,0 +1,35 @@ +"""``youtube.comments`` input guards: URLs required and batch/count bounded.""" + +from __future__ import annotations + +import pytest +from pydantic import ValidationError + +from app.capabilities.youtube.comments.schemas import ( + MAX_COMMENT_VIDEOS, + CommentsInput, +) + +pytestmark = pytest.mark.unit + + +def test_rejects_empty_url_batch(): + with pytest.raises(ValidationError): + CommentsInput(urls=[]) + + +def test_rejects_batch_over_the_cap(): + too_many = [f"https://youtu.be/{i}" for i in range(MAX_COMMENT_VIDEOS + 1)] + with pytest.raises(ValidationError): + CommentsInput(urls=too_many) + + +def test_defaults_max_comments_and_newest_first(): + payload = CommentsInput(urls=["https://youtu.be/abc"]) + assert payload.max_comments == 20 + assert payload.sort_by == "NEWEST_FIRST" + + +def test_rejects_zero_max_comments(): + with pytest.raises(ValidationError): + CommentsInput(urls=["https://youtu.be/abc"], max_comments=0) diff --git a/surfsense_backend/tests/unit/capabilities/youtube/scrape/test_executor.py b/surfsense_backend/tests/unit/capabilities/youtube/scrape/test_executor.py new file mode 100644 index 000000000..08f15981b --- /dev/null +++ b/surfsense_backend/tests/unit/capabilities/youtube/scrape/test_executor.py @@ -0,0 +1,87 @@ +"""``youtube.scrape`` executor: verb input → actor input mapping → typed items. + +Boundary mocked: the proprietary scraper (injected fake). NOT mocked: the verb's +own payload→YouTubeScrapeInput mapping and the dict→VideoItem wrapping. +""" + +from __future__ import annotations + +import pytest + +from app.capabilities.youtube.scrape.executor import build_scrape_executor +from app.capabilities.youtube.scrape.schemas import ScrapeInput, ScrapeOutput +from app.proprietary.scrapers.youtube import YouTubeScrapeInput + +pytestmark = pytest.mark.unit + + +class _FakeScraper: + """Records the actor input it was called with and returns canned items.""" + + def __init__(self, items: list[dict]): + self._items = items + self.calls: list[YouTubeScrapeInput] = [] + + async def __call__(self, actor_input: YouTubeScrapeInput) -> list[dict]: + self.calls.append(actor_input) + return self._items + + +async def test_maps_urls_to_start_urls_and_wraps_items(): + scraper = _FakeScraper([{"id": "abc", "title": "Hello"}]) + execute = build_scrape_executor(scrape_fn=scraper) + + out = await execute(ScrapeInput(urls=["https://www.youtube.com/watch?v=abc"])) + + assert isinstance(out, ScrapeOutput) + assert len(out.items) == 1 + assert out.items[0].id == "abc" + assert out.items[0].title == "Hello" + + (actor_input,) = scraper.calls + assert [u.url for u in actor_input.startUrls] == [ + "https://www.youtube.com/watch?v=abc" + ] + assert actor_input.searchQueries == [] + + +async def test_forwards_search_queries(): + scraper = _FakeScraper([]) + execute = build_scrape_executor(scrape_fn=scraper) + + await execute(ScrapeInput(search_queries=["python", "rust"])) + + (actor_input,) = scraper.calls + assert actor_input.searchQueries == ["python", "rust"] + assert actor_input.startUrls == [] + + +async def test_max_results_caps_every_content_type(): + scraper = _FakeScraper([]) + execute = build_scrape_executor(scrape_fn=scraper) + + await execute(ScrapeInput(search_queries=["x"], max_results=7)) + + (actor_input,) = scraper.calls + # Videos, shorts, and streams must all inherit the caller's cap, otherwise a + # channel scrape would silently return only plain videos (actor default 0). + assert actor_input.maxResults == 7 + assert actor_input.maxResultsShorts == 7 + assert actor_input.maxResultStreams == 7 + + +async def test_forwards_subtitle_options(): + scraper = _FakeScraper([]) + execute = build_scrape_executor(scrape_fn=scraper) + + await execute( + ScrapeInput( + urls=["https://youtu.be/abc"], + download_subtitles=True, + subtitles_language="fr", + ) + ) + + (actor_input,) = scraper.calls + assert actor_input.downloadSubtitles is True + assert actor_input.subtitlesLanguage == "fr" diff --git a/surfsense_backend/tests/unit/capabilities/youtube/scrape/test_schemas.py b/surfsense_backend/tests/unit/capabilities/youtube/scrape/test_schemas.py new file mode 100644 index 000000000..9d0477dfe --- /dev/null +++ b/surfsense_backend/tests/unit/capabilities/youtube/scrape/test_schemas.py @@ -0,0 +1,40 @@ +"""``youtube.scrape`` input guards: a source is required and the batch is bounded.""" + +from __future__ import annotations + +import pytest +from pydantic import ValidationError + +from app.capabilities.youtube.scrape.schemas import ( + MAX_YOUTUBE_SOURCES, + ScrapeInput, +) + +pytestmark = pytest.mark.unit + + +def test_rejects_input_with_neither_urls_nor_queries(): + with pytest.raises(ValidationError): + ScrapeInput() + + +def test_accepts_urls_only(): + payload = ScrapeInput(urls=["https://www.youtube.com/watch?v=abc"]) + assert payload.search_queries == [] + + +def test_accepts_search_queries_only(): + payload = ScrapeInput(search_queries=["python tutorial"]) + assert payload.urls == [] + + +def test_max_results_defaults_and_is_bounded(): + assert ScrapeInput(search_queries=["x"]).max_results == 10 + with pytest.raises(ValidationError): + ScrapeInput(search_queries=["x"], max_results=0) + + +def test_rejects_more_sources_than_the_cap(): + too_many = [f"https://youtu.be/{i}" for i in range(MAX_YOUTUBE_SOURCES + 1)] + with pytest.raises(ValidationError): + ScrapeInput(urls=too_many) diff --git a/surfsense_backend/tests/unit/capabilities/youtube/test_registry.py b/surfsense_backend/tests/unit/capabilities/youtube/test_registry.py new file mode 100644 index 000000000..756b65176 --- /dev/null +++ b/surfsense_backend/tests/unit/capabilities/youtube/test_registry.py @@ -0,0 +1,32 @@ +"""The youtube namespace registers each verb as one Capability the doors/agent read.""" + +from __future__ import annotations + +import pytest + +from app.capabilities import ( + youtube, # noqa: F401 — importing the namespace registers its verbs +) +from app.capabilities.core.store import get_capability +from app.capabilities.youtube.comments.schemas import CommentsInput, CommentsOutput +from app.capabilities.youtube.scrape.schemas import ScrapeInput, ScrapeOutput + +pytestmark = pytest.mark.unit + + +def test_youtube_scrape_is_registered_and_free(): + cap = get_capability("youtube.scrape") + + assert cap.name == "youtube.scrape" + assert cap.input_schema is ScrapeInput + assert cap.output_schema is ScrapeOutput + assert cap.billing_unit is None + + +def test_youtube_comments_is_registered_and_free(): + cap = get_capability("youtube.comments") + + assert cap.name == "youtube.comments" + assert cap.input_schema is CommentsInput + assert cap.output_schema is CommentsOutput + assert cap.billing_unit is None