""" Per-account blobs on the node — the playlist store (docs/playlists.md §3.3, §8.2). The node holds bytes it cannot read, one row per playlist plus a manifest, and hands them back to the account that wrote them. Three things have to hold, and only the first is obvious: - a blob round-trips through the process, byte for byte, as **bytes** — the column is a BLOB rather than base64 TEXT because these run to hundreds of kilobytes and base64 is a third of every write; - the plaintext is never in the file, which is the whole claim; and - every cap **refuses** rather than truncating. A truncating cap silently loses tracks, which is the failure the whole design exists to prevent. """ import pytest from meshbay_node.bundle_store import BundleStore @pytest.mark.asyncio async def test_a_blob_round_trips_as_bytes(tmp_path): store = BundleStore(db_path=tmp_path / "bundles.db") await store.open() # Every byte value, so a text column or an encoding step anywhere in the # path shows up as a difference rather than surviving by luck. sealed = bytes(range(256)) * 8 await store.store_user_blob("u1", "playlists", 3, sealed) row = await store.fetch_user_blob("u1", "playlists") assert row == {"rev": 3, "blob_enc": sealed} assert isinstance(row["blob_enc"], bytes) await store.close() @pytest.mark.asyncio async def test_a_blob_survives_the_process(tmp_path): """Reopened from disk, not read back out of the same connection.""" db = tmp_path / "bundles.db" sealed = b"\x00\x01sealed-body\xff" store = BundleStore(db_path=db) await store.open() await store.store_user_blob("u1", "playlist:abc-123", 41, sealed) await store.close() again = BundleStore(db_path=db) await again.open() assert (await again.fetch_user_blob("u1", "playlist:abc-123"))["blob_enc"] == sealed await again.close() @pytest.mark.asyncio async def test_one_playlist_is_one_row(tmp_path): """The point of the split: rewriting Favourites must not touch the rest.""" store = BundleStore(db_path=tmp_path / "bundles.db") await store.open() await store.store_user_blob("u1", "playlists", 1, b"manifest") await store.store_user_blob("u1", "playlist:favorites", 1, b"fav-v1") await store.store_user_blob("u1", "playlist:evening", 1, b"evening-v1") await store.store_user_blob("u1", "playlist:favorites", 2, b"fav-v2") assert (await store.fetch_user_blob("u1", "playlist:favorites"))["rev"] == 2 assert (await store.fetch_user_blob("u1", "playlist:evening"))["blob_enc"] == b"evening-v1" assert (await store.fetch_user_blob("u1", "playlists"))["blob_enc"] == b"manifest" await store.close() @pytest.mark.asyncio async def test_an_unwritten_kind_is_absent_not_an_error(tmp_path): """"No playlist here yet" is the ordinary state of a fresh node.""" store = BundleStore(db_path=tmp_path / "bundles.db") await store.open() assert await store.fetch_user_blob("u1", "playlists") is None assert await store.list_user_blobs("u1") == [] await store.close() @pytest.mark.asyncio async def test_listing_reports_kinds_and_revisions_and_no_payload(tmp_path): """What a client that lost its local state needs, and nothing more: the kinds carry client-generated UUIDs and cannot be guessed.""" store = BundleStore(db_path=tmp_path / "bundles.db") await store.open() await store.store_user_blob("u1", "playlists", 7, b"m") await store.store_user_blob("u1", "playlist:aaa", 2, b"secret-body") listing = await store.list_user_blobs("u1") assert listing == [{"kind": "playlist:aaa", "rev": 2}, {"kind": "playlists", "rev": 7}] assert "blob_enc" not in listing[0] await store.close() @pytest.mark.asyncio async def test_one_account_never_sees_another(tmp_path): """`user_id` comes from the authenticated session; this is what that buys.""" store = BundleStore(db_path=tmp_path / "bundles.db") await store.open() await store.store_user_blob("alice", "playlists", 1, b"alice") await store.store_user_blob("bob", "playlists", 1, b"bob") assert (await store.fetch_user_blob("alice", "playlists"))["blob_enc"] == b"alice" assert await store.list_user_blobs("bob") == [{"kind": "playlists", "rev": 1}] await store.delete_user_blob("alice", "playlists") assert await store.fetch_user_blob("alice", "playlists") is None assert await store.fetch_user_blob("bob", "playlists") is not None await store.close() @pytest.mark.asyncio async def test_deleting_something_absent_says_so_rather_than_raising(tmp_path): store = BundleStore(db_path=tmp_path / "bundles.db") await store.open() assert await store.delete_user_blob("u1", "playlist:gone") is False await store.store_user_blob("u1", "playlist:gone", 1, b"x") assert await store.delete_user_blob("u1", "playlist:gone") is True await store.close() @pytest.mark.asyncio async def test_the_account_total_is_what_the_cap_is_checked_against(tmp_path): """The per-blob caps bound one playlist; only this bounds the account, and an unbounded write primitive pointed at somebody else's disk needs it.""" store = BundleStore(db_path=tmp_path / "bundles.db") await store.open() assert await store.user_blob_total_bytes("u1") == 0 await store.store_user_blob("u1", "playlists", 1, b"x" * 100) await store.store_user_blob("u1", "playlist:a", 1, b"y" * 250) assert await store.user_blob_total_bytes("u1") == 350 # Replacing a blob replaces its contribution rather than adding to it. await store.store_user_blob("u1", "playlist:a", 2, b"y" * 50) assert await store.user_blob_total_bytes("u1") == 150 await store.delete_user_blob("u1", "playlist:a") assert await store.user_blob_total_bytes("u1") == 100 await store.close() @pytest.mark.asyncio async def test_the_plaintext_is_not_in_the_file(tmp_path): """The same check test_chat_key_storage.py makes for epoch keys, for the same reason: a plaintext column beside the sealed one is the obvious thing to write and would collapse the whole claim, silently.""" db = tmp_path / "bundles.db" store = BundleStore(db_path=db) await store.open() await store.store_user_blob( "u1", "playlist:abc", 1, b"SEALED-CIPHERTEXT-ONLY") await store.close() raw = db.read_bytes() assert b"SEALED-CIPHERTEXT-ONLY" in raw, ( "the sealed bytes should be there — this test is only meaningful if it " "is actually reading the right file") # What must never be: a track title, an artist, a path. The store is handed # ciphertext and stores exactly that; anything readable here would mean the # client sealed nothing or the node unwrapped it. for leak in (b"Un titre", b"tracks", b"artist", b"favorites"): assert leak not in raw, f"{leak!r} is in bundles.db in clear"