gabriel / musehub public
test_musehub_ui_settings_ssr.py python
145 lines 5.0 KB
c0f0b481 release: merge dev → main (#5) Gabriel Cardona <cgcardona@gmail.com> 5d ago
1 """SSR-specific tests for the MuseHub repo settings page.
2
3 Verifies that the settings page uses server-side rendering (Jinja2 templates)
4 rather than a client-side JS shell. The handler passes ``RepoSettingsResponse``
5 into the template context so field values are embedded in the HTML at
6 render-time — no client-side fetch required to display the form.
7
8 Test matrix:
9 - test_settings_page_renders_repo_name_server_side — GET page, assert repo name in form value
10 - test_settings_page_general_form_has_hx_patch — general form has ``hx-patch`` attribute
11 - test_settings_page_danger_zone_has_hx_delete — delete form has ``hx-delete``
12 - test_settings_page_section_nav_present — section nav links present
13 - test_settings_unknown_repo_404 — unknown slug → 404
14 """
15 from __future__ import annotations
16
17 import pytest
18 from httpx import AsyncClient
19 from sqlalchemy.ext.asyncio import AsyncSession
20
21 from musehub.db.musehub_models import MusehubRepo
22
23
24 # ---------------------------------------------------------------------------
25 # Fixtures / helpers
26 # ---------------------------------------------------------------------------
27
28
29 async def _make_repo(
30 db_session: AsyncSession,
31 owner: str = "ssrowner",
32 slug: str = "ssr-settings-repo",
33 visibility: str = "public",
34 name: str | None = None,
35 ) -> MusehubRepo:
36 """Seed a minimal repo for SSR settings tests and return the ORM row."""
37 repo_name = name or slug
38 repo = MusehubRepo(
39 name=repo_name,
40 owner=owner,
41 slug=slug,
42 visibility=visibility,
43 owner_user_id="ssr-settings-uid",
44 )
45 db_session.add(repo)
46 await db_session.commit()
47 await db_session.refresh(repo)
48 return repo
49
50
51 # ---------------------------------------------------------------------------
52 # SSR: repo name embedded in form value at render-time
53 # ---------------------------------------------------------------------------
54
55
56 @pytest.mark.anyio
57 async def test_settings_page_renders_repo_name_server_side(
58 client: AsyncClient,
59 db_session: AsyncSession,
60 ) -> None:
61 """GET settings page embeds the repo name directly into the HTML form value.
62
63 With SSR, the template renders ``value="{{ s.name }}"`` so the repo name
64 is present in the raw HTML response without any client-side fetch.
65 """
66 repo = await _make_repo(
67 db_session, owner="ssrname", slug="my-ssr-repo", name="my-ssr-repo"
68 )
69 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
70 assert resp.status_code == 200
71 assert "my-ssr-repo" in resp.text
72
73
74 # ---------------------------------------------------------------------------
75 # HTMX attributes
76 # ---------------------------------------------------------------------------
77
78
79 @pytest.mark.anyio
80 async def test_settings_page_general_form_has_hx_patch(
81 client: AsyncClient,
82 db_session: AsyncSession,
83 ) -> None:
84 """General settings form uses ``hx-patch`` for HTMX section-save."""
85 repo = await _make_repo(db_session, owner="htmxpatch", slug="htmx-patch-repo")
86 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
87 assert resp.status_code == 200
88 assert "hx-patch" in resp.text
89
90
91 @pytest.mark.anyio
92 async def test_settings_page_danger_zone_has_hx_delete(
93 client: AsyncClient,
94 db_session: AsyncSession,
95 ) -> None:
96 """Danger Zone delete form uses ``hx-delete`` for HTMX repo deletion."""
97 repo = await _make_repo(db_session, owner="htmxdel", slug="htmx-delete-repo")
98 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
99 assert resp.status_code == 200
100 assert "hx-delete" in resp.text
101
102
103 # ---------------------------------------------------------------------------
104 # Section navigation
105 # ---------------------------------------------------------------------------
106
107
108 @pytest.mark.anyio
109 async def test_settings_page_section_nav_present(
110 client: AsyncClient,
111 db_session: AsyncSession,
112 ) -> None:
113 """Settings page includes Alpine.js-powered section navigation links.
114
115 The nav uses ``x-on:click.prevent`` to switch sections client-side without
116 a server round-trip, and ``:class`` binding to highlight the active link.
117 """
118 repo = await _make_repo(db_session, owner="secnav", slug="sec-nav-repo")
119 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
120 assert resp.status_code == 200
121 html = resp.text
122 assert "settings-nav-link" in html
123 # Alpine.js section switching
124 assert "x-data" in html
125 assert "x-show" in html
126 # All four sections present
127 assert "section-general" in html
128 assert "section-merge" in html
129 assert "section-collaboration" in html
130 assert "section-danger" in html
131
132
133 # ---------------------------------------------------------------------------
134 # 404 for unknown repo
135 # ---------------------------------------------------------------------------
136
137
138 @pytest.mark.anyio
139 async def test_settings_unknown_repo_404(
140 client: AsyncClient,
141 db_session: AsyncSession,
142 ) -> None:
143 """GET settings for an unknown repo/slug returns 404."""
144 resp = await client.get("/nobody/nonexistent-repo-ssr/settings")
145 assert resp.status_code == 404