"""Comprehensive tests for LocalFileTransport — unit, integration, security, stress. Coverage matrix --------------- Unit _repo_root : valid URL → resolved path; bad scheme; missing .muse/ fetch_remote_info : reads repo.json + branch heads make_transport : file:// → LocalFileTransport; https:// → HttpTransport Integration (two real repos on disk) push from A → B via file:// using push_stream pull-equivalent: fetch_remote_info + fetch_stream from B after push round-trip: push A→B, verify B branch heads, then fetch B→A-mirror Stress push bundle with 50 commits and 200 objects via push_stream fetch 20 commits via fetch_stream """ from __future__ import annotations import base64 import datetime import hashlib import json import os import pathlib import pytest from muse._version import __version__ from muse.core.object_store import write_object from muse.core.pack import build_mpack from muse.core.snapshot import compute_commit_id, compute_snapshot_id from muse.core.store import ( CommitRecord, SnapshotRecord, get_all_branch_heads, get_head_commit_id, read_commit, write_commit, write_snapshot, ) from muse.core._types import Manifest, blob_id from muse.core.transport import ( HttpTransport, LocalFileTransport, TransportError, make_transport, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _sha(b: bytes) -> str: return blob_id(b) def _make_repo(path: pathlib.Path, branch: str = "main") -> pathlib.Path: """Create a minimal initialised Muse repo at *path*.""" muse = path / ".muse" (muse / "refs" / "heads").mkdir(parents=True) (muse / "objects").mkdir() (muse / "commits").mkdir() (muse / "snapshots").mkdir() (muse / "repo.json").write_text( json.dumps({"repo_id": f"repo-{path.name}", "schema_version": __version__, "domain": "midi", "default_branch": branch}) ) (muse / "HEAD").write_text(f"ref: refs/heads/{branch}\n") return path def _add_commit( root: pathlib.Path, label: str, branch: str = "main", parent: str | None = None, content: bytes = b"hello", ) -> str: """Write a commit with a real content-addressed ID and return it. *label* is used only to derive a unique message so that different calls with different labels produce different commit IDs even when all other inputs are the same. """ oid = _sha(content) write_object(root, oid, content) manifest: Manifest = {"file.txt": oid} snap_id = compute_snapshot_id(manifest) snap = SnapshotRecord(snapshot_id=snap_id, manifest=manifest) write_snapshot(root, snap) message = f"commit {label[:8]}" committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc) parent_ids = [parent] if parent else [] real_cid = compute_commit_id(parent_ids, snap_id, message, committed_at.isoformat()) commit = CommitRecord( commit_id=real_cid, repo_id=f"repo-{root.name}", branch=branch, snapshot_id=snap_id, message=message, committed_at=committed_at, parent_commit_id=parent, ) write_commit(root, commit) (root / ".muse" / "refs" / "heads" / branch).write_text(real_cid) return real_cid # --------------------------------------------------------------------------- # Unit — _repo_root # --------------------------------------------------------------------------- class TestRepoRoot: def test_valid_url_returns_resolved_path(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path / "myrepo") url = f"file://{repo}" result = LocalFileTransport._repo_root(url) assert result == repo.resolve() def test_invalid_scheme_raises_transport_error(self, tmp_path: pathlib.Path) -> None: with pytest.raises(TransportError, match="file://"): LocalFileTransport._repo_root("https://hub.example.com/repos/r1") def test_missing_muse_dir_raises_404(self, tmp_path: pathlib.Path) -> None: with pytest.raises(TransportError) as exc_info: LocalFileTransport._repo_root(f"file://{tmp_path}") assert exc_info.value.status_code == 404 assert ".muse/" in str(exc_info.value) def test_path_with_double_dots_normalized(self, tmp_path: pathlib.Path) -> None: """resolve() must collapse .. so the check is on the canonical path.""" repo = _make_repo(tmp_path / "repo") # Construct a URL with a harmless .. that stays inside the repo. url = f"file://{repo}/subdir/../" # The path resolves to the repo root — .muse/ exists there. result = LocalFileTransport._repo_root(url) assert result == repo.resolve() def test_symlink_target_with_no_muse_is_rejected(self, tmp_path: pathlib.Path) -> None: """A symlink that resolves to a dir without .muse/ must raise TransportError.""" target = tmp_path / "innocent" target.mkdir() link = tmp_path / "evil_link" link.symlink_to(target) with pytest.raises(TransportError) as exc_info: LocalFileTransport._repo_root(f"file://{link}") assert exc_info.value.status_code == 404 def test_symlink_to_valid_repo_is_accepted(self, tmp_path: pathlib.Path) -> None: """A symlink that resolves to a valid repo is accepted after resolve().""" repo = _make_repo(tmp_path / "real_repo") link = tmp_path / "alias" link.symlink_to(repo) result = LocalFileTransport._repo_root(f"file://{link}") # Should return the canonical (resolved) path, not the symlink. assert result == repo.resolve() # --------------------------------------------------------------------------- # Unit — fetch_remote_info # --------------------------------------------------------------------------- class TestFetchRemoteInfo: def test_reads_repo_json_and_branch_heads(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path / "remote") cid = _add_commit(repo, "a" * 64) t = LocalFileTransport() info = t.fetch_remote_info(f"file://{repo}", signing=None) assert info["repo_id"] == f"repo-{repo.name}" assert info["domain"] == "midi" assert info["default_branch"] == "main" assert info["branch_heads"]["main"] == cid def test_multiple_branches_returned(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path / "remote") cid_main = _add_commit(repo, "a" * 64, branch="main") cid_dev = _add_commit(repo, "b" * 64, branch="dev") t = LocalFileTransport() info = t.fetch_remote_info(f"file://{repo}", signing=None) assert info["branch_heads"]["main"] == cid_main assert info["branch_heads"]["dev"] == cid_dev def test_token_is_ignored(self, tmp_path: pathlib.Path) -> None: """LocalFileTransport ignores the token arg — no auth for local repos.""" repo = _make_repo(tmp_path / "remote") _add_commit(repo, "c" * 64) t = LocalFileTransport() info = t.fetch_remote_info(f"file://{repo}", signing="should-be-ignored") assert info["repo_id"] == f"repo-{repo.name}" def test_corrupted_repo_json_raises_transport_error(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path / "bad") (repo / ".muse" / "repo.json").write_text("NOT JSON") t = LocalFileTransport() with pytest.raises(TransportError, match="repo.json"): t.fetch_remote_info(f"file://{repo}", signing=None) # --------------------------------------------------------------------------- # Unit — fetch_pack # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Unit — push_pack # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Security — branch name and path traversal # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # make_transport factory # --------------------------------------------------------------------------- class TestMakeTransport: def test_file_url_returns_local_transport(self) -> None: assert isinstance(make_transport("file:///some/path"), LocalFileTransport) def test_https_url_returns_http_transport(self) -> None: assert isinstance(make_transport("https://hub.example.com/repos/r1"), HttpTransport) def test_http_url_returns_http_transport(self) -> None: assert isinstance(make_transport("http://hub.example.com/repos/r1"), HttpTransport) def test_empty_url_returns_http_transport(self) -> None: assert isinstance(make_transport(""), HttpTransport) # --------------------------------------------------------------------------- # Integration — full round-trip between two real repos # --------------------------------------------------------------------------- def _push( t: LocalFileTransport, url: str, local: pathlib.Path, commit_ids: list[str], branch: str, *, have: list[str] | None = None, force: bool = False, ) -> dict: """Helper: build_mpack → push_stream. Returns the PushResult dict.""" bundle = build_mpack(local, commit_ids=commit_ids, have=have or []) local_head = commit_ids[-1] if commit_ids else None return t.push_stream( url, None, objects=list(bundle.get("objects") or []), commits=list(bundle.get("commits") or []), snapshots=list(bundle.get("snapshots") or []), branch=branch, force=force, have=have or [], local_head=local_head, ) class TestIntegrationRoundTrip: def test_push_then_fetch_info(self, tmp_path: pathlib.Path) -> None: """Push from local → remote via push_stream; branch heads must reflect the push.""" local = _make_repo(tmp_path / "local") remote = _make_repo(tmp_path / "remote") cid = _add_commit(local, _sha(b"initial"), branch="main") t = LocalFileTransport() result = _push(t, f"file://{remote}", local, [cid], "main") assert result["ok"] is True info = t.fetch_remote_info(f"file://{remote}", None) assert info["branch_heads"]["main"] == cid def test_fetch_stream_after_push(self, tmp_path: pathlib.Path) -> None: """After pushing A→B, fetch_stream from B must return the same commit.""" src = _make_repo(tmp_path / "src") dst = _make_repo(tmp_path / "dst") cid = _add_commit(src, _sha(b"content"), branch="main") t = LocalFileTransport() _push(t, f"file://{dst}", src, [cid], "main") fetched = t.fetch_stream(f"file://{dst}", None, want=[cid], have=[]) fetched_ids = {c["commit_id"] for c in (fetched.get("commits") or [])} assert cid in fetched_ids def test_multi_branch_round_trip(self, tmp_path: pathlib.Path) -> None: """Push two branches via push_stream; remote must have both.""" local = _make_repo(tmp_path / "local") remote = _make_repo(tmp_path / "remote") cid_main = _add_commit(local, _sha(b"main-commit"), branch="main") cid_dev = _add_commit(local, _sha(b"dev-commit"), branch="dev") t = LocalFileTransport() url = f"file://{remote}" _push(t, url, local, [cid_main], "main") _push(t, url, local, [cid_dev], "dev") info = t.fetch_remote_info(url, None) assert info["branch_heads"]["main"] == cid_main assert info["branch_heads"]["dev"] == cid_dev def test_incremental_push_is_fast_forward(self, tmp_path: pathlib.Path) -> None: """Second push_stream whose parent is the remote tip must be accepted.""" local = _make_repo(tmp_path / "local") remote = _make_repo(tmp_path / "remote") cid1 = _add_commit(local, "commit-1", branch="main") t = LocalFileTransport() url = f"file://{remote}" _push(t, url, local, [cid1], "main") # Second commit with cid1 as parent. cid2 = _add_commit(local, "commit-2", branch="main", parent=cid1) result = _push(t, url, local, [cid2], "main", have=[cid1]) assert result["ok"] is True assert get_head_commit_id(remote, "main") == cid2 # --------------------------------------------------------------------------- # Stress — large bundle # --------------------------------------------------------------------------- class TestStress: def test_push_large_bundle(self, tmp_path: pathlib.Path) -> None: """Push a bundle with 50 commits and 200 distinct objects via push_stream.""" remote = _make_repo(tmp_path / "remote") local = _make_repo(tmp_path / "local") prev_cid: str | None = None last_cid = "" committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc) for i in range(50): # Write 4 objects per commit (200 total). manifest: Manifest = {} for j in range(4): blob = f"blob-{i}-{j}".encode() oid = _sha(blob) write_object(local, oid, blob) manifest[f"file_{i}_{j}.txt"] = oid snap_id = compute_snapshot_id(manifest) snap = SnapshotRecord(snapshot_id=snap_id, manifest=manifest) write_snapshot(local, snap) message = f"commit {i}" parent_ids = [prev_cid] if prev_cid else [] cid = compute_commit_id(parent_ids, snap_id, message, committed_at.isoformat()) commit = CommitRecord( commit_id=cid, repo_id=f"repo-{local.name}", branch="main", snapshot_id=snap_id, message=message, committed_at=committed_at, parent_commit_id=prev_cid, ) write_commit(local, commit) prev_cid = cid last_cid = cid (local / ".muse" / "refs" / "heads" / "main").write_text(last_cid) t = LocalFileTransport() result = _push(t, f"file://{remote}", local, [last_cid], "main") assert result["ok"] is True assert get_head_commit_id(remote, "main") == last_cid def test_fetch_stream_large_bundle(self, tmp_path: pathlib.Path) -> None: """Fetch from a remote with 20 commits via fetch_stream; verify all are returned.""" remote = _make_repo(tmp_path / "remote") all_cids: list[str] = [] prev: str | None = None for i in range(20): cid = _add_commit(remote, f"remote-commit-{i}", parent=prev) all_cids.append(cid) prev = cid last = all_cids[-1] result = LocalFileTransport().fetch_stream( f"file://{remote}", None, want=[last], have=[] ) fetched_ids = {c["commit_id"] for c in (result.get("commits") or [])} assert last in fetched_ids