|
1 | 1 | use crate::util::{RequestHelper, Response, TestApp}; |
| 2 | +use chrono::{DateTime, TimeDelta, Utc}; |
| 3 | +use claims::{assert_ge, assert_le, assert_ok}; |
| 4 | +use crates_io_database::models::{AbandonedUsername, NewAbandonedUsername, User}; |
| 5 | +use crates_io_database::schema::{abandoned_usernames, reserved_usernames}; |
| 6 | +use diesel::HasQuery; |
| 7 | +use diesel::prelude::*; |
| 8 | +use diesel_async::RunQueryDsl; |
2 | 9 | use http::StatusCode; |
3 | 10 | use insta::assert_snapshot; |
4 | 11 | use serde_json::json; |
@@ -26,6 +33,13 @@ impl crate::util::MockCookieUser { |
26 | 33 | assert_eq!(response.status(), StatusCode::OK); |
27 | 34 | assert_eq!(response.json(), json!({ "ok": true })); |
28 | 35 | } |
| 36 | + |
| 37 | + async fn request_username_update(&self, new_username: &str) -> Response<()> { |
| 38 | + let model = self.as_model(); |
| 39 | + let body = json!({"user": { "username": new_username }}); |
| 40 | + let url = format!("/api/v1/users/{}", model.id); |
| 41 | + self.put(&url, body.to_string()).await |
| 42 | + } |
29 | 43 | } |
30 | 44 |
|
31 | 45 | /// Given a crates.io user, check to make sure that the user |
@@ -111,6 +125,118 @@ async fn test_invalid_email_address() { |
111 | 125 | assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"invalid email address"}]}"#); |
112 | 126 | } |
113 | 127 |
|
| 128 | +/// Runs what should be a successful rename and ensures that the |
| 129 | +/// expected DB tables have been updated appropriately |
| 130 | +#[tokio::test(flavor = "multi_thread")] |
| 131 | +async fn test_change_username_happy_path() { |
| 132 | + let (app, _, user) = TestApp::init().with_user().await; |
| 133 | + let model = user.as_model(); |
| 134 | + let mut conn = app.db_conn().await; |
| 135 | + |
| 136 | + let old_username = &model.username; |
| 137 | + let new_username = "new-foo_username1"; |
| 138 | + |
| 139 | + // actuallly do the request |
| 140 | + let put_request_start = Utc::now(); // for checking the timestamps later |
| 141 | + let response: Response<()> = user.request_username_update(new_username).await; |
| 142 | + let put_request_end = Utc::now(); |
| 143 | + |
| 144 | + assert_snapshot!(response.status(), @"200 OK"); |
| 145 | + assert_snapshot!(response.json(), @r#"{"ok":true}"#); |
| 146 | + |
| 147 | + // ───── Postconditions ───── |
| 148 | + // 1. check that new abandoned username record was created |
| 149 | + let records: Vec<AbandonedUsername> = assert_ok!( |
| 150 | + AbandonedUsername::query() |
| 151 | + .filter(abandoned_usernames::username.eq(old_username)) |
| 152 | + .load(&mut conn) |
| 153 | + .await |
| 154 | + ); |
| 155 | + assert_eq!(records.len(), 1); |
| 156 | + let record = records.into_iter().next().unwrap(); |
| 157 | + assert_eq!(record.previous_user_id, Some(model.id)); |
| 158 | + assert_eq!(record.username, *old_username); |
| 159 | + assert_eq!( |
| 160 | + record.available_at, |
| 161 | + record.abandoned_at + TimeDelta::days(30) |
| 162 | + ); |
| 163 | + |
| 164 | + assert_ge!(record.abandoned_at, put_request_start); |
| 165 | + assert_le!(record.abandoned_at, put_request_end); |
| 166 | + |
| 167 | + // 2. user record should have been updated |
| 168 | + let updated_user: User = assert_ok!(User::find(&conn, model.id).await); |
| 169 | + assert_eq!(updated_user.username, new_username); |
| 170 | +} |
| 171 | + |
| 172 | +/// Check that invalid usernames are rejected |
| 173 | +#[tokio::test(flavor = "multi_thread")] |
| 174 | +async fn test_reject_prohibited_username_changes() { |
| 175 | + let (app, _, user) = TestApp::init().with_user().await; |
| 176 | + let mut conn = app.db_conn().await; |
| 177 | + let initial_model = User::find(&conn, user.as_model().id) |
| 178 | + .await |
| 179 | + .expect("initial user lookup"); |
| 180 | + |
| 181 | + // populate various tables with values to test against |
| 182 | + let _squatter = app.db_new_user("desirable-username").await; |
| 183 | + diesel::insert_into(reserved_usernames::table) |
| 184 | + .values(reserved_usernames::username.eq("superadminuser")) |
| 185 | + .execute(&mut conn) |
| 186 | + .await |
| 187 | + .expect("db setup"); |
| 188 | + diesel::insert_into(abandoned_usernames::table) |
| 189 | + .values(NewAbandonedUsername { |
| 190 | + username: "on-cooldown", |
| 191 | + previous_user_id: None, |
| 192 | + abandoned_at: &DateTime::parse_from_rfc3339("2000-01-01T12:00:00Z") |
| 193 | + .unwrap() |
| 194 | + .to_utc(), |
| 195 | + available_at: &DateTime::parse_from_rfc3339("3000-01-01T12:00:00Z") |
| 196 | + .unwrap() |
| 197 | + .to_utc(), |
| 198 | + }) |
| 199 | + .execute(&mut conn) |
| 200 | + .await |
| 201 | + .expect("db setup"); |
| 202 | + |
| 203 | + // ───── tests ───── |
| 204 | + // reject empty string "": it fails string validation |
| 205 | + let response = user.request_username_update("").await; |
| 206 | + assert_snapshot!(response.status(), @"400 Bad Request"); |
| 207 | + assert_snapshot!( |
| 208 | + response.json(), |
| 209 | + @r#"{"errors":[{"detail":"username cannot be empty"}]}"#); |
| 210 | + |
| 211 | + // reject "desirable-username": another user is using it |
| 212 | + let response = user.request_username_update("DeSiRaBlE_uSeRnAmE").await; |
| 213 | + assert_snapshot!(response.status(), @"400 Bad Request"); |
| 214 | + assert_snapshot!( |
| 215 | + response.json(), |
| 216 | + @r#"{"errors":[{"detail":"the username `DeSiRaBlE_uSeRnAmE` is not available"}]}"# |
| 217 | + ); |
| 218 | + |
| 219 | + // reject "superadminuser": it is reserved |
| 220 | + let response = user.request_username_update("SuPeRaDmInUsEr").await; |
| 221 | + assert_snapshot!(response.status(), @"400 Bad Request"); |
| 222 | + assert_snapshot!( |
| 223 | + response.json(), |
| 224 | + @r#"{"errors":[{"detail":"the username `SuPeRaDmInUsEr` is reserved"}]}"#); |
| 225 | + |
| 226 | + // reject "on-cooldown": it is unavailable until the year 3000 |
| 227 | + let response = user.request_username_update("On-CoOlDoWn").await; |
| 228 | + assert_snapshot!(response.status(), @"400 Bad Request"); |
| 229 | + assert_snapshot!( |
| 230 | + response.text(), |
| 231 | + @r#"{"errors":[{"detail":"The username `On-CoOlDoWn` was recently in use. This username will be available after 3000-01-01T12:00:00Z."}]}"# |
| 232 | + ); |
| 233 | + |
| 234 | + // ───── Post-conditions ───── |
| 235 | + // user record should not have changed at all |
| 236 | + let final_model = assert_ok!(User::find(&conn, user.as_model().id).await); |
| 237 | + assert_eq!(initial_model.username, final_model.username); |
| 238 | +} |
| 239 | + |
114 | 240 | #[tokio::test(flavor = "multi_thread")] |
115 | 241 | async fn test_invalid_json() { |
116 | 242 | let (_app, _anon, user) = TestApp::init().with_user().await; |
|
0 commit comments