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