gabriel / musehub public
factories.py python
323 lines 9.6 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago
1 """Test factories for MuseHub ORM models.
2
3 Provides two layers:
4 1. ``*Factory`` classes (factory_boy ``Factory`` subclasses) that generate
5 realistic attribute dictionaries without touching the database.
6 2. Async ``create_*`` helpers that instantiate the ORM model from the
7 factory data, persist it, and return the refreshed ORM object.
8
9 Usage in tests::
10
11 from tests.factories import create_repo, create_profile, RepoFactory
12
13 async def test_something(db_session):
14 repo = await create_repo(db_session, owner="alice", visibility="public")
15 assert repo.owner == "alice"
16
17 # Data-only (no DB) — useful for unit-testing pure functions:
18 data = RepoFactory(name="My Jazz EP", owner="charlie")
19 assert data["slug"] == "my-jazz-ep"
20 """
21 from __future__ import annotations
22
23 import itertools
24 import re
25 import secrets
26
27 from muse.core.types import blob_id, content_hash
28 from datetime import datetime, timezone
29
30 import factory
31 from sqlalchemy.ext.asyncio import AsyncSession
32
33 from musehub.core.genesis import compute_identity_id, compute_issue_id, compute_proposal_id
34 from musehub.db import musehub_models as db
35 from musehub.types.json_types import JSONValue
36
37
38 # ---------------------------------------------------------------------------
39 # Helpers
40 # ---------------------------------------------------------------------------
41
42 _id_seq = itertools.count()
43
44
45 def _uid() -> str:
46 return secrets.token_hex(16)
47
48
49 def _now() -> datetime:
50 return datetime.now(tz=timezone.utc)
51
52
53 def _slugify(name: str) -> str:
54 """Convert a human-readable name to a URL-safe slug."""
55 return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") or "repo"
56
57
58 # ---------------------------------------------------------------------------
59 # Attribute factories (no DB access)
60 # ---------------------------------------------------------------------------
61
62 class RepoFactory(factory.Factory):
63 """Generate attribute dicts for MusehubRepo."""
64
65 class Meta:
66 model = dict
67
68 name: str = factory.Sequence(lambda n: f"Test Repo {n}")
69 owner: str = "testuser"
70 slug: str = factory.LazyAttribute(lambda o: _slugify(o.name))
71 visibility: str = "public"
72 owner_user_id: str = factory.LazyAttribute(lambda o: compute_identity_id(o.owner.encode()))
73 description: str = factory.LazyAttribute(lambda o: f"Description for {o.name}")
74 tags = factory.LazyFunction(list)
75
76
77 class BranchFactory(factory.Factory):
78 class Meta:
79 model = dict
80
81 name: str = "main"
82 head_commit_id: str | None = None
83
84
85 class CommitFactory(factory.Factory):
86 class Meta:
87 model = dict
88
89 commit_id: str = factory.LazyFunction(lambda: content_hash({"seq": next(_id_seq)}))
90 message: str = factory.Sequence(lambda n: f"feat: commit number {n}")
91 author: str = "testuser"
92 branch: str = "main"
93 parent_ids = factory.LazyFunction(list)
94 snapshot_id: str | None = None
95 timestamp: datetime = factory.LazyFunction(_now)
96
97
98 class ProfileFactory(factory.Factory):
99 class Meta:
100 model = dict
101
102 user_id: str = factory.LazyFunction(_uid)
103 username: str = factory.Sequence(lambda n: f"user{n}")
104 display_name: str = factory.LazyAttribute(lambda o: o.username.title())
105 bio: str = "A musician who uses Muse VCS."
106 avatar_url: str | None = None
107 location: str | None = None
108 website_url: str | None = None
109 social_url: str | None = None
110 is_verified: bool = False
111 cc_license: str | None = None
112 pinned_repo_ids = factory.LazyFunction(list)
113
114
115 class IssueFactory(factory.Factory):
116 class Meta:
117 model = dict
118
119 title: str = factory.Sequence(lambda n: f"Issue #{n}")
120 body: str = "Issue body text."
121 author: str = "testuser"
122 status: str = "open"
123
124
125 class SessionFactory(factory.Factory):
126 class Meta:
127 model = dict
128
129 session_id: str = factory.LazyFunction(_uid)
130 participants = factory.LazyFunction(lambda: ["testuser"])
131 commits = factory.LazyFunction(list)
132 notes: str | None = None
133 location: str | None = None
134 intent: str | None = None
135
136
137 # ---------------------------------------------------------------------------
138 # Async persistence helpers
139 # ---------------------------------------------------------------------------
140
141 async def create_repo(
142 session: AsyncSession,
143 **kwargs: JSONValue,
144 ) -> db.MusehubRepo:
145 """Insert and return a MusehubRepo row using RepoFactory defaults."""
146 from musehub.core.genesis import compute_repo_id
147 data = RepoFactory(**kwargs)
148 created_at = _now()
149 owner_user_id = str(data["owner_user_id"])
150 slug = str(data["slug"])
151 domain = str(data.get("domain_id") or "")
152 repo_id = compute_repo_id(owner_user_id, slug, domain, created_at.isoformat())
153 repo = db.MusehubRepo(
154 repo_id=repo_id,
155 name=data["name"],
156 owner=data["owner"],
157 slug=slug,
158 visibility=data["visibility"],
159 owner_user_id=owner_user_id,
160 description=data["description"],
161 tags=data["tags"],
162 created_at=created_at,
163 domain_id=data.get("domain_id"),
164 )
165 session.add(repo)
166 await session.commit()
167 await session.refresh(repo)
168 return repo
169
170
171 async def create_branch(
172 session: AsyncSession,
173 repo_id: str,
174 **kwargs: JSONValue,
175 ) -> db.MusehubBranch:
176 """Insert and return a MusehubBranch row."""
177 from musehub.core.genesis import compute_branch_id
178 data = BranchFactory(**kwargs)
179 name = str(data["name"])
180 branch = db.MusehubBranch(
181 branch_id=compute_branch_id(repo_id, name),
182 repo_id=repo_id,
183 name=name,
184 head_commit_id=data.get("head_commit_id"),
185 )
186 session.add(branch)
187 await session.commit()
188 await session.refresh(branch)
189 return branch
190
191
192 async def create_commit(
193 session: AsyncSession,
194 repo_id: str,
195 **kwargs: JSONValue,
196 ) -> db.MusehubCommit:
197 """Insert and return a MusehubCommit row."""
198 data = CommitFactory(**kwargs)
199 commit = db.MusehubCommit(
200 commit_id=data["commit_id"],
201 repo_id=repo_id,
202 message=data["message"],
203 author=data["author"],
204 branch=data["branch"],
205 parent_ids=data["parent_ids"],
206 snapshot_id=data.get("snapshot_id"),
207 timestamp=data.get("timestamp") or _now(),
208 )
209 session.add(commit)
210 await session.commit()
211 await session.refresh(commit)
212 return commit
213
214
215 async def create_profile(
216 session: AsyncSession,
217 **kwargs: JSONValue,
218 ) -> db.MusehubIdentity:
219 """Insert and return a MusehubIdentity row."""
220 data = ProfileFactory(**kwargs)
221 profile = db.MusehubIdentity(
222 identity_id=data["user_id"],
223 handle=data["username"],
224 identity_type="human",
225 display_name=data["display_name"],
226 bio=data["bio"],
227 avatar_url=data.get("avatar_url"),
228 location=data.get("location"),
229 website_url=data.get("website_url"),
230 social_url=data.get("social_url"),
231 is_verified=data["is_verified"],
232 cc_license=data.get("cc_license"),
233 )
234 session.add(profile)
235 await session.commit()
236 await session.refresh(profile)
237 return profile
238
239
240 async def create_repo_with_branch(
241 session: AsyncSession,
242 **kwargs: JSONValue,
243 ) -> tuple[db.MusehubRepo, db.MusehubBranch]:
244 """Convenience: create a repo + default 'main' branch atomically."""
245 repo = await create_repo(session, **kwargs)
246 branch = await create_branch(session, repo_id=str(repo.repo_id), name="main")
247 return repo, branch
248
249
250 async def create_issue(
251 session: AsyncSession,
252 repo_id: str,
253 *,
254 author: str = "testuser",
255 title: str = "Test issue",
256 body: str = "",
257 state: str = "open",
258 number: int | None = None,
259 ) -> db.MusehubIssue:
260 """Insert and return a MusehubIssue row."""
261 from sqlalchemy import func, select as sa_select
262 if number is None:
263 result = await session.execute(
264 sa_select(func.count()).select_from(db.MusehubIssue).where(db.MusehubIssue.repo_id == repo_id)
265 )
266 number = (result.scalar() or 0) + 1
267 now = _now()
268 author_identity_id = compute_identity_id(author.encode())
269 issue = db.MusehubIssue(
270 issue_id=compute_issue_id(repo_id, author_identity_id, now.isoformat()),
271 repo_id=repo_id,
272 number=number,
273 title=title,
274 body=body,
275 state=state,
276 author=author,
277 created_at=now,
278 updated_at=now,
279 )
280 session.add(issue)
281 await session.commit()
282 await session.refresh(issue)
283 return issue
284
285
286 async def create_proposal(
287 session: AsyncSession,
288 repo_id: str,
289 *,
290 author: str = "testuser",
291 title: str = "Test proposal",
292 body: str = "",
293 state: str = "open",
294 from_branch: str = "feature/test",
295 to_branch: str = "main",
296 proposal_number: int | None = None,
297 ) -> db.MusehubProposal:
298 """Insert and return a MusehubProposal row."""
299 from sqlalchemy import func, select as sa_select
300 if proposal_number is None:
301 result = await session.execute(
302 sa_select(func.count()).select_from(db.MusehubProposal).where(db.MusehubProposal.repo_id == repo_id)
303 )
304 proposal_number = (result.scalar() or 0) + 1
305 now = _now()
306 author_identity_id = compute_identity_id(author.encode())
307 proposal = db.MusehubProposal(
308 proposal_id=compute_proposal_id(repo_id, author_identity_id, from_branch, to_branch, now.isoformat()),
309 repo_id=repo_id,
310 proposal_number=proposal_number,
311 title=title,
312 body=body,
313 state=state,
314 author=author,
315 from_branch=from_branch,
316 to_branch=to_branch,
317 created_at=now,
318 updated_at=now,
319 )
320 session.add(proposal)
321 await session.commit()
322 await session.refresh(proposal)
323 return proposal
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago