gabriel / musehub public
test_musehub_profile_service.py python
297 lines 12.0 KB
Raw
sha256:3c58668648c7323bb9f5c6881cfe6a3f14fc93fcb73b537d253732952a5bf8bf chore: bump version to 0.2.0rc12 Sonnet 4.6 patch 6 days ago
1 """Unit tests for musehub/services/musehub_profile.py.
2
3 Tests the service-layer profile functions directly (no HTTP), covering:
4 - Profile CRUD (create, get_by_username, get_by_user_id, update)
5 - Contribution graph shape and zero-commit baseline
6 - get_public_repos filters private repos
7 - get_session_credits baseline (no sessions → 0)
8 """
9 from __future__ import annotations
10
11 import pytest
12 from sqlalchemy.ext.asyncio import AsyncSession
13
14 from musehub.models.musehub import ProfileUpdateRequest
15 from musehub.services import musehub_profile
16 from tests.factories import create_profile, create_repo
17 from muse.core.types import long_id
18
19
20 # ---------------------------------------------------------------------------
21 # create_profile / get_profile_by_username
22 # ---------------------------------------------------------------------------
23
24 async def test_create_profile_and_get_by_username(db_session: AsyncSession) -> None:
25 profile = await create_profile(db_session, username="artistone", display_name="Artist One")
26 found = await musehub_profile.get_profile_by_username(db_session, "artistone")
27 assert found is not None
28 assert found.handle == "artistone"
29 assert found.display_name == "Artist One"
30
31
32 async def test_get_profile_by_username_missing_returns_none(db_session: AsyncSession) -> None:
33 result = await musehub_profile.get_profile_by_username(db_session, "ghost-user")
34 assert result is None
35
36
37 async def test_get_profile_by_user_id(db_session: AsyncSession) -> None:
38 profile = await create_profile(db_session, username="byid-user")
39 found = await musehub_profile.get_profile_by_user_id(db_session, profile.identity_id)
40 assert found is not None
41 assert found.handle == "byid-user"
42
43
44 async def test_get_profile_by_user_id_missing_returns_none(db_session: AsyncSession) -> None:
45 result = await musehub_profile.get_profile_by_user_id(db_session, "00000000-dead-beef-0000-000000000000")
46 assert result is None
47
48
49 # ---------------------------------------------------------------------------
50 # update_profile
51 # ---------------------------------------------------------------------------
52
53 async def test_update_profile_bio(db_session: AsyncSession) -> None:
54 orm_profile = await create_profile(db_session, username="bio-user", bio="old bio")
55 await musehub_profile.update_profile(
56 db_session,
57 orm_profile,
58 ProfileUpdateRequest(bio="new bio"),
59 )
60 updated = await musehub_profile.get_profile_by_username(db_session, "bio-user")
61 assert updated is not None
62 assert updated.bio == "new bio"
63
64
65 async def test_update_profile_display_name(db_session: AsyncSession) -> None:
66 orm_profile = await create_profile(db_session, username="name-user", display_name="Old Name")
67 await musehub_profile.update_profile(
68 db_session,
69 orm_profile,
70 ProfileUpdateRequest(display_name="New Name"),
71 )
72 updated = await musehub_profile.get_profile_by_username(db_session, "name-user")
73 assert updated is not None
74 assert updated.display_name == "New Name"
75
76
77 # ---------------------------------------------------------------------------
78 # get_public_repos
79 # ---------------------------------------------------------------------------
80
81 async def test_get_public_repos_returns_public_only(db_session: AsyncSession) -> None:
82 profile = await create_profile(db_session, username="pub-repo-user")
83 await create_repo(
84 db_session,
85 owner="pub-repo-user",
86 owner_user_id=profile.identity_id,
87 slug="public-one",
88 visibility="public",
89 )
90 await create_repo(
91 db_session,
92 owner="pub-repo-user",
93 owner_user_id=profile.identity_id,
94 slug="private-one",
95 visibility="private",
96 )
97
98 repos = await musehub_profile.get_public_repos(db_session, profile.handle)
99 slugs = [r.slug for r in repos]
100 assert "public-one" in slugs
101 assert "private-one" not in slugs
102
103
104 async def test_get_public_repos_empty_for_no_repos(db_session: AsyncSession) -> None:
105 profile = await create_profile(db_session, username="no-repos-user")
106 repos = await musehub_profile.get_public_repos(db_session, profile.handle)
107 assert repos == []
108
109
110 # ---------------------------------------------------------------------------
111 # get_session_credits
112 # ---------------------------------------------------------------------------
113
114 async def test_session_credits_zero_baseline(db_session: AsyncSession) -> None:
115 profile = await create_profile(db_session, username="credit-user")
116 credits = await musehub_profile.get_session_credits(db_session, profile.handle)
117 assert credits == 0
118
119
120 # ---------------------------------------------------------------------------
121 # get_full_profile
122 # ---------------------------------------------------------------------------
123
124 async def test_get_full_profile_returns_structured_response(db_session: AsyncSession) -> None:
125 profile = await create_profile(
126 db_session,
127 username="full-profile-user",
128 bio="Full profile bio",
129 display_name="Full User",
130 )
131 result = await musehub_profile.get_full_profile(db_session, "full-profile-user")
132
133 assert result is not None
134 assert result.username == "full-profile-user"
135 assert result.bio == "Full profile bio"
136 assert result.display_name == "Full User"
137 assert isinstance(result.repos, list)
138 assert result.session_credits == 0
139
140
141 async def test_get_full_profile_missing_returns_none(db_session: AsyncSession) -> None:
142 result = await musehub_profile.get_full_profile(db_session, "nobody-at-all")
143 assert result is None
144
145
146 # ---------------------------------------------------------------------------
147 # get_public_repos — domain field
148 # ---------------------------------------------------------------------------
149
150 async def test_get_public_repos_domain_defaults_to_code(db_session: AsyncSession) -> None:
151 profile = await create_profile(db_session, username="domain-default-user")
152 await create_repo(
153 db_session,
154 owner="domain-default-user",
155 owner_user_id=profile.identity_id,
156 slug="no-domain-repo",
157 visibility="public",
158 )
159 repos = await musehub_profile.get_public_repos(db_session, profile.handle)
160 assert len(repos) == 1
161 assert repos[0].domain == "code"
162
163
164 async def test_get_public_repos_preserves_explicit_domain(db_session: AsyncSession) -> None:
165 """domain_id is a plain string label — no musehub_domains join required."""
166 profile = await create_profile(db_session, username="domain-explicit-user")
167 await create_repo(
168 db_session,
169 owner="domain-explicit-user",
170 owner_user_id=profile.identity_id,
171 slug="midi-repo",
172 visibility="public",
173 domain_id="midi",
174 )
175 repos = await musehub_profile.get_public_repos(db_session, profile.handle)
176 assert len(repos) == 1
177 assert repos[0].domain == "midi"
178
179
180 # ---------------------------------------------------------------------------
181 # build_activity_canvas — domain-driven, no phantom rows
182 # ---------------------------------------------------------------------------
183
184 async def test_canvas_empty_for_user_with_no_repos(db_session: AsyncSession) -> None:
185 """Canvas returns [] for a user with no repos — no phantom domain rows."""
186 profile = await create_profile(db_session, username="no-repos-canvas-user")
187 canvas = await musehub_profile.build_activity_canvas(db_session, profile.handle)
188 assert canvas == []
189
190
191 async def test_canvas_shows_only_domains_with_commits(db_session: AsyncSession) -> None:
192 """Canvas only includes domains where the user has commits. domain_id is a plain string."""
193 import secrets
194 from datetime import datetime, timezone, timedelta
195 from musehub.db.musehub_repo_models import MusehubCommit, MusehubCommitRef, MusehubRepo
196 from musehub.core.genesis import compute_repo_id, compute_identity_id
197
198 handle = f"canvas-real-{secrets.token_hex(4)}"
199 await create_profile(db_session, username=handle)
200 owner_id = compute_identity_id(handle.encode())
201 ts = datetime.now(tz=timezone.utc) - timedelta(days=5)
202
203 # Code repo with one commit — must appear
204 code_repo = MusehubRepo(
205 repo_id=compute_repo_id(owner_id, "code-proj", "code", ts.isoformat()),
206 name="code-proj", owner=handle, slug="code-proj",
207 visibility="public", owner_user_id=owner_id, domain_id="code",
208 )
209 db_session.add(code_repo)
210 _cid1 = long_id(secrets.token_hex(32))
211 db_session.add(MusehubCommit(
212 commit_id=_cid1,
213 branch="main", parent_ids=[],
214 author=handle, message="init", timestamp=ts,
215 ))
216 db_session.add(MusehubCommitRef(repo_id=code_repo.repo_id, commit_id=_cid1))
217
218 # Midi repo with no commits — must NOT appear
219 midi_repo = MusehubRepo(
220 repo_id=compute_repo_id(owner_id, "midi-proj", "midi", ts.isoformat()),
221 name="midi-proj", owner=handle, slug="midi-proj",
222 visibility="public", owner_user_id=owner_id, domain_id="midi",
223 )
224 db_session.add(midi_repo)
225 await db_session.commit()
226
227 canvas = await musehub_profile.build_activity_canvas(db_session, handle)
228 domain_names = [d.domain for d in canvas]
229 assert "code" in domain_names, "code domain must appear — it has commits"
230 assert "midi" not in domain_names, "midi domain must not appear — no commits"
231
232
233 async def test_canvas_null_domain_id_counts_as_code(db_session: AsyncSession) -> None:
234 """Repos with domain_id=null must appear in the 'code' bucket on the canvas."""
235 import secrets
236 from datetime import datetime, timezone, timedelta
237 from musehub.db.musehub_repo_models import MusehubCommit, MusehubCommitRef, MusehubRepo
238 from musehub.core.genesis import compute_repo_id, compute_identity_id
239
240 handle = f"canvas-null-{secrets.token_hex(4)}"
241 await create_profile(db_session, username=handle)
242 owner_id = compute_identity_id(handle.encode())
243 ts = datetime.now(tz=timezone.utc) - timedelta(days=3)
244
245 repo = MusehubRepo(
246 repo_id=compute_repo_id(owner_id, "legacy-repo", "", ts.isoformat()),
247 name="legacy-repo", owner=handle, slug="legacy-repo",
248 visibility="public", owner_user_id=owner_id, domain_id=None,
249 )
250 db_session.add(repo)
251 _cid2 = long_id(secrets.token_hex(32))
252 db_session.add(MusehubCommit(
253 commit_id=_cid2,
254 branch="main", parent_ids=[],
255 author=handle, message="init", timestamp=ts,
256 ))
257 db_session.add(MusehubCommitRef(repo_id=repo.repo_id, commit_id=_cid2))
258 await db_session.commit()
259
260 canvas = await musehub_profile.build_activity_canvas(db_session, handle)
261 domain_names = [d.domain for d in canvas]
262 assert "code" in domain_names, "null domain_id repos must count toward 'code'"
263
264
265 async def test_canvas_excludes_internal_domains(db_session: AsyncSession) -> None:
266 """identity and social repos never appear in the activity canvas."""
267 import secrets
268 from datetime import datetime, timezone, timedelta
269 from musehub.db.musehub_repo_models import MusehubCommit, MusehubCommitRef, MusehubRepo
270 from musehub.core.genesis import compute_repo_id, compute_identity_id
271
272 handle = f"canvas-internal-{secrets.token_hex(4)}"
273 await create_profile(db_session, username=handle)
274 owner_id = compute_identity_id(handle.encode())
275
276 ts = datetime.now(tz=timezone.utc) - timedelta(days=2)
277 for domain_id in ("identity", "social"):
278 repo = MusehubRepo(
279 repo_id=compute_repo_id(owner_id, f"{domain_id}-repo", domain_id, ts.isoformat()),
280 name=f"{domain_id}-repo", owner=handle, slug=f"{domain_id}-repo",
281 visibility="private", owner_user_id=owner_id, domain_id=domain_id,
282 )
283 db_session.add(repo)
284 _cid = long_id(secrets.token_hex(32))
285 db_session.add(MusehubCommit(
286 commit_id=_cid,
287 branch="main", parent_ids=[],
288 author=handle, message="sys", timestamp=ts,
289 ))
290 db_session.add(MusehubCommitRef(repo_id=repo.repo_id, commit_id=_cid))
291 await db_session.commit()
292
293 canvas = await musehub_profile.build_activity_canvas(db_session, handle)
294 domain_names = [d.domain for d in canvas]
295 assert "identity" not in domain_names
296 assert "social" not in domain_names
297
File History 1 commit
sha256:35d76015db2541686c33edd44343ea2d9f751325b4a5556cc9c4c9c0f84edbbe chore: bump version to 0.2.0rc12 Sonnet 4.6 patch 5 days ago