"""Tests for checklist 2.4 — Object / commit integrity. Covers: - SHA-256 content-addressed object verification on push and push/objects - Forged parent_id rejection at receive time """ from __future__ import annotations import hashlib import uuid from datetime import datetime, timezone def _uuid4() -> str: return uuid.uuid4().hex[:8] import msgpack import pytest from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from tests.factories import create_repo as factory_create_repo from musehub.types.json_types import JSONObject, StrDict # ── helpers ──────────────────────────────────────────────────────────────────── def _utc_now() -> str: return datetime.now(tz=timezone.utc).isoformat() def _sha256_object_id(content: bytes) -> str: return "sha256:" + hashlib.sha256(content).hexdigest() def _mp(data: JSONObject) -> bytes: return msgpack.packb(data, use_bin_type=True) def _sha256_id(seed: str) -> str: return "sha256:" + hashlib.sha256(seed.encode()).hexdigest() def _make_commit( commit_id: str | None = None, parent: str | None = None, snap_id: str | None = None, ) -> JSONObject: uid = uuid.uuid4().hex return { "commit_id": commit_id or _sha256_id(f"commit-{uid}"), "branch": "main", "snapshot_id": snap_id or _sha256_id(f"snap-{uid}"), "message": "test commit", "committed_at": _utc_now(), "parent_commit_id": parent, "author": "Test User ", } def _make_valid_object(content: bytes) -> JSONObject: """Object whose object_id is the correct sha256 of content.""" return { "object_id": _sha256_object_id(content), "content": content, "path": "file.bin", } def _make_tampered_object(content: bytes) -> JSONObject: """Object whose object_id claims sha256 of DIFFERENT content.""" wrong_content = content + b"\x00" # flip one byte return { "object_id": _sha256_object_id(wrong_content), # hash of wrong_content "content": content, # but we send content "path": "file.bin", } def _make_non_sha256_object(content: bytes) -> JSONObject: """Object_id without sha256: prefix — should be accepted without hash check.""" return { "object_id": "blob:" + uuid.uuid4().hex, "content": content, "path": "file.bin", } # ── SHA-256 object verification: /push endpoint ──────────────────────────────── async def test_push_with_valid_sha256_object_succeeds( client: AsyncClient, db_session: AsyncSession, wire_headers: StrDict, ) -> None: """An object whose sha256(content) matches object_id must be accepted.""" repo = await factory_create_repo( db_session, slug=f"integrity-valid-sha-{_uuid4()}", owner="test-user-wire" ) content = b"hello, content-addressed world" obj = _make_valid_object(content) commit = _make_commit() resp = await client.post( f"/{repo.owner}/{repo.slug}/push", content=_mp({ "bundle": {"commits": [commit], "snapshots": [], "objects": [obj]}, "branch": "main", }), headers=wire_headers, ) assert resp.status_code == 200 async def test_push_with_tampered_sha256_object_returns_422( client: AsyncClient, db_session: AsyncSession, wire_headers: StrDict, ) -> None: """An object whose sha256(content) does NOT match object_id must be rejected with 422.""" repo = await factory_create_repo( db_session, slug=f"integrity-tampered-sha-{_uuid4()}", owner="test-user-wire" ) content = b"legit content" obj = _make_tampered_object(content) commit = _make_commit() resp = await client.post( f"/{repo.owner}/{repo.slug}/push", content=_mp({ "bundle": {"commits": [commit], "snapshots": [], "objects": [obj]}, "branch": "main", }), headers=wire_headers, ) assert resp.status_code == 422 assert "mismatch" in resp.text.lower() async def test_push_with_non_sha256_object_id_is_rejected( client: AsyncClient, db_session: AsyncSession, wire_headers: StrDict, ) -> None: """Objects without sha256: prefix are rejected — the wire schema requires it.""" repo = await factory_create_repo( db_session, slug=f"integrity-non-sha256-{_uuid4()}", owner="test-user-wire" ) obj = _make_non_sha256_object(b"some bytes") commit = _make_commit() resp = await client.post( f"/{repo.owner}/{repo.slug}/push", content=_mp({ "bundle": {"commits": [commit], "snapshots": [], "objects": [obj]}, "branch": "main", }), headers=wire_headers, ) assert resp.status_code == 422 # ── SHA-256 object verification: /push/objects endpoint ─────────────────────── async def test_push_objects_with_valid_sha256_succeeds( client: AsyncClient, db_session: AsyncSession, wire_headers: StrDict, ) -> None: """push/objects with a valid sha256 object_id must return 200.""" repo = await factory_create_repo( db_session, slug=f"integrity-objects-valid-{_uuid4()}", owner="test-user-wire" ) content = b"chunked upload content" obj = _make_valid_object(content) resp = await client.post( f"/{repo.owner}/{repo.slug}/push/objects", content=_mp({"objects": [obj]}), headers=wire_headers, ) assert resp.status_code == 200 async def test_push_objects_with_tampered_sha256_returns_422( client: AsyncClient, db_session: AsyncSession, wire_headers: StrDict, ) -> None: """push/objects with a tampered sha256 object_id must return 422.""" repo = await factory_create_repo( db_session, slug=f"integrity-objects-tampered-{_uuid4()}", owner="test-user-wire" ) content = b"attacker-supplied content" obj = _make_tampered_object(content) resp = await client.post( f"/{repo.owner}/{repo.slug}/push/objects", content=_mp({"objects": [obj]}), headers=wire_headers, ) assert resp.status_code == 422 assert "mismatch" in resp.text.lower() # ── Parent commit integrity ──────────────────────────────────────────────────── async def test_push_commit_with_parent_in_bundle_succeeds( client: AsyncClient, db_session: AsyncSession, wire_headers: StrDict, ) -> None: """A commit whose parent_commit_id is in the same push bundle must be accepted.""" repo = await factory_create_repo( db_session, slug=f"integrity-parent-in-bundle-{_uuid4()}", owner="test-user-wire" ) parent_id = _sha256_id(f"parent-{uuid.uuid4().hex}") child_id = _sha256_id(f"child-{uuid.uuid4().hex}") parent = _make_commit(commit_id=parent_id) child = _make_commit(commit_id=child_id, parent=parent_id) resp = await client.post( f"/{repo.owner}/{repo.slug}/push", content=_mp({ "bundle": {"commits": [parent, child], "snapshots": [], "objects": []}, "branch": "main", }), headers=wire_headers, ) assert resp.status_code == 200 async def test_push_commit_with_parent_in_db_succeeds( client: AsyncClient, db_session: AsyncSession, wire_headers: StrDict, ) -> None: """A commit whose parent is already in the DB for this repo must be accepted.""" repo = await factory_create_repo( db_session, slug=f"integrity-parent-in-db-{_uuid4()}", owner="test-user-wire" ) parent_id = _sha256_id(f"parent-{uuid.uuid4().hex}") child_id = _sha256_id(f"child-{uuid.uuid4().hex}") parent = _make_commit(commit_id=parent_id) # Push parent first r1 = await client.post( f"/{repo.owner}/{repo.slug}/push", content=_mp({ "bundle": {"commits": [parent], "snapshots": [], "objects": []}, "branch": "main", }), headers=wire_headers, ) assert r1.status_code == 200 # Push child referencing parent (already in DB) child = _make_commit(commit_id=child_id, parent=parent_id) r2 = await client.post( f"/{repo.owner}/{repo.slug}/push", content=_mp({ "bundle": {"commits": [child], "snapshots": [], "objects": []}, "branch": "main", }), headers=wire_headers, ) assert r2.status_code == 200 async def test_push_commit_with_forged_parent_id_is_rejected( client: AsyncClient, db_session: AsyncSession, wire_headers: StrDict, ) -> None: """A commit referencing a parent that exists in neither the bundle nor this repo's DB must be rejected (forged history reference).""" repo = await factory_create_repo( db_session, slug=f"integrity-forged-parent-{_uuid4()}", owner="test-user-wire" ) forged_parent_id = _sha256_id(f"forged-{uuid.uuid4().hex}") # does not exist anywhere child_id = _sha256_id(f"child-{uuid.uuid4().hex}") child = _make_commit(commit_id=child_id, parent=forged_parent_id) resp = await client.post( f"/{repo.owner}/{repo.slug}/push", content=_mp({ "bundle": {"commits": [child], "snapshots": [], "objects": []}, "branch": "main", }), headers=wire_headers, ) # Should be rejected — either 409 (push rejected) or 422 assert resp.status_code in (409, 422) assert "parent" in resp.text.lower() or "rejected" in resp.text.lower() async def test_push_commit_with_parent_from_different_repo_is_rejected( client: AsyncClient, db_session: AsyncSession, wire_headers: StrDict, ) -> None: """A commit whose parent_id exists in a DIFFERENT repo must be rejected (cross-repo forgery).""" repo_a = await factory_create_repo( db_session, slug=f"integrity-repo-a-{_uuid4()}", owner="test-user-wire" ) repo_b = await factory_create_repo( db_session, slug=f"integrity-repo-b-{_uuid4()}", owner="test-user-wire" ) # Push a commit to repo_a commit_in_a_id = _sha256_id(f"commit-a-{uuid.uuid4().hex}") commit_in_a = _make_commit(commit_id=commit_in_a_id) r1 = await client.post( f"/{repo_a.owner}/{repo_a.slug}/push", content=_mp({ "bundle": {"commits": [commit_in_a], "snapshots": [], "objects": []}, "branch": "main", }), headers=wire_headers, ) assert r1.status_code == 200 # Now push to repo_b claiming a parent that only exists in repo_a child_id = _sha256_id(f"child-b-{uuid.uuid4().hex}") child = _make_commit(commit_id=child_id, parent=commit_in_a_id) r2 = await client.post( f"/{repo_b.owner}/{repo_b.slug}/push", content=_mp({ "bundle": {"commits": [child], "snapshots": [], "objects": []}, "branch": "main", }), headers=wire_headers, ) assert r2.status_code in (409, 422) assert "parent" in r2.text.lower() or "rejected" in r2.text.lower() async def test_push_root_commit_with_no_parent_succeeds( client: AsyncClient, db_session: AsyncSession, wire_headers: StrDict, ) -> None: """A root commit (no parent_commit_id) must be accepted — this is the initial push.""" repo = await factory_create_repo( db_session, slug=f"integrity-root-commit-{_uuid4()}", owner="test-user-wire" ) root = _make_commit(parent=None) resp = await client.post( f"/{repo.owner}/{repo.slug}/push", content=_mp({ "bundle": {"commits": [root], "snapshots": [], "objects": []}, "branch": "main", }), headers=wire_headers, ) assert resp.status_code == 200