diff --git a/src/tests/routes/crates/list.rs b/src/tests/routes/crates/list.rs index 27d50e78389..fd5e89ed740 100644 --- a/src/tests/routes/crates/list.rs +++ b/src/tests/routes/crates/list.rs @@ -819,3 +819,31 @@ fn pagination_parameters_only_accept_integers() { json!({ "errors": [{ "detail": "invalid digit found in string" }] }) ); } + +#[test] +fn crates_by_user_id() { + let (app, _, user) = TestApp::init().with_user(); + let id = user.as_model().id; + app.db(|conn| { + CrateBuilder::new("foo_my_packages", id).expect_build(conn); + }); + + let response = user.search_by_user_id(id); + assert_eq!(response.crates.len(), 1); +} + +#[test] +fn crates_by_user_id_not_including_deleted_owners() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + let krate = CrateBuilder::new("foo_my_packages", user.id).expect_build(conn); + krate + .owner_remove(app.as_inner(), conn, user, "foo") + .unwrap(); + }); + + let response = anon.search_by_user_id(user.id); + assert_eq!(response.crates.len(), 0); +} diff --git a/src/tests/routes/me/email_notifications.rs b/src/tests/routes/me/email_notifications.rs new file mode 100644 index 00000000000..9e104bf3478 --- /dev/null +++ b/src/tests/routes/me/email_notifications.rs @@ -0,0 +1,136 @@ +use crate::builders::CrateBuilder; +use crate::util::{RequestHelper, TestApp}; +use crate::{new_user, OkBool}; +use cargo_registry::schema::crate_owners; +use diesel::prelude::*; + +#[derive(Serialize)] +struct EmailNotificationsUpdate { + id: i32, + email_notifications: bool, +} + +impl crate::util::MockCookieUser { + fn update_email_notifications(&self, updates: Vec) -> OkBool { + self.put( + "/api/v1/me/email_notifications", + json!(updates).to_string().as_bytes(), + ) + .good() + } +} + +/// A user should be able to update the email notifications for crates they own. Only the crates that +/// were sent in the request should be updated to the corresponding `email_notifications` value. +#[test] +fn test_update_email_notifications() { + let (app, _, user) = TestApp::init().with_user(); + + let my_crates = app.db(|conn| { + vec![ + CrateBuilder::new("test_package", user.as_model().id).expect_build(conn), + CrateBuilder::new("another_package", user.as_model().id).expect_build(conn), + ] + }); + + let a_id = my_crates.get(0).unwrap().id; + let b_id = my_crates.get(1).unwrap().id; + + // Update crate_a: email_notifications = false + // crate_a should be false, crate_b should be true + user.update_email_notifications(vec![EmailNotificationsUpdate { + id: a_id, + email_notifications: false, + }]); + let json = user.show_me(); + + assert!( + !json + .owned_crates + .iter() + .find(|c| c.id == a_id) + .unwrap() + .email_notifications + ); + assert!( + json.owned_crates + .iter() + .find(|c| c.id == b_id) + .unwrap() + .email_notifications + ); + + // Update crate_b: email_notifications = false + // Both should be false now + user.update_email_notifications(vec![EmailNotificationsUpdate { + id: b_id, + email_notifications: false, + }]); + let json = user.show_me(); + + assert!( + !json + .owned_crates + .iter() + .find(|c| c.id == a_id) + .unwrap() + .email_notifications + ); + assert!( + !json + .owned_crates + .iter() + .find(|c| c.id == b_id) + .unwrap() + .email_notifications + ); + + // Update crate_a and crate_b: email_notifications = true + // Both should be true + user.update_email_notifications(vec![ + EmailNotificationsUpdate { + id: a_id, + email_notifications: true, + }, + EmailNotificationsUpdate { + id: b_id, + email_notifications: true, + }, + ]); + let json = user.show_me(); + + json.owned_crates.iter().for_each(|c| { + assert!(c.email_notifications); + }) +} + +/// A user should not be able to update the `email_notifications` value for a crate that is not +/// owned by them. +#[test] +fn test_update_email_notifications_not_owned() { + let (app, _, user) = TestApp::init().with_user(); + + let not_my_crate = app.db(|conn| { + let u = new_user("arbitrary_username") + .create_or_update(None, &app.as_inner().emails, conn) + .unwrap(); + CrateBuilder::new("test_package", u.id).expect_build(conn) + }); + + user.update_email_notifications(vec![EmailNotificationsUpdate { + id: not_my_crate.id, + email_notifications: false, + }]); + + let email_notifications: bool = app + .db(|conn| { + crate_owners::table + .select(crate_owners::email_notifications) + .filter(crate_owners::crate_id.eq(not_my_crate.id)) + .first(conn) + }) + .unwrap(); + + // There should be no change to the `email_notifications` value for a crate not belonging to me + assert!(email_notifications); +} diff --git a/src/tests/routes/me/get.rs b/src/tests/routes/me/get.rs new file mode 100644 index 00000000000..3d1f957a5ed --- /dev/null +++ b/src/tests/routes/me/get.rs @@ -0,0 +1,52 @@ +use crate::builders::CrateBuilder; +use crate::util::{RequestHelper, TestApp}; +use cargo_registry::views::{EncodablePrivateUser, OwnedCrate}; + +impl crate::util::MockCookieUser { + pub fn show_me(&self) -> UserShowPrivateResponse { + let url = "/api/v1/me"; + self.get(url).good() + } +} + +#[derive(Deserialize)] +pub struct UserShowPrivateResponse { + pub user: EncodablePrivateUser, + pub owned_crates: Vec, +} + +#[test] +fn me() { + let url = "/api/v1/me"; + let (app, anon) = TestApp::init().empty(); + anon.get(url).assert_forbidden(); + + let user = app.db_new_user("foo"); + let json = user.show_me(); + + assert_eq!(json.owned_crates.len(), 0); + + app.db(|conn| { + CrateBuilder::new("foo_my_packages", user.as_model().id).expect_build(conn); + assert_eq!(json.user.email, user.as_model().email(conn).unwrap()); + }); + let updated_json = user.show_me(); + + assert_eq!(updated_json.owned_crates.len(), 1); +} + +#[test] +fn test_user_owned_crates_doesnt_include_deleted_ownership() { + let (app, _, user) = TestApp::init().with_user(); + let user_model = user.as_model(); + + app.db(|conn| { + let krate = CrateBuilder::new("foo_my_packages", user_model.id).expect_build(conn); + krate + .owner_remove(app.as_inner(), conn, user_model, &user_model.gh_login) + .unwrap(); + }); + + let json = user.show_me(); + assert_eq!(json.owned_crates.len(), 0); +} diff --git a/src/tests/routes/me/mod.rs b/src/tests/routes/me/mod.rs index 5c76635567e..881f4b7339f 100644 --- a/src/tests/routes/me/mod.rs +++ b/src/tests/routes/me/mod.rs @@ -1 +1,4 @@ +mod email_notifications; +pub mod get; pub mod tokens; +mod updates; diff --git a/src/tests/routes/me/updates.rs b/src/tests/routes/me/updates.rs new file mode 100644 index 00000000000..e135af0a8ff --- /dev/null +++ b/src/tests/routes/me/updates.rs @@ -0,0 +1,97 @@ +use crate::builders::{CrateBuilder, VersionBuilder}; +use crate::util::{RequestHelper, TestApp}; +use crate::OkBool; +use cargo_registry::schema::versions; +use cargo_registry::views::EncodableVersion; +use diesel::prelude::*; +use diesel::update; +use http::StatusCode; + +#[test] +fn api_token_cannot_get_user_updates() { + let (_, _, _, token) = TestApp::init().with_token(); + token.get("/api/v1/me/updates").assert_forbidden(); +} + +#[test] +fn following() { + #[derive(Deserialize)] + struct R { + versions: Vec, + meta: Meta, + } + #[derive(Deserialize)] + struct Meta { + more: bool, + } + + let (app, _, user) = TestApp::init().with_user(); + let user_model = user.as_model(); + let user_id = user_model.id; + app.db(|conn| { + CrateBuilder::new("foo_fighters", user_id) + .version(VersionBuilder::new("1.0.0")) + .expect_build(conn); + + // Make foo_fighters's version mimic a version published before we started recording who + // published versions + let none: Option = None; + update(versions::table) + .set(versions::published_by.eq(none)) + .execute(conn) + .unwrap(); + + CrateBuilder::new("bar_fighters", user_id) + .version(VersionBuilder::new("1.0.0")) + .expect_build(conn); + }); + + let r: R = user.get("/api/v1/me/updates").good(); + assert_eq!(r.versions.len(), 0); + assert!(!r.meta.more); + + user.put::("/api/v1/crates/foo_fighters/follow", b"") + .good(); + user.put::("/api/v1/crates/bar_fighters/follow", b"") + .good(); + + let r: R = user.get("/api/v1/me/updates").good(); + assert_eq!(r.versions.len(), 2); + assert!(!r.meta.more); + let foo_version = r + .versions + .iter() + .find(|v| v.krate == "foo_fighters") + .unwrap(); + assert_none!(&foo_version.published_by); + let bar_version = r + .versions + .iter() + .find(|v| v.krate == "bar_fighters") + .unwrap(); + assert_eq!( + bar_version.published_by.as_ref().unwrap().login, + user_model.gh_login + ); + + let r: R = user + .get_with_query("/api/v1/me/updates", "per_page=1") + .good(); + assert_eq!(r.versions.len(), 1); + assert!(r.meta.more); + + user.delete::("/api/v1/crates/bar_fighters/follow") + .good(); + let r: R = user + .get_with_query("/api/v1/me/updates", "page=2&per_page=1") + .good(); + assert_eq!(r.versions.len(), 0); + assert!(!r.meta.more); + + let response = user.get_with_query::<()>("/api/v1/me/updates", "page=0"); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response.into_json(), + json!({ "errors": [{ "detail": "page indexing starts from 1, page 0 is invalid" }] }) + ); +} diff --git a/src/tests/routes/mod.rs b/src/tests/routes/mod.rs index 08ee5c0d525..49163c72174 100644 --- a/src/tests/routes/mod.rs +++ b/src/tests/routes/mod.rs @@ -17,5 +17,7 @@ pub mod crates; pub mod keywords; pub mod me; pub mod metrics; +pub mod session; pub mod summary; +pub mod users; pub mod versions; diff --git a/src/tests/routes/session/authorize.rs b/src/tests/routes/session/authorize.rs new file mode 100644 index 00000000000..e573e6b5a3d --- /dev/null +++ b/src/tests/routes/session/authorize.rs @@ -0,0 +1,13 @@ +use crate::util::{RequestHelper, TestApp}; +use http::StatusCode; + +#[test] +fn access_token_needs_data() { + let (_, anon) = TestApp::init().empty(); + let response = anon.get::<()>("/api/private/session/authorize"); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response.into_json(), + json!({ "errors": [{ "detail": "invalid state parameter" }] }) + ); +} diff --git a/src/tests/routes/session/begin.rs b/src/tests/routes/session/begin.rs new file mode 100644 index 00000000000..5c2ac297fa4 --- /dev/null +++ b/src/tests/routes/session/begin.rs @@ -0,0 +1,14 @@ +use crate::util::{RequestHelper, TestApp}; + +#[derive(Deserialize)] +struct AuthResponse { + url: String, + state: String, +} + +#[test] +fn auth_gives_a_token() { + let (_, anon) = TestApp::init().empty(); + let json: AuthResponse = anon.get("/api/private/session/begin").good(); + assert!(json.url.contains(&json.state)); +} diff --git a/src/tests/routes/session/mod.rs b/src/tests/routes/session/mod.rs new file mode 100644 index 00000000000..7c5e64fbf7d --- /dev/null +++ b/src/tests/routes/session/mod.rs @@ -0,0 +1,2 @@ +mod authorize; +mod begin; diff --git a/src/tests/routes/users/mod.rs b/src/tests/routes/users/mod.rs new file mode 100644 index 00000000000..c788314a57b --- /dev/null +++ b/src/tests/routes/users/mod.rs @@ -0,0 +1,3 @@ +mod read; +mod stats; +pub mod update; diff --git a/src/tests/routes/users/read.rs b/src/tests/routes/users/read.rs new file mode 100644 index 00000000000..1587bd86c28 --- /dev/null +++ b/src/tests/routes/users/read.rs @@ -0,0 +1,58 @@ +use crate::util::{RequestHelper, TestApp}; +use cargo_registry::models::NewUser; +use cargo_registry::views::EncodablePublicUser; + +#[derive(Deserialize)] +pub struct UserShowPublicResponse { + pub user: EncodablePublicUser, +} + +#[test] +fn show() { + let (app, anon, _) = TestApp::init().with_user(); + app.db_new_user("Bar"); + + let json: UserShowPublicResponse = anon.get("/api/v1/users/foo").good(); + assert_eq!("foo", json.user.login); + + let json: UserShowPublicResponse = anon.get("/api/v1/users/bAr").good(); + assert_eq!("Bar", json.user.login); + assert_eq!(Some("https://github.com/Bar".into()), json.user.url); +} + +#[test] +fn show_latest_user_case_insensitively() { + let (app, anon) = TestApp::init().empty(); + + app.db(|conn| { + // Please do not delete or modify the setup of this test in order to get it to pass. + // This setup mimics how GitHub works. If someone abandons a GitHub account, the username is + // available for anyone to take. We need to support having multiple user accounts + // with the same gh_login in crates.io. `gh_id` is stable across renames, so that field + // should be used for uniquely identifying GitHub accounts whenever possible. For the + // crates.io/user/:username pages, the best we can do is show the last crates.io account + // created with that username. + assert_ok!(NewUser::new( + 1, + "foobar", + Some("I was first then deleted my github account"), + None, + "bar" + ) + .create_or_update(None, &app.as_inner().emails, conn)); + assert_ok!(NewUser::new( + 2, + "FOOBAR", + Some("I was second, I took the foobar username on github"), + None, + "bar" + ) + .create_or_update(None, &app.as_inner().emails, conn)); + }); + + let json: UserShowPublicResponse = anon.get("api/v1/users/fOObAr").good(); + assert_eq!( + "I was second, I took the foobar username on github", + json.user.name.unwrap() + ); +} diff --git a/src/tests/routes/users/stats.rs b/src/tests/routes/users/stats.rs new file mode 100644 index 00000000000..371ea42befb --- /dev/null +++ b/src/tests/routes/users/stats.rs @@ -0,0 +1,60 @@ +use crate::util::{RequestHelper, TestApp}; + +#[derive(Deserialize)] +struct UserStats { + total_downloads: i64, +} + +#[test] +fn user_total_downloads() { + use crate::builders::CrateBuilder; + use crate::util::{RequestHelper, TestApp}; + use diesel::{update, RunQueryDsl}; + + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + let another_user = app.db_new_user("bar"); + let another_user = another_user.as_model(); + + app.db(|conn| { + let mut krate = CrateBuilder::new("foo_krate1", user.id).expect_build(conn); + krate.downloads = 10; + update(&krate).set(&krate).execute(conn).unwrap(); + + let mut krate2 = CrateBuilder::new("foo_krate2", user.id).expect_build(conn); + krate2.downloads = 20; + update(&krate2).set(&krate2).execute(conn).unwrap(); + + let mut another_krate = CrateBuilder::new("bar_krate1", another_user.id).expect_build(conn); + another_krate.downloads = 2; + update(&another_krate) + .set(&another_krate) + .execute(conn) + .unwrap(); + + let mut no_longer_my_krate = CrateBuilder::new("nacho", user.id).expect_build(conn); + no_longer_my_krate.downloads = 5; + update(&no_longer_my_krate) + .set(&no_longer_my_krate) + .execute(conn) + .unwrap(); + no_longer_my_krate + .owner_remove(app.as_inner(), conn, user, &user.gh_login) + .unwrap(); + }); + + let url = format!("/api/v1/users/{}/stats", user.id); + let stats: UserStats = anon.get(&url).good(); + // does not include crates user never owned (2) or no longer owns (5) + assert_eq!(stats.total_downloads, 30); +} + +#[test] +fn user_total_downloads_no_crates() { + let (_, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + let url = format!("/api/v1/users/{}/stats", user.id); + + let stats: UserStats = anon.get(&url).good(); + assert_eq!(stats.total_downloads, 0); +} diff --git a/src/tests/routes/users/update.rs b/src/tests/routes/users/update.rs new file mode 100644 index 00000000000..29ad2dff3b0 --- /dev/null +++ b/src/tests/routes/users/update.rs @@ -0,0 +1,95 @@ +use crate::util::{RequestHelper, Response, TestApp}; +use crate::OkBool; +use http::StatusCode; + +pub trait MockEmailHelper: RequestHelper { + // TODO: I don't like the name of this method or `update_email` on the `MockCookieUser` impl; + // this is starting to look like a builder might help? + // I want to explore alternative abstractions in any case. + fn update_email_more_control(&self, user_id: i32, email: Option<&str>) -> Response { + // When updating your email in crates.io, the request goes to the user route with PUT. + // Ember sends all the user attributes. We check to make sure the ID in the URL matches + // the ID of the currently logged in user, then we ignore everything but the email address. + let body = json!({"user": { + "email": email, + "name": "Arbitrary Name", + "login": "arbitrary_login", + "avatar": "https://arbitrary.com/img.jpg", + "url": "https://arbitrary.com", + "kind": null + }}); + let url = format!("/api/v1/users/{user_id}"); + self.put(&url, body.to_string().as_bytes()) + } +} + +impl MockEmailHelper for crate::util::MockCookieUser {} +impl MockEmailHelper for crate::util::MockAnonymousUser {} + +impl crate::util::MockCookieUser { + pub fn update_email(&self, email: &str) -> OkBool { + let model = self.as_model(); + self.update_email_more_control(model.id, Some(email)).good() + } +} + +/// Given a crates.io user, check to make sure that the user +/// cannot add to the database an empty string or null as +/// their email. If an attempt is made, update_user.rs will +/// return an error indicating that an empty email cannot be +/// added. +/// +/// This is checked on the frontend already, but I'd like to +/// make sure that a user cannot get around that and delete +/// their email by adding an empty string. +#[test] +fn test_empty_email_not_added() { + let (_app, _anon, user) = TestApp::init().with_user(); + let model = user.as_model(); + + let response = user.update_email_more_control(model.id, Some("")); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response.into_json(), + json!({ "errors": [{ "detail": "empty email rejected" }] }) + ); + + let response = user.update_email_more_control(model.id, None); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response.into_json(), + json!({ "errors": [{ "detail": "empty email rejected" }] }) + ); +} + +/// Check to make sure that neither other signed in users nor anonymous users can edit another +/// user's email address. +/// +/// If an attempt is made, update_user.rs will return an error indicating that the current user +/// does not match the requested user. +#[test] +fn test_other_users_cannot_change_my_email() { + let (app, anon, user) = TestApp::init().with_user(); + let another_user = app.db_new_user("not_me"); + let another_user_model = another_user.as_model(); + + let response = user.update_email_more_control( + another_user_model.id, + Some("pineapple@pineapples.pineapple"), + ); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response.into_json(), + json!({ "errors": [{ "detail": "current user does not match requested user" }] }) + ); + + let response = anon.update_email_more_control( + another_user_model.id, + Some("pineapple@pineapples.pineapple"), + ); + assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!( + response.into_json(), + json!({ "errors": [{ "detail": "must be logged in to perform that action" }] }) + ); +} diff --git a/src/tests/user.rs b/src/tests/user.rs index 1d7ec20643b..68da78b3203 100644 --- a/src/tests/user.rs +++ b/src/tests/user.rs @@ -1,353 +1,16 @@ use crate::{ - builders::{CrateBuilder, VersionBuilder}, new_user, - util::{MockCookieUser, RequestHelper, Response}, + util::{MockCookieUser, RequestHelper}, OkBool, TestApp, }; -use cargo_registry::{ - models::{Email, NewUser, User}, - schema::crate_owners, - views::{EncodablePrivateUser, EncodablePublicUser, EncodableVersion, OwnedCrate}, -}; - -use conduit::StatusCode; +use cargo_registry::models::{Email, NewUser, User}; use diesel::prelude::*; -#[derive(Deserialize)] -struct AuthResponse { - url: String, - state: String, -} - -#[derive(Deserialize)] -pub struct UserShowPublicResponse { - pub user: EncodablePublicUser, -} - -#[derive(Deserialize)] -pub struct UserShowPrivateResponse { - pub user: EncodablePrivateUser, - pub owned_crates: Vec, -} - -#[derive(Deserialize)] -struct UserStats { - total_downloads: i64, -} - -#[derive(Serialize)] -struct EmailNotificationsUpdate { - id: i32, - email_notifications: bool, -} - -trait MockEmailHelper: RequestHelper { - // TODO: I don't like the name of this method or `update_email` on the `MockCookieUser` impl; - // this is starting to look like a builder might help? - // I want to explore alternative abstractions in any case. - fn update_email_more_control(&self, user_id: i32, email: Option<&str>) -> Response { - // When updating your email in crates.io, the request goes to the user route with PUT. - // Ember sends all the user attributes. We check to make sure the ID in the URL matches - // the ID of the currently logged in user, then we ignore everything but the email address. - let body = json!({"user": { - "email": email, - "name": "Arbitrary Name", - "login": "arbitrary_login", - "avatar": "https://arbitrary.com/img.jpg", - "url": "https://arbitrary.com", - "kind": null - }}); - let url = format!("/api/v1/users/{user_id}"); - self.put(&url, body.to_string().as_bytes()) - } -} - impl crate::util::MockCookieUser { - fn show_me(&self) -> UserShowPrivateResponse { - let url = "/api/v1/me"; - self.get(url).good() - } - - fn update_email(&self, email: &str) -> OkBool { - let model = self.as_model(); - self.update_email_more_control(model.id, Some(email)).good() - } - fn confirm_email(&self, email_token: &str) -> OkBool { let url = format!("/api/v1/confirm/{email_token}"); self.put(&url, &[]).good() } - - fn update_email_notifications(&self, updates: Vec) -> OkBool { - self.put( - "/api/v1/me/email_notifications", - json!(updates).to_string().as_bytes(), - ) - .good() - } -} - -impl MockEmailHelper for crate::util::MockCookieUser {} -impl MockEmailHelper for crate::util::MockAnonymousUser {} - -#[test] -fn auth_gives_a_token() { - let (_, anon) = TestApp::init().empty(); - let json: AuthResponse = anon.get("/api/private/session/begin").good(); - assert!(json.url.contains(&json.state)); -} - -#[test] -fn access_token_needs_data() { - let (_, anon) = TestApp::init().empty(); - let response = anon.get::<()>("/api/private/session/authorize"); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response.into_json(), - json!({ "errors": [{ "detail": "invalid state parameter" }] }) - ); -} - -#[test] -fn me() { - let url = "/api/v1/me"; - let (app, anon) = TestApp::init().empty(); - anon.get(url).assert_forbidden(); - - let user = app.db_new_user("foo"); - let json = user.show_me(); - - assert_eq!(json.owned_crates.len(), 0); - - app.db(|conn| { - CrateBuilder::new("foo_my_packages", user.as_model().id).expect_build(conn); - assert_eq!(json.user.email, user.as_model().email(conn).unwrap()); - }); - let updated_json = user.show_me(); - - assert_eq!(updated_json.owned_crates.len(), 1); -} - -#[test] -fn show() { - let (app, anon, _) = TestApp::init().with_user(); - app.db_new_user("Bar"); - - let json: UserShowPublicResponse = anon.get("/api/v1/users/foo").good(); - assert_eq!("foo", json.user.login); - - let json: UserShowPublicResponse = anon.get("/api/v1/users/bAr").good(); - assert_eq!("Bar", json.user.login); - assert_eq!(Some("https://github.com/Bar".into()), json.user.url); -} - -#[test] -fn show_latest_user_case_insensitively() { - let (app, anon) = TestApp::init().empty(); - - app.db(|conn| { - // Please do not delete or modify the setup of this test in order to get it to pass. - // This setup mimics how GitHub works. If someone abandons a GitHub account, the username is - // available for anyone to take. We need to support having multiple user accounts - // with the same gh_login in crates.io. `gh_id` is stable across renames, so that field - // should be used for uniquely identifying GitHub accounts whenever possible. For the - // crates.io/user/:username pages, the best we can do is show the last crates.io account - // created with that username. - assert_ok!(NewUser::new( - 1, - "foobar", - Some("I was first then deleted my github account"), - None, - "bar" - ) - .create_or_update(None, &app.as_inner().emails, conn)); - assert_ok!(NewUser::new( - 2, - "FOOBAR", - Some("I was second, I took the foobar username on github"), - None, - "bar" - ) - .create_or_update(None, &app.as_inner().emails, conn)); - }); - - let json: UserShowPublicResponse = anon.get("api/v1/users/fOObAr").good(); - assert_eq!( - "I was second, I took the foobar username on github", - json.user.name.unwrap() - ); -} - -#[test] -fn crates_by_user_id() { - let (app, _, user) = TestApp::init().with_user(); - let id = user.as_model().id; - app.db(|conn| { - CrateBuilder::new("foo_my_packages", id).expect_build(conn); - }); - - let response = user.search_by_user_id(id); - assert_eq!(response.crates.len(), 1); -} - -#[test] -fn crates_by_user_id_not_including_deleted_owners() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - let krate = CrateBuilder::new("foo_my_packages", user.id).expect_build(conn); - krate - .owner_remove(app.as_inner(), conn, user, "foo") - .unwrap(); - }); - - let response = anon.search_by_user_id(user.id); - assert_eq!(response.crates.len(), 0); -} - -#[test] -fn api_token_cannot_get_user_updates() { - let (_, _, _, token) = TestApp::init().with_token(); - token.get("/api/v1/me/updates").assert_forbidden(); -} - -#[test] -fn following() { - use cargo_registry::schema::versions; - use diesel::update; - - #[derive(Deserialize)] - struct R { - versions: Vec, - meta: Meta, - } - #[derive(Deserialize)] - struct Meta { - more: bool, - } - - let (app, _, user) = TestApp::init().with_user(); - let user_model = user.as_model(); - let user_id = user_model.id; - app.db(|conn| { - CrateBuilder::new("foo_fighters", user_id) - .version(VersionBuilder::new("1.0.0")) - .expect_build(conn); - - // Make foo_fighters's version mimic a version published before we started recording who - // published versions - let none: Option = None; - update(versions::table) - .set(versions::published_by.eq(none)) - .execute(conn) - .unwrap(); - - CrateBuilder::new("bar_fighters", user_id) - .version(VersionBuilder::new("1.0.0")) - .expect_build(conn); - }); - - let r: R = user.get("/api/v1/me/updates").good(); - assert_eq!(r.versions.len(), 0); - assert!(!r.meta.more); - - user.put::("/api/v1/crates/foo_fighters/follow", b"") - .good(); - user.put::("/api/v1/crates/bar_fighters/follow", b"") - .good(); - - let r: R = user.get("/api/v1/me/updates").good(); - assert_eq!(r.versions.len(), 2); - assert!(!r.meta.more); - let foo_version = r - .versions - .iter() - .find(|v| v.krate == "foo_fighters") - .unwrap(); - assert_none!(&foo_version.published_by); - let bar_version = r - .versions - .iter() - .find(|v| v.krate == "bar_fighters") - .unwrap(); - assert_eq!( - bar_version.published_by.as_ref().unwrap().login, - user_model.gh_login - ); - - let r: R = user - .get_with_query("/api/v1/me/updates", "per_page=1") - .good(); - assert_eq!(r.versions.len(), 1); - assert!(r.meta.more); - - user.delete::("/api/v1/crates/bar_fighters/follow") - .good(); - let r: R = user - .get_with_query("/api/v1/me/updates", "page=2&per_page=1") - .good(); - assert_eq!(r.versions.len(), 0); - assert!(!r.meta.more); - - let response = user.get_with_query::<()>("/api/v1/me/updates", "page=0"); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response.into_json(), - json!({ "errors": [{ "detail": "page indexing starts from 1, page 0 is invalid" }] }) - ); -} - -#[test] -fn user_total_downloads() { - use diesel::update; - - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - let another_user = app.db_new_user("bar"); - let another_user = another_user.as_model(); - - app.db(|conn| { - let mut krate = CrateBuilder::new("foo_krate1", user.id).expect_build(conn); - krate.downloads = 10; - update(&krate).set(&krate).execute(conn).unwrap(); - - let mut krate2 = CrateBuilder::new("foo_krate2", user.id).expect_build(conn); - krate2.downloads = 20; - update(&krate2).set(&krate2).execute(conn).unwrap(); - - let mut another_krate = CrateBuilder::new("bar_krate1", another_user.id).expect_build(conn); - another_krate.downloads = 2; - update(&another_krate) - .set(&another_krate) - .execute(conn) - .unwrap(); - - let mut no_longer_my_krate = CrateBuilder::new("nacho", user.id).expect_build(conn); - no_longer_my_krate.downloads = 5; - update(&no_longer_my_krate) - .set(&no_longer_my_krate) - .execute(conn) - .unwrap(); - no_longer_my_krate - .owner_remove(app.as_inner(), conn, user, &user.gh_login) - .unwrap(); - }); - - let url = format!("/api/v1/users/{}/stats", user.id); - let stats: UserStats = anon.get(&url).good(); - // does not include crates user never owned (2) or no longer owns (5) - assert_eq!(stats.total_downloads, 30); -} - -#[test] -fn user_total_downloads_no_crates() { - let (_, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - let url = format!("/api/v1/users/{}/stats", user.id); - - let stats: UserStats = anon.get(&url).good(); - assert_eq!(stats.total_downloads, 0); } #[test] @@ -477,67 +140,6 @@ fn test_email_get_and_put() { assert!(json.user.email_verification_sent); } -/// Given a crates.io user, check to make sure that the user -/// cannot add to the database an empty string or null as -/// their email. If an attempt is made, update_user.rs will -/// return an error indicating that an empty email cannot be -/// added. -/// -/// This is checked on the frontend already, but I'd like to -/// make sure that a user cannot get around that and delete -/// their email by adding an empty string. -#[test] -fn test_empty_email_not_added() { - let (_app, _anon, user) = TestApp::init().with_user(); - let model = user.as_model(); - - let response = user.update_email_more_control(model.id, Some("")); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response.into_json(), - json!({ "errors": [{ "detail": "empty email rejected" }] }) - ); - - let response = user.update_email_more_control(model.id, None); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response.into_json(), - json!({ "errors": [{ "detail": "empty email rejected" }] }) - ); -} - -/// Check to make sure that neither other signed in users nor anonymous users can edit another -/// user's email address. -/// -/// If an attempt is made, update_user.rs will return an error indicating that the current user -/// does not match the requested user. -#[test] -fn test_other_users_cannot_change_my_email() { - let (app, anon, user) = TestApp::init().with_user(); - let another_user = app.db_new_user("not_me"); - let another_user_model = another_user.as_model(); - - let response = user.update_email_more_control( - another_user_model.id, - Some("pineapple@pineapples.pineapple"), - ); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response.into_json(), - json!({ "errors": [{ "detail": "current user does not match requested user" }] }) - ); - - let response = anon.update_email_more_control( - another_user_model.id, - Some("pineapple@pineapples.pineapple"), - ); - assert_eq!(response.status(), StatusCode::FORBIDDEN); - assert_eq!( - response.into_json(), - json!({ "errors": [{ "detail": "must be logged in to perform that action" }] }) - ); -} - /// Given a new user, test that their email can be added /// to the email table and a token for the email is generated /// and added to the token table. When /confirm/:email_token is @@ -614,134 +216,3 @@ fn test_existing_user_email() { assert!(!json.user.email_verified); assert!(!json.user.email_verification_sent); } - -#[test] -fn test_user_owned_crates_doesnt_include_deleted_ownership() { - let (app, _, user) = TestApp::init().with_user(); - let user_model = user.as_model(); - - app.db(|conn| { - let krate = CrateBuilder::new("foo_my_packages", user_model.id).expect_build(conn); - krate - .owner_remove(app.as_inner(), conn, user_model, &user_model.gh_login) - .unwrap(); - }); - - let json = user.show_me(); - assert_eq!(json.owned_crates.len(), 0); -} - -/// A user should be able to update the email notifications for crates they own. Only the crates that -/// were sent in the request should be updated to the corresponding `email_notifications` value. -#[test] -fn test_update_email_notifications() { - let (app, _, user) = TestApp::init().with_user(); - - let my_crates = app.db(|conn| { - vec![ - CrateBuilder::new("test_package", user.as_model().id).expect_build(conn), - CrateBuilder::new("another_package", user.as_model().id).expect_build(conn), - ] - }); - - let a_id = my_crates.get(0).unwrap().id; - let b_id = my_crates.get(1).unwrap().id; - - // Update crate_a: email_notifications = false - // crate_a should be false, crate_b should be true - user.update_email_notifications(vec![EmailNotificationsUpdate { - id: a_id, - email_notifications: false, - }]); - let json = user.show_me(); - - assert!( - !json - .owned_crates - .iter() - .find(|c| c.id == a_id) - .unwrap() - .email_notifications - ); - assert!( - json.owned_crates - .iter() - .find(|c| c.id == b_id) - .unwrap() - .email_notifications - ); - - // Update crate_b: email_notifications = false - // Both should be false now - user.update_email_notifications(vec![EmailNotificationsUpdate { - id: b_id, - email_notifications: false, - }]); - let json = user.show_me(); - - assert!( - !json - .owned_crates - .iter() - .find(|c| c.id == a_id) - .unwrap() - .email_notifications - ); - assert!( - !json - .owned_crates - .iter() - .find(|c| c.id == b_id) - .unwrap() - .email_notifications - ); - - // Update crate_a and crate_b: email_notifications = true - // Both should be true - user.update_email_notifications(vec![ - EmailNotificationsUpdate { - id: a_id, - email_notifications: true, - }, - EmailNotificationsUpdate { - id: b_id, - email_notifications: true, - }, - ]); - let json = user.show_me(); - - json.owned_crates.iter().for_each(|c| { - assert!(c.email_notifications); - }) -} - -/// A user should not be able to update the `email_notifications` value for a crate that is not -/// owned by them. -#[test] -fn test_update_email_notifications_not_owned() { - let (app, _, user) = TestApp::init().with_user(); - - let not_my_crate = app.db(|conn| { - let u = new_user("arbitrary_username") - .create_or_update(None, &app.as_inner().emails, conn) - .unwrap(); - CrateBuilder::new("test_package", u.id).expect_build(conn) - }); - - user.update_email_notifications(vec![EmailNotificationsUpdate { - id: not_my_crate.id, - email_notifications: false, - }]); - - let email_notifications: bool = app - .db(|conn| { - crate_owners::table - .select(crate_owners::email_notifications) - .filter(crate_owners::crate_id.eq(not_my_crate.id)) - .first(conn) - }) - .unwrap(); - - // There should be no change to the `email_notifications` value for a crate not belonging to me - assert!(email_notifications); -}