# retoor import json import pytest from devplacepy.services.nodes.adapters.base import AdapterError, clean, tag_for from devplacepy.services.nodes.adapters.direct import DevTo, GitHub, HackerNews, Mastodon, StackExchange from devplacepy.services.nodes.adapters.reddit import Reddit from devplacepy.services.nodes.adapters.registry import ADAPTERS from devplacepy.services.nodes.spec import SPEC def body(value) -> bytes: return json.dumps(value).encode() def test_clean_strips_tags_entities_and_caps(): assert clean("

a & b

c", 50) == "a & b c" assert len(clean("x" * 900, 100)) == 100 assert clean(None, 10) == "" def test_tag_for_picks_longest_word(): assert tag_for("sqlite wal mode") == "sqlite" assert tag_for("!!") == "" def test_hackernews_parse(): data = { "hits": [ {"objectID": "7", "title": "T", "points": 5, "num_comments": 2, "author": "a", "created_at": "2026"}, {"objectID": "", "title": "no id"}, "junk", ] } hits = HackerNews().parse(body(data), 8) assert [item.url for item in hits] == ["https://news.ycombinator.com/item?id=7"] assert hits[0].score == 5 and hits[0].via == "direct" def test_stackexchange_parse(): data = {"items": [{"title": "Q & A", "link": "https://stackoverflow.com/q/1", "score": 3, "answer_count": 2, "tags": ["c"], "owner": {"display_name": "bob"}}]} hit = StackExchange().parse(body(data), 8)[0] assert hit.title == "Q & A" and hit.author == "bob" and hit.comments == 2 def test_github_parse_requires_http_url(): data = {"items": [{"title": "I", "html_url": "javascript:alert(1)"}, {"title": "J", "html_url": "https://github.com/a/b/issues/1", "user": {"login": "u"}}]} hits = GitHub().parse(body(data), 8) assert [item.title for item in hits] == ["J"] def test_devto_and_mastodon_need_a_tag(): with pytest.raises(AdapterError) as error: DevTo().request("!!", 5) assert error.value.code == "no_tag" with pytest.raises(AdapterError): Mastodon().request("", 5) assert "tag=sqlite" in DevTo().request("sqlite", 5) assert "/tag/sqlite" in Mastodon().request("sqlite", 5) def test_mastodon_parse_strips_html(): data = [{"url": "https://m.example/1", "content": "

hello world

", "account": {"acct": "x@y"}, "favourites_count": 2, "reblogs_count": 1}] hit = Mastodon().parse(body(data), 5)[0] assert hit.snippet == "hello world" and hit.score == 3 def test_reddit_parse_and_via(): data = {"data": {"children": [{"data": {"title": "T", "permalink": "/r/x/comments/1/t/", "score": 9, "num_comments": 4, "author": "a", "selftext": "body"}}, {"data": {"title": "bad", "permalink": "http://evil"}}]}} hits = Reddit().parse(body(data), 5) assert len(hits) == 1 and hits[0].url == "https://www.reddit.com/r/x/comments/1/t/" and hits[0].via == "fetch" @pytest.mark.parametrize("adapter", list(ADAPTERS.values())) def test_forged_structure_is_rejected(adapter): for forged in (b"not json", body("string"), body({"unexpected": 1}), body(42)): with pytest.raises(AdapterError): adapter.parse(forged, 5) def test_oversize_body_is_rejected(): with pytest.raises(AdapterError) as error: HackerNews().parse(b" " * (2 * 1024 * 1024 + 1), 5) assert error.value.code == "body_too_large" def test_node_adapters_are_exactly_the_spec_adapters(): node_backed = {name for name, adapter in ADAPTERS.items() if not adapter.direct} assert node_backed == {item.adapter for item in SPEC.destinations}