Retro Bulletin Board Systems on atproto. Web app and TUI. lazy mirror of alyraffauf/atbbs atbbs.xyz
forums python tui atproto bbs
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

add python tests for core logic

+297 -2
+3
justfile
··· 6 6 tui: 7 7 uv run python -m tui 8 8 9 + test *args: 10 + uv run pytest {{ args }} 11 + 9 12 fmt: 10 13 uv format 11 14 cd web && npx --yes prettier --write src/
+5 -1
pyproject.toml
··· 28 28 "data/shared.json" = "core/_shared.json" 29 29 30 30 [dependency-groups] 31 - dev = [] 31 + dev = ["pytest>=8.0"] 32 + 33 + [tool.pytest.ini_options] 34 + testpaths = ["tests"] 35 + pythonpath = ["."]
+48
tests/test_filters.py
··· 1 + import pytest 2 + 3 + from core.filters import filter_moderated 4 + from core.models import Record 5 + 6 + 7 + def _record(did: str, rkey: str) -> Record: 8 + uri = f"at://{did}/xyz.atbbs.post/{rkey}" 9 + return Record(uri=uri, cid="bafy", value={}) 10 + 11 + 12 + def test_filter_drops_banned_dids(): 13 + records = [_record("did:plc:alice", "1"), _record("did:plc:eve", "2")] 14 + out = filter_moderated(records, banned_dids={"did:plc:eve"}, hidden_posts=set()) 15 + assert [r.uri for r in out] == ["at://did:plc:alice/xyz.atbbs.post/1"] 16 + 17 + 18 + def test_filter_drops_hidden_posts(): 19 + a = _record("did:plc:alice", "1") 20 + b = _record("did:plc:alice", "2") 21 + out = filter_moderated([a, b], banned_dids=set(), hidden_posts={b.uri}) 22 + assert out == [a] 23 + 24 + 25 + def test_filter_combines_both_rules(): 26 + a = _record("did:plc:alice", "1") 27 + b = _record("did:plc:alice", "hidden") 28 + c = _record("did:plc:eve", "3") 29 + out = filter_moderated( 30 + [a, b, c], banned_dids={"did:plc:eve"}, hidden_posts={b.uri} 31 + ) 32 + assert out == [a] 33 + 34 + 35 + def test_filter_no_rules_passes_through(): 36 + records = [_record("did:plc:alice", "1"), _record("did:plc:bob", "2")] 37 + assert filter_moderated(records, banned_dids=set(), hidden_posts=set()) == records 38 + 39 + 40 + def test_filter_empty_records(): 41 + assert filter_moderated([], banned_dids={"did:plc:eve"}, hidden_posts=set()) == [] 42 + 43 + 44 + def test_filter_propagates_malformed_uri(): 45 + """Malformed records are a programmer error upstream — fail loud, don't silently drop.""" 46 + bad = Record(uri="not-an-at-uri", cid="bafy", value={}) 47 + with pytest.raises(ValueError): 48 + filter_moderated([bad], banned_dids=set(), hidden_posts=set())
+103
tests/test_models.py
··· 1 + import pytest 2 + 3 + from core.models import AtUri, BacklinkRef, MiniDoc, Post, make_at_uri 4 + 5 + 6 + URI = "at://did:plc:abc/xyz.atbbs.post/3lkw1" 7 + 8 + 9 + def test_aturi_parse_fields(): 10 + parsed = AtUri.parse(URI) 11 + assert parsed.did == "did:plc:abc" 12 + assert parsed.collection == "xyz.atbbs.post" 13 + assert parsed.rkey == "3lkw1" 14 + 15 + 16 + def test_aturi_str_from_constructor(): 17 + assert str(AtUri("did:plc:abc", "xyz.atbbs.post", "3lkw1")) == URI 18 + 19 + 20 + def test_make_at_uri(): 21 + assert make_at_uri("did:plc:abc", "xyz.atbbs.post", "3lkw1") == URI 22 + 23 + 24 + def test_make_at_uri_roundtrips_through_parse(): 25 + uri = make_at_uri("did:plc:abc", "xyz.atbbs.post", "3lkw1") 26 + parsed = AtUri.parse(uri) 27 + assert parsed.did == "did:plc:abc" 28 + assert parsed.collection == "xyz.atbbs.post" 29 + assert parsed.rkey == "3lkw1" 30 + 31 + 32 + def test_aturi_str_roundtrip(): 33 + assert str(AtUri.parse(URI)) == URI 34 + 35 + 36 + def test_aturi_eq_with_string(): 37 + assert AtUri.parse(URI) == URI 38 + 39 + 40 + def test_aturi_eq_with_string_reverse(): 41 + assert URI == AtUri.parse(URI) 42 + 43 + 44 + def test_aturi_eq_with_aturi(): 45 + assert AtUri.parse(URI) == AtUri.parse(URI) 46 + 47 + 48 + @pytest.mark.parametrize("other", [42, None, [], {}, 3.14, object()]) 49 + def test_aturi_eq_other_type_is_false(other): 50 + assert (AtUri.parse(URI) == other) is False 51 + 52 + 53 + def test_aturi_hash_consistent_with_equality(): 54 + a = AtUri.parse(URI) 55 + b = AtUri.parse(URI) 56 + assert a == b 57 + assert hash(a) == hash(b) 58 + 59 + 60 + def test_aturi_hash_set_membership(): 61 + seen = {AtUri.parse(URI)} 62 + assert AtUri.parse(URI) in seen 63 + 64 + 65 + @pytest.mark.parametrize( 66 + "bad_uri", 67 + [ 68 + "https://example.com/x/y/z", 69 + "did:plc:abc/xyz.atbbs.post/3lkw1", 70 + "at://did:plc:abc/xyz.atbbs.post", 71 + "at://did:plc:abc/xyz.atbbs.post/3lkw1/extra", 72 + "at://did:plc:abc//3lkw1", 73 + "at:///xyz.atbbs.post/3lkw1", 74 + "", 75 + ], 76 + ) 77 + def test_aturi_parse_rejects_malformed(bad_uri): 78 + with pytest.raises(ValueError): 79 + AtUri.parse(bad_uri) 80 + 81 + 82 + def test_backlinkref_uri(): 83 + ref = BacklinkRef(did="did:plc:abc", collection="xyz.atbbs.post", rkey="3lkw1") 84 + assert ref.uri == URI 85 + 86 + 87 + def _post(root=None): 88 + return Post( 89 + uri=URI, 90 + scope="board", 91 + body="hi", 92 + created_at="2024-01-15T12:30:00+00:00", 93 + author=MiniDoc(did="did:plc:abc", handle="alice.test"), 94 + root=root, 95 + ) 96 + 97 + 98 + def test_post_is_root_when_no_root(): 99 + assert _post(root=None).is_root is True 100 + 101 + 102 + def test_post_is_root_false_when_root_set(): 103 + assert _post(root="at://did:plc:abc/xyz.atbbs.post/parent").is_root is False
+89
tests/test_util.py
··· 1 + import os 2 + import time 3 + from datetime import datetime 4 + 5 + import pytest 6 + 7 + from core.util import ( 8 + attachment_cid, 9 + blob_url, 10 + format_datetime_local, 11 + format_datetime_utc, 12 + now_iso, 13 + ) 14 + 15 + 16 + @pytest.fixture 17 + def tz(): 18 + """Set the process timezone for a test, then restore.""" 19 + original = os.environ.get("TZ") 20 + 21 + def _set(name: str): 22 + os.environ["TZ"] = name 23 + time.tzset() 24 + 25 + yield _set 26 + if original is None: 27 + os.environ.pop("TZ", None) 28 + else: 29 + os.environ["TZ"] = original 30 + time.tzset() 31 + 32 + 33 + def test_now_iso_is_parseable_utc(): 34 + value = now_iso() 35 + dt = datetime.fromisoformat(value) 36 + assert dt.utcoffset().total_seconds() == 0 37 + 38 + 39 + def test_now_iso_uses_z_suffix(): 40 + value = now_iso() 41 + assert value.endswith("Z") 42 + assert "+00:00" not in value 43 + 44 + 45 + def test_format_datetime_utc_passthrough(): 46 + assert format_datetime_utc("2024-01-15T12:30:00+00:00") == "2024-01-15 12:30 UTC" 47 + 48 + 49 + def test_format_datetime_utc_converts_offset_to_utc(): 50 + # 17:30 +05:00 == 12:30 UTC 51 + assert format_datetime_utc("2024-01-15T17:30:00+05:00") == "2024-01-15 12:30 UTC" 52 + 53 + 54 + def test_format_datetime_utc_converts_negative_offset(): 55 + # 07:30 -05:00 == 12:30 UTC 56 + assert format_datetime_utc("2024-01-15T07:30:00-05:00") == "2024-01-15 12:30 UTC" 57 + 58 + 59 + def test_format_datetime_local_converts_to_local(tz): 60 + tz("America/New_York") 61 + # 17:30 UTC in January = 12:30 EST (UTC-5) 62 + assert format_datetime_local("2024-01-15T17:30:00+00:00") == "2024-01-15 12:30" 63 + 64 + 65 + def test_blob_url(): 66 + url = blob_url("https://pds.example", "did:plc:abc", "bafy123") 67 + assert url == "https://pds.example/xrpc/com.atproto.sync.getBlob?did=did:plc:abc&cid=bafy123" 68 + 69 + 70 + def test_blob_url_strips_trailing_slash(): 71 + url = blob_url("https://pds.example/", "did:plc:abc", "bafy123") 72 + assert url == "https://pds.example/xrpc/com.atproto.sync.getBlob?did=did:plc:abc&cid=bafy123" 73 + 74 + 75 + @pytest.mark.parametrize( 76 + "attachment,expected", 77 + [ 78 + ({"file": {"ref": {"$link": "bafy123"}}}, "bafy123"), 79 + ({}, ""), 80 + ({"file": None}, ""), 81 + ({"file": {}}, ""), 82 + ({"file": {"ref": None}}, ""), 83 + ({"file": {"ref": {}}}, ""), 84 + ({"file": {"ref": {"$link": None}}}, ""), 85 + ], 86 + ids=["present", "missing_file", "null_file", "missing_ref", "null_ref", "missing_link", "null_link"], 87 + ) 88 + def test_attachment_cid(attachment, expected): 89 + assert attachment_cid(attachment) == expected
+49 -1
uv.lock
··· 100 100 { name = "textual" }, 101 101 ] 102 102 103 + [package.dev-dependencies] 104 + dev = [ 105 + { name = "pytest" }, 106 + ] 107 + 103 108 [package.metadata] 104 109 requires-dist = [ 105 110 { name = "aiohttp", specifier = ">=3.13.5" }, ··· 112 117 ] 113 118 114 119 [package.metadata.requires-dev] 115 - dev = [] 120 + dev = [{ name = "pytest", specifier = ">=8.0" }] 116 121 117 122 [[package]] 118 123 name = "attrs" ··· 339 344 ] 340 345 341 346 [[package]] 347 + name = "iniconfig" 348 + version = "2.3.0" 349 + source = { registry = "https://pypi.org/simple" } 350 + sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } 351 + wheels = [ 352 + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, 353 + ] 354 + 355 + [[package]] 342 356 name = "linkify-it-py" 343 357 version = "2.1.0" 344 358 source = { registry = "https://pypi.org/simple" } ··· 431 445 { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, 432 446 { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, 433 447 { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, 448 + ] 449 + 450 + [[package]] 451 + name = "packaging" 452 + version = "26.2" 453 + source = { registry = "https://pypi.org/simple" } 454 + sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } 455 + wheels = [ 456 + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, 434 457 ] 435 458 436 459 [[package]] ··· 452 475 ] 453 476 454 477 [[package]] 478 + name = "pluggy" 479 + version = "1.6.0" 480 + source = { registry = "https://pypi.org/simple" } 481 + sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } 482 + wheels = [ 483 + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, 484 + ] 485 + 486 + [[package]] 455 487 name = "propcache" 456 488 version = "0.4.1" 457 489 source = { registry = "https://pypi.org/simple" } ··· 506 538 sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } 507 539 wheels = [ 508 540 { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, 541 + ] 542 + 543 + [[package]] 544 + name = "pytest" 545 + version = "9.0.3" 546 + source = { registry = "https://pypi.org/simple" } 547 + dependencies = [ 548 + { name = "colorama", marker = "sys_platform == 'win32'" }, 549 + { name = "iniconfig" }, 550 + { name = "packaging" }, 551 + { name = "pluggy" }, 552 + { name = "pygments" }, 553 + ] 554 + sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } 555 + wheels = [ 556 + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, 509 557 ] 510 558 511 559 [[package]]