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