gabriel / musehub public
test_opengraph_repo_cards.py python
230 lines 7.8 KB
Raw
sha256:6ad6d62107bbd6940c52d63327966b8a65c6391f4a3da6cd9e7548ba4b38bc48 feat(musehub#129): OG repo preview cards — endpoint, cache,… Human minor ⚠ breaking 14 days ago
1 """Open Graph repo card tests — musehub#129 OG_01 through OG_06."""
2
3 from __future__ import annotations
4
5 from datetime import datetime, timezone
6
7 import pytest
8 from httpx import AsyncClient
9 from sqlalchemy.ext.asyncio import AsyncSession
10
11 from musehub.core.genesis import compute_identity_id, compute_repo_id
12 from musehub.db.musehub_repo_models import MusehubRepo
13 from musehub.services.opengraph_cards import compute_content_hash
14 from musehub.services.opengraph_renderer import StubOpenGraphRenderer, reset_renderer_for_tests
15
16 _PNG_MAGIC = b"\x89PNG\r\n\x1a\n"
17
18
19 async def _make_repo(
20 db: AsyncSession,
21 owner: str = "alice",
22 slug: str = "og-test",
23 *,
24 description: str = "A test repository for OG cards",
25 visibility: str = "public",
26 ) -> MusehubRepo:
27 created_at = datetime.now(tz=timezone.utc)
28 owner_id = compute_identity_id(owner.encode())
29 repo = MusehubRepo(
30 repo_id=compute_repo_id(owner_id, slug, "code", created_at.isoformat()),
31 name=slug,
32 owner=owner,
33 slug=slug,
34 visibility=visibility,
35 description=description,
36 owner_user_id=owner_id,
37 created_at=created_at,
38 updated_at=created_at,
39 )
40 db.add(repo)
41 await db.commit()
42 await db.refresh(repo)
43 return repo
44
45
46 @pytest.fixture(autouse=True)
47 def _reset_og_renderer() -> None:
48 reset_renderer_for_tests()
49
50
51 # ---------------------------------------------------------------------------
52 # OG_01 — endpoint returns PNG
53 # ---------------------------------------------------------------------------
54
55
56 async def test_og01_opengraph_image_returns_png(
57 client: AsyncClient,
58 db_session: AsyncSession,
59 ) -> None:
60 await _make_repo(db_session, owner="alice", slug="og-test")
61 resp = await client.get("/alice/og-test/opengraph-image")
62 assert resp.status_code == 200
63 assert resp.headers.get("content-type", "").startswith("image/png")
64 assert resp.content[:8] == _PNG_MAGIC
65
66
67 async def test_og01_private_repo_returns_404(
68 client: AsyncClient,
69 db_session: AsyncSession,
70 ) -> None:
71 await _make_repo(db_session, owner="alice", slug="secret", visibility="private")
72 resp = await client.get("/alice/secret/opengraph-image")
73 assert resp.status_code == 404
74
75
76 # ---------------------------------------------------------------------------
77 # OG_02 — description in template context (stub renderer)
78 # ---------------------------------------------------------------------------
79
80
81 async def test_og02_description_in_renderer_context(
82 client: AsyncClient,
83 db_session: AsyncSession,
84 ) -> None:
85 desc = "Unique OG description string for OG_02"
86 await _make_repo(db_session, owner="bob", slug="desc-repo", description=desc)
87 await client.get("/bob/desc-repo/opengraph-image")
88 ctx = StubOpenGraphRenderer.last_context
89 assert ctx is not None
90 assert ctx.description == desc
91
92
93 async def test_og02_empty_description_uses_fallback(
94 client: AsyncClient,
95 db_session: AsyncSession,
96 ) -> None:
97 await _make_repo(db_session, owner="carol", slug="no-desc", description="")
98 await client.get("/carol/no-desc/opengraph-image")
99 ctx = StubOpenGraphRenderer.last_context
100 assert ctx is not None
101 assert "carol" in ctx.description
102
103
104 # ---------------------------------------------------------------------------
105 # OG_03 — avatar context for registered and unregistered owners
106 # ---------------------------------------------------------------------------
107
108
109 async def test_og03_avatar_b64_present_for_unregistered_owner(
110 client: AsyncClient,
111 db_session: AsyncSession,
112 ) -> None:
113 await _make_repo(db_session, owner="dave", slug="avatar-repo")
114 await client.get("/dave/avatar-repo/opengraph-image")
115 ctx = StubOpenGraphRenderer.last_context
116 assert ctx is not None
117 assert len(ctx.avatar_png_b64) > 20
118
119
120 # ---------------------------------------------------------------------------
121 # OG_04 — stats reflect DB (commits count via stub context)
122 # ---------------------------------------------------------------------------
123
124
125 async def test_og04_stats_in_renderer_context(
126 client: AsyncClient,
127 db_session: AsyncSession,
128 ) -> None:
129 await _make_repo(db_session, owner="erin", slug="stats-repo")
130 await client.get("/erin/stats-repo/opengraph-image")
131 ctx = StubOpenGraphRenderer.last_context
132 assert ctx is not None
133 assert ctx.total_commits >= 0
134 assert ctx.fork_count == 0
135 assert ctx.open_issue_count == 0
136
137
138 # ---------------------------------------------------------------------------
139 # OG_05 — cache: second request does not re-render; metadata change does
140 # ---------------------------------------------------------------------------
141
142
143 async def test_og05_cache_hit_avoids_second_render(
144 client: AsyncClient,
145 db_session: AsyncSession,
146 ) -> None:
147 repo = await _make_repo(db_session, owner="frank", slug="cache-repo")
148 await client.get("/frank/cache-repo/opengraph-image")
149 assert StubOpenGraphRenderer.render_count == 1
150 await client.get("/frank/cache-repo/opengraph-image")
151 assert StubOpenGraphRenderer.render_count == 1
152
153 repo.description = "Updated description busts the cache"
154 await db_session.commit()
155 await client.get("/frank/cache-repo/opengraph-image")
156 assert StubOpenGraphRenderer.render_count == 2
157
158
159 # ---------------------------------------------------------------------------
160 # OG_06 — repo page HTML contains absolute og:image with ?cb=
161 # ---------------------------------------------------------------------------
162
163
164 async def test_og06_repo_page_emits_og_image_meta(
165 client: AsyncClient,
166 db_session: AsyncSession,
167 ) -> None:
168 await _make_repo(db_session, owner="grace", slug="meta-repo", description="OG meta test")
169 resp = await client.get("/grace/meta-repo")
170 assert resp.status_code == 200
171 assert 'property="og:image"' in resp.text
172 assert "opengraph-image?cb=" in resp.text
173 assert resp.text.index("http") < resp.text.index("opengraph-image")
174
175
176 async def test_og06_twitter_large_image_card(
177 client: AsyncClient,
178 db_session: AsyncSession,
179 ) -> None:
180 await _make_repo(db_session, owner="hank", slug="twitter-repo")
181 resp = await client.get("/hank/twitter-repo")
182 assert 'name="twitter:card"' in resp.text
183 assert "summary_large_image" in resp.text
184
185
186 # ---------------------------------------------------------------------------
187 # Unit — content hash stability
188 # ---------------------------------------------------------------------------
189
190
191 def test_content_hash_changes_when_description_changes() -> None:
192 from musehub.services.opengraph_cards import OpenGraphCardContext, description_fallback
193
194 base = OpenGraphCardContext(
195 owner="a",
196 repo_slug="b",
197 description=description_fallback("a"),
198 domain_id="code",
199 total_commits=1,
200 fork_count=0,
201 open_issue_count=0,
202 owner_identity_hex="abc",
203 identity_type="agent",
204 avatar_png_b64="e30=",
205 )
206 h1 = compute_content_hash(base)
207 changed = OpenGraphCardContext(
208 owner=base.owner,
209 repo_slug=base.repo_slug,
210 description="different",
211 domain_id=base.domain_id,
212 total_commits=base.total_commits,
213 fork_count=base.fork_count,
214 open_issue_count=base.open_issue_count,
215 owner_identity_hex=base.owner_identity_hex,
216 identity_type=base.identity_type,
217 avatar_png_b64=base.avatar_png_b64,
218 )
219 h2 = compute_content_hash(changed)
220 assert h1 != h2
221
222
223 # ---------------------------------------------------------------------------
224 # Security — slug injection blocked at validation layer
225 # ---------------------------------------------------------------------------
226
227
228 async def test_og_security_invalid_slug_rejected(client: AsyncClient) -> None:
229 resp = await client.get("/alice/not%20valid/opengraph-image")
230 assert resp.status_code == 422
File History 1 commit
sha256:6ad6d62107bbd6940c52d63327966b8a65c6391f4a3da6cd9e7548ba4b38bc48 feat(musehub#129): OG repo preview cards — endpoint, cache,… Human minor 14 days ago