gabriel / musehub public
test_musehub_profile_service.py python
297 lines 12.0 KB
Raw
sha256:73f24973678ceefd56628ee17c6d0535d86e33533828af6c63348f50941515ad Merge branch 'fix/smoke-test-tag-label' into dev Human 15 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 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 50 days ago