Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
crates/store/src/tests.rs +57 −0
| @@ -87,6 +87,63 @@ async fn create_and_fetch_user() { | |||
| 87 | } | 87 | } |
| 88 | 88 | ||
| 89 | #[tokio::test] | 89 | #[tokio::test] |
| 90 | async 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] | ||
| 90 | async fn duplicate_username_conflicts_case_insensitively() { | 147 | async fn duplicate_username_conflicts_case_insensitively() { |
| 91 | for fx in sqlite_fixtures().await { | 148 | for fx in sqlite_fixtures().await { |
| 92 | fx.store | 149 | 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] | ||
| 271 | async 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] | ||
| 283 | async 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] | ||
| 296 | async 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] | ||
| 323 | async 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 | } | ||