gabriel / musehub public
factories.py python
236 lines 6.9 KB
7923a405 test(supercharge): comprehensive test suite overhaul — all 11 points Gabriel Cardona <gabriel@tellurstori.com> 6d 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 hashlib
24 import re
25 import uuid
26 from datetime import datetime, timezone
27
28 import factory
29 from sqlalchemy.ext.asyncio import AsyncSession
30
31 from musehub.db import musehub_models as db
32
33
34 # ---------------------------------------------------------------------------
35 # Helpers
36 # ---------------------------------------------------------------------------
37
38 def _uid() -> str:
39 return str(uuid.uuid4())
40
41
42 def _now() -> datetime:
43 return datetime.now(tz=timezone.utc)
44
45
46 def _slugify(name: str) -> str:
47 """Convert a human-readable name to a URL-safe slug."""
48 return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") or "repo"
49
50
51 def _sha(seed: str) -> str:
52 return hashlib.sha256(seed.encode()).hexdigest()
53
54
55 # ---------------------------------------------------------------------------
56 # Attribute factories (no DB access)
57 # ---------------------------------------------------------------------------
58
59 class RepoFactory(factory.Factory):
60 """Generate attribute dicts for MusehubRepo."""
61
62 class Meta:
63 model = dict
64
65 name: str = factory.Sequence(lambda n: f"Test Repo {n}")
66 owner: str = "testuser"
67 slug: str = factory.LazyAttribute(lambda o: _slugify(o.name))
68 visibility: str = "public"
69 owner_user_id: str = factory.LazyFunction(_uid)
70 description: str = factory.LazyAttribute(lambda o: f"Description for {o.name}")
71 tags: list = factory.LazyFunction(list)
72 key_signature: str | None = None
73 tempo_bpm: int | None = None
74
75
76 class BranchFactory(factory.Factory):
77 class Meta:
78 model = dict
79
80 name: str = "main"
81 head_commit_id: str | None = None
82
83
84 class CommitFactory(factory.Factory):
85 class Meta:
86 model = dict
87
88 commit_id: str = factory.LazyFunction(lambda: _sha(str(uuid.uuid4())))
89 message: str = factory.Sequence(lambda n: f"feat: commit number {n}")
90 author: str = "testuser"
91 branch: str = "main"
92 parent_ids: list = factory.LazyFunction(list)
93 snapshot_id: str | None = None
94 timestamp: datetime = factory.LazyFunction(_now)
95
96
97 class ProfileFactory(factory.Factory):
98 class Meta:
99 model = dict
100
101 user_id: str = factory.LazyFunction(_uid)
102 username: str = factory.Sequence(lambda n: f"user{n}")
103 display_name: str = factory.LazyAttribute(lambda o: o.username.title())
104 bio: str = "A musician who uses Muse VCS."
105 avatar_url: str | None = None
106 location: str | None = None
107 website_url: str | None = None
108 twitter_handle: str | None = None
109 is_verified: bool = False
110 cc_license: str | None = None
111 pinned_repo_ids: list = factory.LazyFunction(list)
112
113
114 class IssueFactory(factory.Factory):
115 class Meta:
116 model = dict
117
118 title: str = factory.Sequence(lambda n: f"Issue #{n}")
119 body: str = "Issue body text."
120 author: str = "testuser"
121 status: str = "open"
122
123
124 class SessionFactory(factory.Factory):
125 class Meta:
126 model = dict
127
128 session_id: str = factory.LazyFunction(_uid)
129 participants: list = factory.LazyFunction(lambda: ["testuser"])
130 commits: list = factory.LazyFunction(list)
131 notes: str | None = None
132 location: str | None = None
133 intent: str | None = None
134
135
136 # ---------------------------------------------------------------------------
137 # Async persistence helpers
138 # ---------------------------------------------------------------------------
139
140 async def create_repo(
141 session: AsyncSession,
142 **kwargs: object,
143 ) -> db.MusehubRepo:
144 """Insert and return a MusehubRepo row using RepoFactory defaults."""
145 data = RepoFactory(**kwargs)
146 repo = db.MusehubRepo(
147 name=data["name"],
148 owner=data["owner"],
149 slug=data["slug"],
150 visibility=data["visibility"],
151 owner_user_id=data["owner_user_id"],
152 description=data["description"],
153 tags=data["tags"],
154 key_signature=data.get("key_signature"),
155 tempo_bpm=data.get("tempo_bpm"),
156 )
157 session.add(repo)
158 await session.commit()
159 await session.refresh(repo)
160 return repo
161
162
163 async def create_branch(
164 session: AsyncSession,
165 repo_id: str,
166 **kwargs: object,
167 ) -> db.MusehubBranch:
168 """Insert and return a MusehubBranch row."""
169 data = BranchFactory(**kwargs)
170 branch = db.MusehubBranch(
171 repo_id=repo_id,
172 name=data["name"],
173 head_commit_id=data.get("head_commit_id"),
174 )
175 session.add(branch)
176 await session.commit()
177 await session.refresh(branch)
178 return branch
179
180
181 async def create_commit(
182 session: AsyncSession,
183 repo_id: str,
184 **kwargs: object,
185 ) -> db.MusehubCommit:
186 """Insert and return a MusehubCommit row."""
187 data = CommitFactory(**kwargs)
188 commit = db.MusehubCommit(
189 commit_id=data["commit_id"],
190 repo_id=repo_id,
191 message=data["message"],
192 author=data["author"],
193 branch=data["branch"],
194 parent_ids=data["parent_ids"],
195 snapshot_id=data.get("snapshot_id"),
196 timestamp=data.get("timestamp") or _now(),
197 )
198 session.add(commit)
199 await session.commit()
200 await session.refresh(commit)
201 return commit
202
203
204 async def create_profile(
205 session: AsyncSession,
206 **kwargs: object,
207 ) -> db.MusehubProfile:
208 """Insert and return a MusehubProfile row."""
209 data = ProfileFactory(**kwargs)
210 profile = db.MusehubProfile(
211 user_id=data["user_id"],
212 username=data["username"],
213 display_name=data["display_name"],
214 bio=data["bio"],
215 avatar_url=data.get("avatar_url"),
216 location=data.get("location"),
217 website_url=data.get("website_url"),
218 twitter_handle=data.get("twitter_handle"),
219 is_verified=data["is_verified"],
220 cc_license=data.get("cc_license"),
221 pinned_repo_ids=data["pinned_repo_ids"],
222 )
223 session.add(profile)
224 await session.commit()
225 await session.refresh(profile)
226 return profile
227
228
229 async def create_repo_with_branch(
230 session: AsyncSession,
231 **kwargs: object,
232 ) -> tuple[db.MusehubRepo, db.MusehubBranch]:
233 """Convenience: create a repo + default 'main' branch atomically."""
234 repo = await create_repo(session, **kwargs)
235 branch = await create_branch(session, repo_id=str(repo.repo_id), name="main")
236 return repo, branch