"""Tests for indexer and GroupIndex.""" import asyncio import os import time import pytest from pathlib import Path from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey from meshbay_common.crypto import generate_gek from meshbay_node.indexer import DirectoryIndexer, GroupIndex, IndexCache import meshbay_node.indexer.indexer as indexer_mod from conftest import one_root from meshbay_node.keystore import NodeKeys @pytest.fixture def sk_node(): return Ed25519PrivateKey.generate() @pytest.fixture def gek(): return generate_gek() @pytest.fixture def shared_dir(tmp_path): d = tmp_path / "shared" d.mkdir() (d / "video.mkv").write_bytes(os.urandom(1024)) # Above MIN_AUDIO_SIZE_BYTES: these tests use this file as a generic # "here's an audio entry" stand-in, not as a test of the tiny-file gate # itself (see test_is_indexable_size_* / test_initial_scan_skips_a_ # corrupt_tiny_audio_file below for that). (d / "music.mp3").write_bytes(os.urandom(indexer_mod.MIN_AUDIO_SIZE_BYTES + 1024)) (d / "readme.md").write_bytes(b"# Hello MeshBay") subdir = d / "docs" subdir.mkdir() (subdir / "manual.pdf").write_bytes(os.urandom(2048)) return d # ── GroupIndex tests ────────────────────────────────────────────────────────── def test_group_index_serialize_deserialize_private(sk_node, gek, shared_dir): idx = GroupIndex(group_id="grp-001", sk_node=sk_node, gek=gek) from meshbay_common.protocol import IndexEntry idx.add_entry(IndexEntry( id="abc123", name="video.mkv", path="", size=1024, type="video", added_at=int(time.time()), duration=120)) wire = idx.serialize() recovered = GroupIndex.deserialize(wire, sk_node=sk_node, gek=gek) assert recovered.group_id == "grp-001" assert recovered.count == 1 assert recovered.entries[0].name == "video.mkv" assert recovered.entries[0].type == "video" def test_group_index_serialize_deserialize_public(sk_node): idx = GroupIndex(group_id="pub-001", sk_node=sk_node, gek=None) from meshbay_common.protocol import IndexEntry idx.add_entry(IndexEntry( id="xyz789", name="readme.txt", path="", size=42, type="document", added_at=int(time.time()))) wire = idx.serialize() recovered = GroupIndex.deserialize(wire, sk_node=sk_node, gek=None) assert recovered.count == 1 assert recovered.entries[0].id == "xyz789" def test_group_index_wrong_gek_rejected(sk_node, gek): idx = GroupIndex(group_id="grp-002", sk_node=sk_node, gek=gek) from meshbay_common.protocol import IndexEntry idx.add_entry(IndexEntry(id="a", name="f.mp3", path="", size=1, type="audio", added_at=0)) wire = idx.serialize() wrong_gek = generate_gek() with pytest.raises(Exception): # InvalidTag from AEAD GroupIndex.deserialize(wire, sk_node=sk_node, gek=wrong_gek) def test_group_index_tampered_rejected(sk_node, gek): idx = GroupIndex(group_id="grp-003", sk_node=sk_node, gek=gek) from meshbay_common.protocol import IndexEntry idx.add_entry(IndexEntry(id="b", name="f.mp4", path="", size=1, type="video", added_at=0)) wire = bytearray(idx.serialize()) wire[-5] ^= 0xFF # flip bytes at the end with pytest.raises(Exception): GroupIndex.deserialize(bytes(wire), sk_node=sk_node, gek=gek) def test_group_index_diff(sk_node, gek): from meshbay_common.protocol import IndexEntry v1 = GroupIndex(group_id="g", sk_node=sk_node, gek=gek, version=1) v1.add_entry(IndexEntry(id="aaa", name="a.mp4", path="", size=1, type="video", added_at=0)) v1.add_entry(IndexEntry(id="bbb", name="b.mp3", path="", size=1, type="audio", added_at=0)) v2 = GroupIndex(group_id="g", sk_node=sk_node, gek=gek, version=2) v2.add_entry(IndexEntry(id="aaa", name="a.mp4", path="", size=1, type="video", added_at=0)) v2.add_entry(IndexEntry(id="ccc", name="c.mkv", path="", size=1, type="video", added_at=0)) delta = v2.diff(v1) assert delta.base_version == 1 assert delta.version == 2 assert len(delta.additions) == 1 assert delta.additions[0].id == "ccc" assert "bbb" in delta.deletions # ── DirectoryIndexer tests ──────────────────────────────────────────────────── @pytest.mark.asyncio async def test_initial_scan(shared_dir, sk_node, gek): indexer = DirectoryIndexer( roots=one_root(shared_dir), group_id="scan-test", sk_node=sk_node, gek=gek, ) await indexer.initial_scan() entries = indexer.index.entries names = {e.name for e in entries} assert "video.mkv" in names assert "music.mp3" in names assert "readme.md" in names assert "manual.pdf" in names assert indexer.index.count == 4 @pytest.mark.asyncio async def test_type_detection(shared_dir, sk_node, gek): indexer = DirectoryIndexer(roots=one_root(shared_dir), group_id="g", sk_node=sk_node, gek=gek) await indexer.initial_scan() by_name = {e.name: e.type for e in indexer.index.entries} assert by_name["video.mkv"] == "video" assert by_name["music.mp3"] == "audio" assert by_name["readme.md"] == "document" assert by_name["manual.pdf"] == "document" def test_type_detection_covers_wma_and_musepack(): """ Both formats are real audio a real library can contain, even though neither one plays natively in a browser