fabrica

hanna/fabrica

test: cover profile queries, avatars, explore, and settings auth

99168ec · hanna committed on 2026-07-25

Add a store round-trip test for update_profile / set_avatar_mime and web
tests for the profile page, identicon fallback, the anonymous Explore
listing (no in-content sign-in button), and the settings auth redirect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
2 files changed · +118 −0UnifiedSplit
crates/store/src/tests.rs +57 −0
@@ -87,6 +87,63 @@ async fn create_and_fetch_user() {
87}87}
8888
89#[tokio::test]89#[tokio::test]
90async fn update_profile_and_avatar_round_trip() {
91 for fx in sqlite_fixtures().await {
92 let user = fx
93 .store
94 .create_user(sample_user("prof", "prof@example.com"))
95 .await
96 .unwrap();
97
98 let ok = fx
99 .store
100 .update_profile(
101 &user.id,
102 crate::ProfileUpdate {
103 display_name: Some("Professor".to_string()),
104 pronouns: Some("they/them".to_string()),
105 bio: Some("Hello there.".to_string()),
106 website: Some("https://example.com".to_string()),
107 location: Some("Everywhere".to_string()),
108 social_github: Some("prof".to_string()),
109 social_mastodon: Some("@prof@example.social".to_string()),
110 },
111 )
112 .await
113 .unwrap();
114 assert!(ok);
115
116 let got = fx.store.user_by_id(&user.id).await.unwrap().unwrap();
117 assert_eq!(got.pronouns.as_deref(), Some("they/them"));
118 assert_eq!(got.bio.as_deref(), Some("Hello there."));
119 assert_eq!(got.social_mastodon.as_deref(), Some("@prof@example.social"));
120 assert!(got.avatar_mime.is_none(), "no avatar yet");
121
122 // Setting then clearing the avatar content type round-trips.
123 assert!(
124 fx.store
125 .set_avatar_mime(&user.id, Some("image/png"))
126 .await
127 .unwrap()
128 );
129 let got = fx.store.user_by_id(&user.id).await.unwrap().unwrap();
130 assert_eq!(got.avatar_mime.as_deref(), Some("image/png"));
131
132 assert!(fx.store.set_avatar_mime(&user.id, None).await.unwrap());
133 let got = fx.store.user_by_id(&user.id).await.unwrap().unwrap();
134 assert!(got.avatar_mime.is_none(), "avatar cleared");
135
136 // A missing user reports no update.
137 assert!(
138 !fx.store
139 .update_profile("nope", crate::ProfileUpdate::default())
140 .await
141 .unwrap()
142 );
143 }
144}
145
146#[tokio::test]
90async fn duplicate_username_conflicts_case_insensitively() {147async fn duplicate_username_conflicts_case_insensitively() {
91 for fx in sqlite_fixtures().await {148 for fx in sqlite_fixtures().await {
92 fx.store149 fx.store
crates/web/src/tests.rs +61 −0
@@ -266,3 +266,64 @@ async fn unknown_owner_is_404() {
266 let res = app.oneshot(get("/nobody")).await.unwrap();266 let res = app.oneshot(get("/nobody")).await.unwrap();
267 assert_eq!(res.status(), StatusCode::NOT_FOUND);267 assert_eq!(res.status(), StatusCode::NOT_FOUND);
268}268}
269
270#[tokio::test]
271async fn profile_page_renders_with_identity() {
272 let (_state, app) = app().await;
273 let res = app.oneshot(get("/ada")).await.unwrap();
274 assert_eq!(res.status(), StatusCode::OK);
275 let html = body_string(res).await;
276 // Display name in the heading and the "User · username" subtitle.
277 assert!(html.contains("Ada"), "display name shown");
278 assert!(html.contains("User · ada"), "profile subtitle shown");
279 assert!(html.contains("/avatar/ada"), "avatar image referenced");
280}
281
282#[tokio::test]
283async fn avatar_falls_back_to_identicon_svg() {
284 let (_state, app) = app().await;
285 let res = app.oneshot(get("/avatar/ada")).await.unwrap();
286 assert_eq!(res.status(), StatusCode::OK);
287 assert_eq!(
288 res.headers().get(header::CONTENT_TYPE).unwrap(),
289 "image/svg+xml; charset=utf-8"
290 );
291 let body = body_string(res).await;
292 assert!(body.starts_with("<svg"), "identicon is an SVG");
293}
294
295#[tokio::test]
296async fn explore_lists_public_repos_for_anonymous() {
297 let (state, app) = app().await;
298 let ada = state.store.user_by_username("ada").await.unwrap().unwrap();
299 state
300 .store
301 .create_repo(NewRepo {
302 owner_id: ada.id,
303 group_id: None,
304 name: "open".to_string(),
305 path: "open".to_string(),
306 description: None,
307 is_private: false,
308 default_branch: "main".to_string(),
309 })
310 .await
311 .unwrap();
312
313 let res = app.oneshot(get("/explore")).await.unwrap();
314 assert_eq!(res.status(), StatusCode::OK);
315 let html = body_string(res).await;
316 assert!(html.contains("Public repositories"), "explore heading");
317 assert!(html.contains("ada/open"), "public repo listed");
318 // The anonymous landing must not offer an in-content sign-in button.
319 assert!(!html.contains("btn-primary"), "no content sign-in button");
320}
321
322#[tokio::test]
323async fn settings_requires_authentication() {
324 let (_state, app) = app().await;
325 let res = app.oneshot(get("/settings")).await.unwrap();
326 // RequireUser redirects anonymous visitors to /login.
327 assert_eq!(res.status(), StatusCode::SEE_OTHER);
328 assert_eq!(res.headers().get(header::LOCATION).unwrap(), "/login");
329}