gabriel / musehub public
test_proposal_reopen.py python
209 lines 6.7 KB
Raw
sha256:73f24973678ceefd56628ee17c6d0535d86e33533828af6c63348f50941515ad Merge branch 'fix/smoke-test-tag-label' into dev Human 15 days ago
1 """TDD — POST /repos/{repo_id}/proposals/{proposal_id}/reopen
2
3 Bug / gap
4 ---------
5 A proposal whose merge produced a corrupt commit_id (bug #36) needs to be
6 reset to ``open`` so the author can re-trigger the merge after the fix is
7 deployed. No reopen endpoint exists today.
8
9 Acceptance criteria
10 -------------------
11 R1 POST reopen on a merged proposal → 200, state="open",
12 mergeCommitId=None, mergedAt=None.
13 R2 POST reopen on a closed proposal → 200, state="open".
14 R3 POST reopen on an already-open proposal → 409.
15 R4 Endpoint requires valid MSign auth → 401 without headers.
16 R5 Unknown proposal_id → 404.
17 """
18 from __future__ import annotations
19
20 from datetime import datetime, timezone
21
22 import pytest
23 from httpx import AsyncClient
24 from sqlalchemy.ext.asyncio import AsyncSession
25
26 from muse.core.types import fake_id
27 from musehub.core.genesis import compute_branch_id
28 from musehub.db.musehub_repo_models import MusehubBranch, MusehubCommit, MusehubCommitRef
29 from musehub.db.musehub_social_models import MusehubProposal
30 from musehub.types.json_types import StrDict
31
32
33 # ---------------------------------------------------------------------------
34 # Helpers
35 # ---------------------------------------------------------------------------
36
37
38 async def _create_repo(client: AsyncClient, auth_headers: StrDict, name: str) -> str:
39 resp = await client.post(
40 "/api/repos",
41 json={"name": name, "owner": "testuser", "initialize": False},
42 headers=auth_headers,
43 )
44 assert resp.status_code == 201, resp.text
45 return str(resp.json()["repoId"])
46
47
48 async def _seed_proposal(
49 db: AsyncSession,
50 repo_id: str,
51 *,
52 state: str = "merged",
53 merge_commit_id: str | None = None,
54 ) -> str:
55 """Insert branches + a proposal row directly so we can control its state."""
56 for branch in ("main", "feat/x"):
57 commit_id = fake_id(f"{repo_id}{branch}")
58 db.add(MusehubCommit(
59 commit_id=commit_id,
60 branch=branch,
61 parent_ids=[],
62 message=f"initial on {branch}",
63 author="aaronrene",
64 timestamp=datetime.now(tz=timezone.utc),
65 ))
66 db.add(MusehubCommitRef(repo_id=repo_id, commit_id=commit_id))
67 db.add(MusehubBranch(
68 branch_id=compute_branch_id(repo_id, branch),
69 repo_id=repo_id,
70 name=branch,
71 head_commit_id=commit_id,
72 ))
73 proposal_id = fake_id(f"{repo_id}-proposal")
74 db.add(MusehubProposal(
75 proposal_id=proposal_id,
76 proposal_number=1,
77 repo_id=repo_id,
78 title="feat: some work",
79 body="",
80 state=state,
81 from_branch="feat/x",
82 to_branch="main",
83 author="aaronrene",
84 merge_commit_id=merge_commit_id,
85 merged_at=datetime.now(tz=timezone.utc) if state == "merged" else None,
86 ))
87 await db.commit()
88 return proposal_id
89
90
91 # ---------------------------------------------------------------------------
92 # R1 — reopen a merged proposal clears merge fields and sets state open
93 # ---------------------------------------------------------------------------
94
95
96 @pytest.mark.asyncio
97 async def test_r1_reopen_merged_proposal(
98 client: AsyncClient,
99 auth_headers: StrDict,
100 db_session: AsyncSession,
101 ) -> None:
102 """R1: POST reopen on a merged proposal returns 200 with state='open' and
103 cleared merge fields.
104
105 RED: endpoint does not exist (404 / 405).
106 GREEN: service sets state='open', merge_commit_id=None, merged_at=None.
107 """
108 repo_id = await _create_repo(client, auth_headers, "reopen-merged-repo")
109 proposal_id = await _seed_proposal(
110 db_session, repo_id,
111 state="merged",
112 merge_commit_id=fake_id("bad-merge"),
113 )
114
115 resp = await client.post(
116 f"/api/repos/{repo_id}/proposals/{proposal_id}/reopen",
117 headers=auth_headers,
118 )
119 assert resp.status_code == 200, resp.text
120 body = resp.json()
121 assert body["state"] == "open"
122 assert body["mergeCommitId"] is None
123 assert body["mergedAt"] is None
124
125
126 # ---------------------------------------------------------------------------
127 # R2 — reopen a closed proposal
128 # ---------------------------------------------------------------------------
129
130
131 @pytest.mark.asyncio
132 async def test_r2_reopen_closed_proposal(
133 client: AsyncClient,
134 auth_headers: StrDict,
135 db_session: AsyncSession,
136 ) -> None:
137 """R2: POST reopen on a closed (not merged) proposal also returns 200."""
138 repo_id = await _create_repo(client, auth_headers, "reopen-closed-repo")
139 proposal_id = await _seed_proposal(db_session, repo_id, state="closed")
140
141 resp = await client.post(
142 f"/api/repos/{repo_id}/proposals/{proposal_id}/reopen",
143 headers=auth_headers,
144 )
145 assert resp.status_code == 200, resp.text
146 assert resp.json()["state"] == "open"
147
148
149 # ---------------------------------------------------------------------------
150 # R3 — reopening an already-open proposal is a 409
151 # ---------------------------------------------------------------------------
152
153
154 @pytest.mark.asyncio
155 async def test_r3_reopen_already_open_returns_409(
156 client: AsyncClient,
157 auth_headers: StrDict,
158 db_session: AsyncSession,
159 ) -> None:
160 """R3: POST reopen on an open proposal returns 409 — nothing to reopen."""
161 repo_id = await _create_repo(client, auth_headers, "reopen-open-repo")
162 proposal_id = await _seed_proposal(db_session, repo_id, state="open")
163
164 resp = await client.post(
165 f"/api/repos/{repo_id}/proposals/{proposal_id}/reopen",
166 headers=auth_headers,
167 )
168 assert resp.status_code == 409, resp.text
169
170
171 # ---------------------------------------------------------------------------
172 # R4 — auth required
173 # ---------------------------------------------------------------------------
174
175
176 @pytest.mark.asyncio
177 async def test_r4_reopen_requires_auth(
178 client: AsyncClient,
179 db_session: AsyncSession,
180 ) -> None:
181 """R4: POST reopen without auth headers returns 401."""
182 repo_id = fake_id("no-auth-repo")
183 proposal_id = fake_id("no-auth-proposal")
184
185 resp = await client.post(
186 f"/api/repos/{repo_id}/proposals/{proposal_id}/reopen",
187 )
188 assert resp.status_code == 401
189
190
191 # ---------------------------------------------------------------------------
192 # R5 — unknown proposal returns 404
193 # ---------------------------------------------------------------------------
194
195
196 @pytest.mark.asyncio
197 async def test_r5_reopen_unknown_proposal_returns_404(
198 client: AsyncClient,
199 auth_headers: StrDict,
200 ) -> None:
201 """R5: POST reopen on an unknown proposal_id returns 404."""
202 repo_id = fake_id("ghost-repo")
203 proposal_id = fake_id("ghost-proposal")
204
205 resp = await client.post(
206 f"/api/repos/{repo_id}/proposals/{proposal_id}/reopen",
207 headers=auth_headers,
208 )
209 assert resp.status_code == 404
File History 12 commits
sha256:cfefc25a166c3c3eed8ea3529aee19ea350bc05f2954d007420e924133b7d8ce chore: pivot to nightly channel — bump version to 0.2.0.dev… Sonnet 5 patch 13 days ago
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2 chore: bump version to 0.2.0rc15 for musehub#113 fix release Sonnet 4.6 patch 15 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 17 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53 merge: rescue snapshot-recovery hardening (c00aa21d) into d… Opus 4.8 minor 30 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a fix: remove false-positive proposal_comments index drop fro… Sonnet 4.6 patch 34 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 35 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6 Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 36 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 36 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo… Human 36 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop… Human 36 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 48 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583 rename: delta_add → delta_upsert across wire format, models… Sonnet 4.6 patch 51 days ago