gabriel / musehub public
test_object_integrity.py python
300 lines 9.7 KB
Raw
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor ⚠ breaking 143 days ago
1 """Tests for checklist 2.4 — Commit integrity on push/stream.
2
3 Covers:
4 - Forged parent_id rejection at receive time
5 - Root commit with no parent accepted
6 - Parent from different repo rejected
7 """
8 from __future__ import annotations
9
10 import struct
11 import uuid
12 from datetime import datetime, timezone
13
14 import msgpack
15 import pytest
16 from httpx import AsyncClient
17 from sqlalchemy.ext.asyncio import AsyncSession
18
19 from tests.factories import create_repo as factory_create_repo
20 from muse.core.types import blob_id
21 from musehub.types.json_types import JSONObject, StrDict
22
23
24 # ── helpers ────────────────────────────────────────────────────────────────────
25
26 def _utc_now() -> str:
27 return datetime.now(tz=timezone.utc).isoformat()
28
29
30 def _last_frame(raw: bytes) -> JSONObject:
31 """Return the last msgpack frame from a push-stream response body."""
32 unpacker = msgpack.Unpacker(raw=False)
33 unpacker.feed(raw)
34 last: JSONObject = {}
35 for frame in unpacker:
36 last = frame
37 return last
38
39
40 def _is_rejection(resp_content: bytes) -> bool:
41 """True if the push-stream response ended with an error frame."""
42 frame = _last_frame(resp_content)
43 return frame.get("t") == "X" or not frame.get("ok", True)
44
45
46 def _sha256_object_id(content: bytes) -> str:
47 return blob_id(content)
48
49
50 def _mp(data: JSONObject) -> bytes:
51 return msgpack.packb(data, use_bin_type=True)
52
53
54 def _sha256_id(seed: str) -> str:
55 return blob_id(seed.encode())
56
57
58 def _uuid4() -> str:
59 return uuid.uuid4().hex[:8]
60
61
62 def _make_commit(
63 commit_id: str | None = None,
64 parent: str | None = None,
65 snap_id: str | None = None,
66 ) -> JSONObject:
67 uid = uuid.uuid4().hex
68 return {
69 "commit_id": commit_id or _sha256_id(f"commit-{uid}"),
70 "branch": "main",
71 "snapshot_id": snap_id or _sha256_id(f"snap-{uid}"),
72 "message": "test commit",
73 "committed_at": _utc_now(),
74 "parent_commit_id": parent,
75 "author": "Test User <[email protected]>",
76 }
77
78
79 def _make_valid_object(content: bytes) -> JSONObject:
80 """Object whose object_id is the correct sha256 of content."""
81 return {
82 "object_id": _sha256_object_id(content),
83 "content": content,
84 "path": "file.bin",
85 "encoding": "raw",
86 }
87
88
89 def _make_tampered_object(content: bytes) -> JSONObject:
90 """Object whose object_id claims sha256 of DIFFERENT content."""
91 wrong_content = content + b"\x00"
92 return {
93 "object_id": _sha256_object_id(wrong_content), # hash of wrong_content
94 "content": content, # but we send content
95 "path": "file.bin",
96 "encoding": "raw",
97 }
98
99
100 def _make_non_sha256_object(content: bytes) -> JSONObject:
101 """Object_id without sha256: prefix — rejected because all IDs are canonically sha256."""
102 return {
103 "object_id": "blob:" + uuid.uuid4().hex,
104 "content": content,
105 "path": "file.bin",
106 "encoding": "raw",
107 }
108
109
110 # ── MWP frame builders ─────────────────────────────────────────────────────────
111
112 def _mwp_frame(ft: str, data: JSONObject) -> bytes:
113 payload = msgpack.packb(data, use_bin_type=True)
114 envelope = msgpack.packb(
115 {"ft": ft, "sz": len(payload), "id": blob_id(payload)},
116 use_bin_type=True,
117 )
118 return (
119 b"muse"
120 + b"\x01"
121 + struct.pack(">I", len(envelope))
122 + envelope
123 + struct.pack(">Q", len(payload))
124 + payload
125 )
126
127
128 def _mwp_stream(
129 commits: list[JSONObject],
130 *,
131 objects: list[JSONObject] | None = None,
132 branch: str = "main",
133 ) -> bytes:
134 objects = objects or []
135 frames: list[bytes] = [
136 _mwp_frame("H", {
137 "t": "H",
138 "branch": branch,
139 "force": False,
140 "head": None,
141 "have": [],
142 "n_objects": len(objects),
143 "n_commits": len(commits),
144 })
145 ]
146 for obj in objects:
147 frames.append(_mwp_frame("O", {
148 "t": "O",
149 "id": obj["object_id"],
150 "path": obj.get("path", "file.bin"),
151 "content": obj["content"],
152 "enc": "raw",
153 }))
154 frames.append(_mwp_frame("C", {
155 "t": "C",
156 "commits": commits,
157 "snapshots": [],
158 }))
159 frames.append(_mwp_frame("E", {
160 "t": "E",
161 "n_objects": len(objects),
162 "n_commits": len(commits),
163 }))
164 return b"".join(frames)
165
166
167 # ── Parent commit integrity: /push/stream ─────────────────────────────────────
168
169 async def test_push_commit_with_parent_in_bundle_succeeds(
170 client: AsyncClient,
171 db_session: AsyncSession,
172 wire_headers: StrDict,
173 ) -> None:
174 """A commit whose parent_commit_id is in the same push bundle must be accepted."""
175 repo = await factory_create_repo(
176 db_session, slug=f"integrity-parent-in-bundle-{_uuid4()}", owner="test-user-wire"
177 )
178 parent_id = _sha256_id(f"parent-{uuid.uuid4().hex}")
179 child_id = _sha256_id(f"child-{uuid.uuid4().hex}")
180 parent = _make_commit(commit_id=parent_id)
181 child = _make_commit(commit_id=child_id, parent=parent_id)
182
183 resp = await client.post(
184 f"/{repo.owner}/{repo.slug}/push/stream",
185 content=_mwp_stream([parent, child]),
186 headers={**wire_headers, "Content-Type": "application/x-muse-wire"},
187 )
188 assert resp.status_code == 200
189
190
191 async def test_push_commit_with_parent_in_db_succeeds(
192 client: AsyncClient,
193 db_session: AsyncSession,
194 wire_headers: StrDict,
195 ) -> None:
196 """A commit whose parent is already in the DB for this repo must be accepted."""
197 repo = await factory_create_repo(
198 db_session, slug=f"integrity-parent-in-db-{_uuid4()}", owner="test-user-wire"
199 )
200 parent_id = _sha256_id(f"parent-{uuid.uuid4().hex}")
201 child_id = _sha256_id(f"child-{uuid.uuid4().hex}")
202 parent = _make_commit(commit_id=parent_id)
203 stream_headers = {**wire_headers, "Content-Type": "application/x-muse-wire"}
204
205 r1 = await client.post(
206 f"/{repo.owner}/{repo.slug}/push/stream",
207 content=_mwp_stream([parent]),
208 headers=stream_headers,
209 )
210 assert r1.status_code == 200
211
212 child = _make_commit(commit_id=child_id, parent=parent_id)
213 r2 = await client.post(
214 f"/{repo.owner}/{repo.slug}/push/stream",
215 content=_mwp_stream([child]),
216 headers=stream_headers,
217 )
218 assert r2.status_code == 200
219
220
221 async def test_push_commit_with_forged_parent_id_is_rejected(
222 client: AsyncClient,
223 db_session: AsyncSession,
224 wire_headers: StrDict,
225 ) -> None:
226 """A commit referencing a parent that exists in neither the bundle nor the repo DB
227 must be rejected."""
228 repo = await factory_create_repo(
229 db_session, slug=f"integrity-forged-parent-{_uuid4()}", owner="test-user-wire"
230 )
231 forged_parent_id = _sha256_id(f"forged-{uuid.uuid4().hex}")
232 child_id = _sha256_id(f"child-{uuid.uuid4().hex}")
233 child = _make_commit(commit_id=child_id, parent=forged_parent_id)
234
235 resp = await client.post(
236 f"/{repo.owner}/{repo.slug}/push/stream",
237 content=_mwp_stream([child]),
238 headers={**wire_headers, "Content-Type": "application/x-muse-wire"},
239 )
240 assert resp.status_code == 200
241 assert _is_rejection(resp.content), f"Expected rejection, got: {_last_frame(resp.content)}"
242 frame = _last_frame(resp.content)
243 msg = frame.get("msg", "").lower()
244 assert "parent" in msg or "rejected" in msg, f"Expected parent/rejected in error msg, got: {msg}"
245
246
247 async def test_push_commit_with_parent_from_different_repo_is_rejected(
248 client: AsyncClient,
249 db_session: AsyncSession,
250 wire_headers: StrDict,
251 ) -> None:
252 """A commit whose parent_id exists in a DIFFERENT repo must be rejected."""
253 repo_a = await factory_create_repo(
254 db_session, slug=f"integrity-repo-a-{_uuid4()}", owner="test-user-wire"
255 )
256 repo_b = await factory_create_repo(
257 db_session, slug=f"integrity-repo-b-{_uuid4()}", owner="test-user-wire"
258 )
259 stream_headers = {**wire_headers, "Content-Type": "application/x-muse-wire"}
260
261 commit_in_a_id = _sha256_id(f"commit-a-{uuid.uuid4().hex}")
262 commit_in_a = _make_commit(commit_id=commit_in_a_id)
263 r1 = await client.post(
264 f"/{repo_a.owner}/{repo_a.slug}/push/stream",
265 content=_mwp_stream([commit_in_a]),
266 headers=stream_headers,
267 )
268 assert r1.status_code == 200
269
270 child_id = _sha256_id(f"child-b-{uuid.uuid4().hex}")
271 child = _make_commit(commit_id=child_id, parent=commit_in_a_id)
272 r2 = await client.post(
273 f"/{repo_b.owner}/{repo_b.slug}/push/stream",
274 content=_mwp_stream([child]),
275 headers=stream_headers,
276 )
277 assert r2.status_code == 200
278 assert _is_rejection(r2.content), f"Expected rejection, got: {_last_frame(r2.content)}"
279 frame = _last_frame(r2.content)
280 msg = frame.get("msg", "").lower()
281 assert "parent" in msg or "rejected" in msg, f"Expected parent/rejected in error msg, got: {msg}"
282
283
284 async def test_push_root_commit_with_no_parent_succeeds(
285 client: AsyncClient,
286 db_session: AsyncSession,
287 wire_headers: StrDict,
288 ) -> None:
289 """A root commit (no parent_commit_id) must be accepted — this is the initial push."""
290 repo = await factory_create_repo(
291 db_session, slug=f"integrity-root-commit-{_uuid4()}", owner="test-user-wire"
292 )
293 root = _make_commit(parent=None)
294
295 resp = await client.post(
296 f"/{repo.owner}/{repo.slug}/push/stream",
297 content=_mwp_stream([root]),
298 headers={**wire_headers, "Content-Type": "application/x-muse-wire"},
299 )
300 assert resp.status_code == 200
File History 1 commit
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor 143 days ago