diff --git a/src/tests/all.rs b/src/tests/all.rs index 736321be9d7..3e7fbb9e446 100644 --- a/src/tests/all.rs +++ b/src/tests/all.rs @@ -32,15 +32,14 @@ mod authentication; mod blocked_routes; mod builders; mod categories; -mod category; mod dump_db; -mod keyword; mod krate; -mod metrics; mod not_found_error; mod owners; +mod pagination; mod read_only_mode; mod record; +mod routes; mod schema_details; mod server; mod server_binary; diff --git a/src/tests/http-data/version_daily_limit b/src/tests/http-data/routes_crates_new_daily_limit similarity index 100% rename from src/tests/http-data/version_daily_limit rename to src/tests/http-data/routes_crates_new_daily_limit diff --git a/src/tests/http-data/version_version_size b/src/tests/http-data/routes_crates_read_version_size similarity index 100% rename from src/tests/http-data/version_version_size rename to src/tests/http-data/routes_crates_read_version_size diff --git a/src/tests/krate/following.rs b/src/tests/krate/following.rs index 6e856bd468a..935c889e5e6 100644 --- a/src/tests/krate/following.rs +++ b/src/tests/krate/following.rs @@ -2,14 +2,6 @@ use crate::builders::CrateBuilder; use crate::util::{RequestHelper, TestApp}; use crate::OkBool; -#[test] -fn diesel_not_found_results_in_404() { - let (_, _, user) = TestApp::init().with_user(); - - user.get("/api/v1/crates/foo_following/following") - .assert_not_found(); -} - #[test] fn following() { // TODO: Test anon requests as well? @@ -58,23 +50,6 @@ fn following() { assert_eq!(user.search("following=1").crates.len(), 0); } -#[test] -fn disallow_api_token_auth_for_get_crate_following_status() { - let (app, _, _, token) = TestApp::init().with_token(); - let api_token = token.as_model(); - - let a_crate = "a_crate"; - - app.db(|conn| { - CrateBuilder::new(a_crate, api_token.user_id).expect_build(conn); - }); - - // Token auth on GET for get following status is disallowed - token - .get(&format!("/api/v1/crates/{a_crate}/following")) - .assert_forbidden(); -} - #[test] fn getting_followed_crates_allows_api_token_auth() { let (app, _, user, token) = TestApp::init().with_token(); diff --git a/src/tests/krate/mod.rs b/src/tests/krate/mod.rs index 29140ce05a9..ed2b29ea63b 100644 --- a/src/tests/krate/mod.rs +++ b/src/tests/krate/mod.rs @@ -1,11 +1,4 @@ -mod dependencies; -mod downloads; mod following; -mod owners; mod publish; -mod reverse_dependencies; -mod search; -mod show; -mod summary; mod versions; mod yanking; diff --git a/src/tests/krate/publish.rs b/src/tests/krate/publish.rs index dbd4556091e..39d5621568e 100644 --- a/src/tests/krate/publish.rs +++ b/src/tests/krate/publish.rs @@ -197,7 +197,7 @@ fn new_with_underscore_renamed_dependency() { #[test] fn new_krate_with_dependency() { - use super::dependencies::Deps; + use crate::routes::crates::versions::dependencies::Deps; let (app, anon, user, token) = TestApp::full().with_token(); diff --git a/src/tests/pagination.rs b/src/tests/pagination.rs new file mode 100644 index 00000000000..6a6228a130b --- /dev/null +++ b/src/tests/pagination.rs @@ -0,0 +1,29 @@ +use crate::builders::CrateBuilder; +use crate::util::{RequestHelper, TestApp}; +use http::status::StatusCode; +use ipnetwork::IpNetwork; +use serde_json::json; + +#[test] +fn pagination_blocks_ip_from_cidr_block_list() { + let (app, anon, user) = TestApp::init() + .with_config(|config| { + config.max_allowed_page_offset = 1; + config.page_offset_cidr_blocklist = vec!["127.0.0.1/24".parse::().unwrap()]; + }) + .with_user(); + let user = user.as_model(); + + app.db(|conn| { + CrateBuilder::new("pagination_links_1", user.id).expect_build(conn); + CrateBuilder::new("pagination_links_2", user.id).expect_build(conn); + CrateBuilder::new("pagination_links_3", user.id).expect_build(conn); + }); + + let response = anon.get_with_query::<()>("/api/v1/crates", "page=2&per_page=1"); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response.into_json(), + json!({ "errors": [{ "detail": "requested page offset is too large" }] }) + ); +} diff --git a/src/tests/category.rs b/src/tests/routes/categories/get.rs similarity index 74% rename from src/tests/category.rs rename to src/tests/routes/categories/get.rs index c171cd94b6c..c5b5d98dc85 100644 --- a/src/tests/category.rs +++ b/src/tests/routes/categories/get.rs @@ -1,35 +1,10 @@ -use crate::util::insta::assert_yaml_snapshot; -use crate::{ - builders::CrateBuilder, new_category, util::MockAnonymousUser, RequestHelper, TestApp, -}; +use crate::builders::CrateBuilder; +use crate::new_category; +use crate::util::{MockAnonymousUser, RequestHelper, TestApp}; use cargo_registry::models::Category; +use insta::assert_yaml_snapshot; use serde_json::Value; -#[test] -fn index() { - let (app, anon) = TestApp::init().empty(); - - // List 0 categories if none exist - let json: Value = anon.get("/api/v1/categories").good(); - assert_yaml_snapshot!(json); - - // Create a category and a subcategory - app.db(|conn| { - new_category("foo", "foo", "Foo crates") - .create_or_update(conn) - .unwrap(); - new_category("foo::bar", "foo::bar", "Bar crates") - .create_or_update(conn) - .unwrap(); - }); - - // Only the top-level categories should be on the page - let json: Value = anon.get("/api/v1/categories").good(); - assert_yaml_snapshot!(json, { - ".categories[].created_at" => "[datetime]", - }); -} - #[test] fn show() { let (app, anon) = TestApp::init().empty(); @@ -129,19 +104,3 @@ fn update_crate() { assert_eq!(count(&anon, "category-2"), 0); }); } - -#[test] -fn category_slugs_returns_all_slugs_in_alphabetical_order() { - let (app, anon) = TestApp::init().empty(); - app.db(|conn| { - new_category("Foo", "foo", "For crates that foo") - .create_or_update(conn) - .unwrap(); - new_category("Bar", "bar", "For crates that bar") - .create_or_update(conn) - .unwrap(); - }); - - let response: Value = anon.get("/api/v1/category_slugs").good(); - assert_yaml_snapshot!(response); -} diff --git a/src/tests/routes/categories/list.rs b/src/tests/routes/categories/list.rs new file mode 100644 index 00000000000..40945cafb8f --- /dev/null +++ b/src/tests/routes/categories/list.rs @@ -0,0 +1,29 @@ +use crate::new_category; +use crate::util::{RequestHelper, TestApp}; +use insta::assert_yaml_snapshot; +use serde_json::Value; + +#[test] +fn index() { + let (app, anon) = TestApp::init().empty(); + + // List 0 categories if none exist + let json: Value = anon.get("/api/v1/categories").good(); + assert_yaml_snapshot!(json); + + // Create a category and a subcategory + app.db(|conn| { + new_category("foo", "foo", "Foo crates") + .create_or_update(conn) + .unwrap(); + new_category("foo::bar", "foo::bar", "Bar crates") + .create_or_update(conn) + .unwrap(); + }); + + // Only the top-level categories should be on the page + let json: Value = anon.get("/api/v1/categories").good(); + assert_yaml_snapshot!(json, { + ".categories[].created_at" => "[datetime]", + }); +} diff --git a/src/tests/routes/categories/mod.rs b/src/tests/routes/categories/mod.rs new file mode 100644 index 00000000000..89d961bc20f --- /dev/null +++ b/src/tests/routes/categories/mod.rs @@ -0,0 +1,2 @@ +pub mod get; +pub mod list; diff --git a/src/tests/snapshots/all__category__show.snap b/src/tests/routes/categories/snapshots/all__routes__categories__get__show.snap similarity index 85% rename from src/tests/snapshots/all__category__show.snap rename to src/tests/routes/categories/snapshots/all__routes__categories__get__show.snap index 307a11f1f5c..b23a9943ce8 100644 --- a/src/tests/snapshots/all__category__show.snap +++ b/src/tests/routes/categories/snapshots/all__routes__categories__get__show.snap @@ -1,6 +1,6 @@ --- -source: src/tests/category.rs -assertion_line: 51 +source: src/tests/routes/categories/get.rs +assertion_line: 26 expression: json --- category: diff --git a/src/tests/snapshots/all__category__index-2.snap b/src/tests/routes/categories/snapshots/all__routes__categories__list__index-2.snap similarity index 73% rename from src/tests/snapshots/all__category__index-2.snap rename to src/tests/routes/categories/snapshots/all__routes__categories__list__index-2.snap index 32962b3c3d9..fb27ac38dd0 100644 --- a/src/tests/snapshots/all__category__index-2.snap +++ b/src/tests/routes/categories/snapshots/all__routes__categories__list__index-2.snap @@ -1,6 +1,6 @@ --- -source: src/tests/category.rs -assertion_line: 33 +source: src/tests/routes/categories/list.rs +assertion_line: 26 expression: json --- categories: diff --git a/src/tests/routes/categories/snapshots/all__routes__categories__list__index.snap b/src/tests/routes/categories/snapshots/all__routes__categories__list__index.snap new file mode 100644 index 00000000000..11e3d265390 --- /dev/null +++ b/src/tests/routes/categories/snapshots/all__routes__categories__list__index.snap @@ -0,0 +1,9 @@ +--- +source: src/tests/routes/categories/list.rs +assertion_line: 12 +expression: json +--- +categories: [] +meta: + total: 0 + diff --git a/src/tests/routes/category_slugs/list.rs b/src/tests/routes/category_slugs/list.rs new file mode 100644 index 00000000000..cb94fdc871a --- /dev/null +++ b/src/tests/routes/category_slugs/list.rs @@ -0,0 +1,20 @@ +use crate::new_category; +use crate::util::{RequestHelper, TestApp}; +use insta::assert_yaml_snapshot; +use serde_json::Value; + +#[test] +fn category_slugs_returns_all_slugs_in_alphabetical_order() { + let (app, anon) = TestApp::init().empty(); + app.db(|conn| { + new_category("Foo", "foo", "For crates that foo") + .create_or_update(conn) + .unwrap(); + new_category("Bar", "bar", "For crates that bar") + .create_or_update(conn) + .unwrap(); + }); + + let response: Value = anon.get("/api/v1/category_slugs").good(); + assert_yaml_snapshot!(response); +} diff --git a/src/tests/routes/category_slugs/mod.rs b/src/tests/routes/category_slugs/mod.rs new file mode 100644 index 00000000000..d17e233fbff --- /dev/null +++ b/src/tests/routes/category_slugs/mod.rs @@ -0,0 +1 @@ +pub mod list; diff --git a/src/tests/snapshots/all__category__category_slugs_returns_all_slugs_in_alphabetical_order.snap b/src/tests/routes/category_slugs/snapshots/all__routes__category_slugs__list__category_slugs_returns_all_slugs_in_alphabetical_order.snap similarity index 71% rename from src/tests/snapshots/all__category__category_slugs_returns_all_slugs_in_alphabetical_order.snap rename to src/tests/routes/category_slugs/snapshots/all__routes__category_slugs__list__category_slugs_returns_all_slugs_in_alphabetical_order.snap index 3ee47b04deb..1a80e00f95d 100644 --- a/src/tests/snapshots/all__category__category_slugs_returns_all_slugs_in_alphabetical_order.snap +++ b/src/tests/routes/category_slugs/snapshots/all__routes__category_slugs__list__category_slugs_returns_all_slugs_in_alphabetical_order.snap @@ -1,6 +1,6 @@ --- -source: src/tests/category.rs -assertion_line: 153 +source: src/tests/routes/category_slugs/list.rs +assertion_line: 19 expression: response --- category_slugs: diff --git a/src/tests/routes/crates/downloads.rs b/src/tests/routes/crates/downloads.rs new file mode 100644 index 00000000000..a86b54c14f6 --- /dev/null +++ b/src/tests/routes/crates/downloads.rs @@ -0,0 +1,83 @@ +use crate::builders::{CrateBuilder, VersionBuilder}; +use crate::util::{MockAnonymousUser, RequestHelper, TestApp}; +use cargo_registry::views::EncodableVersionDownload; +use chrono::{Duration, Utc}; +use http::StatusCode; + +#[derive(Deserialize)] +struct Downloads { + version_downloads: Vec, +} + +pub fn persist_downloads_count(app: &TestApp) { + app.as_inner() + .downloads_counter + .persist_all_shards(app.as_inner()) + .expect("failed to persist downloads count") + .log(); +} + +#[track_caller] +pub fn assert_dl_count( + anon: &MockAnonymousUser, + name_and_version: &str, + query: Option<&str>, + count: i32, +) { + let url = format!("/api/v1/crates/{name_and_version}/downloads"); + let downloads: Downloads = if let Some(query) = query { + anon.get_with_query(&url, query).good() + } else { + anon.get(&url).good() + }; + let total_downloads = downloads + .version_downloads + .iter() + .map(|vd| vd.downloads) + .sum::(); + assert_eq!(total_downloads, count); +} + +#[test] +fn download() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + CrateBuilder::new("foo_download", user.id) + .version(VersionBuilder::new("1.0.0")) + .expect_build(conn); + }); + + let download = |name_and_version: &str| { + let url = format!("/api/v1/crates/{name_and_version}/download"); + let response = anon.get::<()>(&url); + assert_eq!(response.status(), StatusCode::FOUND); + // TODO: test the with_json code path + }; + + download("foo_download/1.0.0"); + // No downloads are counted until the counters are persisted + assert_dl_count(&anon, "foo_download/1.0.0", None, 0); + assert_dl_count(&anon, "foo_download", None, 0); + persist_downloads_count(&app); + // Now that the counters are persisted the download counts show up. + assert_dl_count(&anon, "foo_download/1.0.0", None, 1); + assert_dl_count(&anon, "foo_download", None, 1); + + download("FOO_DOWNLOAD/1.0.0"); + persist_downloads_count(&app); + assert_dl_count(&anon, "FOO_DOWNLOAD/1.0.0", None, 2); + assert_dl_count(&anon, "FOO_DOWNLOAD", None, 2); + + let yesterday = (Utc::now().date_naive() + Duration::days(-1)).format("%F"); + let query = format!("before_date={yesterday}"); + assert_dl_count(&anon, "FOO_DOWNLOAD/1.0.0", Some(&query), 0); + // crate/downloads always returns the last 90 days and ignores date params + assert_dl_count(&anon, "FOO_DOWNLOAD", Some(&query), 2); + + let tomorrow = (Utc::now().date_naive() + Duration::days(1)).format("%F"); + let query = format!("before_date={tomorrow}"); + assert_dl_count(&anon, "FOO_DOWNLOAD/1.0.0", Some(&query), 2); + assert_dl_count(&anon, "FOO_DOWNLOAD", Some(&query), 2); +} diff --git a/src/tests/routes/crates/following.rs b/src/tests/routes/crates/following.rs new file mode 100644 index 00000000000..daaab20143c --- /dev/null +++ b/src/tests/routes/crates/following.rs @@ -0,0 +1,27 @@ +use crate::builders::CrateBuilder; +use crate::util::{RequestHelper, TestApp}; + +#[test] +fn diesel_not_found_results_in_404() { + let (_, _, user) = TestApp::init().with_user(); + + user.get("/api/v1/crates/foo_following/following") + .assert_not_found(); +} + +#[test] +fn disallow_api_token_auth_for_get_crate_following_status() { + let (app, _, _, token) = TestApp::init().with_token(); + let api_token = token.as_model(); + + let a_crate = "a_crate"; + + app.db(|conn| { + CrateBuilder::new(a_crate, api_token.user_id).expect_build(conn); + }); + + // Token auth on GET for get following status is disallowed + token + .get(&format!("/api/v1/crates/{a_crate}/following")) + .assert_forbidden(); +} diff --git a/src/tests/krate/search.rs b/src/tests/routes/crates/list.rs similarity index 96% rename from src/tests/krate/search.rs rename to src/tests/routes/crates/list.rs index 57381e166b8..5e87d9d5128 100644 --- a/src/tests/krate/search.rs +++ b/src/tests/routes/crates/list.rs @@ -5,7 +5,6 @@ use cargo_registry::models::Category; use cargo_registry::schema::crates; use diesel::{dsl::*, prelude::*, update}; use http::StatusCode; -use ipnetwork::IpNetwork; #[test] fn index() { @@ -823,27 +822,3 @@ fn pagination_parameters_only_accept_integers() { json!({ "errors": [{ "detail": "invalid digit found in string" }] }) ); } - -#[test] -fn pagination_blocks_ip_from_cidr_block_list() { - let (app, anon, user) = TestApp::init() - .with_config(|config| { - config.max_allowed_page_offset = 1; - config.page_offset_cidr_blocklist = vec!["127.0.0.1/24".parse::().unwrap()]; - }) - .with_user(); - let user = user.as_model(); - - app.db(|conn| { - CrateBuilder::new("pagination_links_1", user.id).expect_build(conn); - CrateBuilder::new("pagination_links_2", user.id).expect_build(conn); - CrateBuilder::new("pagination_links_3", user.id).expect_build(conn); - }); - - let response = anon.get_with_query::<()>("/api/v1/crates", "page=2&per_page=1"); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response.into_json(), - json!({ "errors": [{ "detail": "requested page offset is too large" }] }) - ); -} diff --git a/src/tests/routes/crates/mod.rs b/src/tests/routes/crates/mod.rs new file mode 100644 index 00000000000..2619dcbfb94 --- /dev/null +++ b/src/tests/routes/crates/mod.rs @@ -0,0 +1,8 @@ +pub mod downloads; +mod following; +mod list; +mod new; +pub mod owners; +mod read; +mod reverse_dependencies; +pub mod versions; diff --git a/src/tests/routes/crates/new.rs b/src/tests/routes/crates/new.rs new file mode 100644 index 00000000000..8bcec03390d --- /dev/null +++ b/src/tests/routes/crates/new.rs @@ -0,0 +1,23 @@ +use crate::builders::PublishBuilder; +use crate::util::{RequestHelper, TestApp}; + +#[test] +fn daily_limit() { + let (app, _, user) = TestApp::full().with_user(); + + let max_daily_versions = app.as_inner().config.new_version_rate_limit.unwrap(); + for version in 1..=max_daily_versions { + let crate_to_publish = + PublishBuilder::new("foo_daily_limit").version(&format!("0.0.{}", version)); + user.publish_crate(crate_to_publish).good(); + } + + let crate_to_publish = PublishBuilder::new("foo_daily_limit").version("1.0.0"); + let response = user.publish_crate(crate_to_publish); + assert!(response.status().is_success()); + let json = response.into_json(); + assert_eq!( + json["errors"][0]["detail"], + "You have published too many versions of this crate in the last 24 hours" + ); +} diff --git a/src/tests/krate/owners.rs b/src/tests/routes/crates/owners/add.rs similarity index 100% rename from src/tests/krate/owners.rs rename to src/tests/routes/crates/owners/add.rs diff --git a/src/tests/routes/crates/owners/mod.rs b/src/tests/routes/crates/owners/mod.rs new file mode 100644 index 00000000000..d79c5db527a --- /dev/null +++ b/src/tests/routes/crates/owners/mod.rs @@ -0,0 +1 @@ +mod add; diff --git a/src/tests/krate/show.rs b/src/tests/routes/crates/read.rs similarity index 80% rename from src/tests/krate/show.rs rename to src/tests/routes/crates/read.rs index 49d89a90b10..4c34b283f9c 100644 --- a/src/tests/krate/show.rs +++ b/src/tests/routes/crates/read.rs @@ -1,4 +1,4 @@ -use crate::builders::{CrateBuilder, VersionBuilder}; +use crate::builders::{CrateBuilder, PublishBuilder, VersionBuilder}; use crate::util::{RequestHelper, TestApp}; use diesel::prelude::*; @@ -116,6 +116,41 @@ fn show_minimal() { assert!(json.keywords.is_none()); } +#[test] +fn version_size() { + let (_, _, user) = TestApp::full().with_user(); + + let crate_to_publish = PublishBuilder::new("foo_version_size").version("1.0.0"); + user.publish_crate(crate_to_publish).good(); + + // Add a file to version 2 so that it's a different size than version 1 + let files = [("foo_version_size-2.0.0/big", &[b'a'; 1] as &[_])]; + let crate_to_publish = PublishBuilder::new("foo_version_size") + .version("2.0.0") + .files(&files); + user.publish_crate(crate_to_publish).good(); + + let crate_json = user.show_crate("foo_version_size"); + + let version1 = crate_json + .versions + .as_ref() + .unwrap() + .iter() + .find(|v| v.num == "1.0.0") + .expect("Could not find v1.0.0"); + assert_eq!(version1.crate_size, Some(35)); + + let version2 = crate_json + .versions + .as_ref() + .unwrap() + .iter() + .find(|v| v.num == "2.0.0") + .expect("Could not find v2.0.0"); + assert_eq!(version2.crate_size, Some(91)); +} + #[test] fn block_bad_documentation_url() { let (app, anon, user) = TestApp::init().with_user(); diff --git a/src/tests/krate/reverse_dependencies.rs b/src/tests/routes/crates/reverse_dependencies.rs similarity index 100% rename from src/tests/krate/reverse_dependencies.rs rename to src/tests/routes/crates/reverse_dependencies.rs diff --git a/src/tests/routes/crates/versions/authors.rs b/src/tests/routes/crates/versions/authors.rs new file mode 100644 index 00000000000..88820fe6830 --- /dev/null +++ b/src/tests/routes/crates/versions/authors.rs @@ -0,0 +1,20 @@ +use crate::builders::CrateBuilder; +use crate::util::{RequestHelper, TestApp}; +use insta::assert_yaml_snapshot; +use serde_json::Value; + +#[test] +fn authors() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + CrateBuilder::new("foo_authors", user.id) + .version("1.0.0") + .expect_build(conn); + }); + + let json: Value = anon.get("/api/v1/crates/foo_authors/1.0.0/authors").good(); + let json = json.as_object().unwrap(); + assert_yaml_snapshot!(json); +} diff --git a/src/tests/krate/dependencies.rs b/src/tests/routes/crates/versions/dependencies.rs similarity index 100% rename from src/tests/krate/dependencies.rs rename to src/tests/routes/crates/versions/dependencies.rs diff --git a/src/tests/krate/downloads.rs b/src/tests/routes/crates/versions/download.rs similarity index 54% rename from src/tests/krate/downloads.rs rename to src/tests/routes/crates/versions/download.rs index 45b81d9191b..8e568e306a9 100644 --- a/src/tests/krate/downloads.rs +++ b/src/tests/routes/crates/versions/download.rs @@ -1,86 +1,5 @@ use crate::builders::{CrateBuilder, VersionBuilder}; -use crate::util::{MockAnonymousUser, RequestHelper, TestApp}; -use cargo_registry::views::EncodableVersionDownload; -use chrono::{Duration, Utc}; -use http::StatusCode; - -#[derive(Deserialize)] -struct Downloads { - version_downloads: Vec, -} - -fn persist_downloads_count(app: &TestApp) { - app.as_inner() - .downloads_counter - .persist_all_shards(app.as_inner()) - .expect("failed to persist downloads count") - .log(); -} - -#[track_caller] -fn assert_dl_count( - anon: &MockAnonymousUser, - name_and_version: &str, - query: Option<&str>, - count: i32, -) { - let url = format!("/api/v1/crates/{name_and_version}/downloads"); - let downloads: Downloads = if let Some(query) = query { - anon.get_with_query(&url, query).good() - } else { - anon.get(&url).good() - }; - let total_downloads = downloads - .version_downloads - .iter() - .map(|vd| vd.downloads) - .sum::(); - assert_eq!(total_downloads, count); -} - -#[test] -fn download() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - CrateBuilder::new("foo_download", user.id) - .version(VersionBuilder::new("1.0.0")) - .expect_build(conn); - }); - - let download = |name_and_version: &str| { - let url = format!("/api/v1/crates/{name_and_version}/download"); - let response = anon.get::<()>(&url); - assert_eq!(response.status(), StatusCode::FOUND); - // TODO: test the with_json code path - }; - - download("foo_download/1.0.0"); - // No downloads are counted until the counters are persisted - assert_dl_count(&anon, "foo_download/1.0.0", None, 0); - assert_dl_count(&anon, "foo_download", None, 0); - persist_downloads_count(&app); - // Now that the counters are persisted the download counts show up. - assert_dl_count(&anon, "foo_download/1.0.0", None, 1); - assert_dl_count(&anon, "foo_download", None, 1); - - download("FOO_DOWNLOAD/1.0.0"); - persist_downloads_count(&app); - assert_dl_count(&anon, "FOO_DOWNLOAD/1.0.0", None, 2); - assert_dl_count(&anon, "FOO_DOWNLOAD", None, 2); - - let yesterday = (Utc::now().date_naive() + Duration::days(-1)).format("%F"); - let query = format!("before_date={yesterday}"); - assert_dl_count(&anon, "FOO_DOWNLOAD/1.0.0", Some(&query), 0); - // crate/downloads always returns the last 90 days and ignores date params - assert_dl_count(&anon, "FOO_DOWNLOAD", Some(&query), 2); - - let tomorrow = (Utc::now().date_naive() + Duration::days(1)).format("%F"); - let query = format!("before_date={tomorrow}"); - assert_dl_count(&anon, "FOO_DOWNLOAD/1.0.0", Some(&query), 2); - assert_dl_count(&anon, "FOO_DOWNLOAD", Some(&query), 2); -} +use crate::util::{RequestHelper, TestApp}; #[test] fn download_nonexistent_version_of_existing_crate_404s() { @@ -145,6 +64,7 @@ fn force_unconditional_redirect() { #[test] fn download_caches_version_id() { + use super::super::downloads; use cargo_registry::schema::crates::dsl::*; use diesel::prelude::*; @@ -173,7 +93,7 @@ fn download_caches_version_id() { .assert_redirect_ends_with("/crates/foo_download/foo_download-1.0.0.crate"); // Downloads are persisted by version_id, so the rename doesn't matter - persist_downloads_count(&app); + downloads::persist_downloads_count(&app); // Check download count against the new name, rather than rename it back to the original value - assert_dl_count(&anon, "other/1.0.0", None, 2); + downloads::assert_dl_count(&anon, "other/1.0.0", None, 2); } diff --git a/src/tests/routes/crates/versions/mod.rs b/src/tests/routes/crates/versions/mod.rs new file mode 100644 index 00000000000..e390cf98a90 --- /dev/null +++ b/src/tests/routes/crates/versions/mod.rs @@ -0,0 +1,4 @@ +mod authors; +pub mod dependencies; +pub mod download; +mod read; diff --git a/src/tests/routes/crates/versions/read.rs b/src/tests/routes/crates/versions/read.rs new file mode 100644 index 00000000000..f474a7153b9 --- /dev/null +++ b/src/tests/routes/crates/versions/read.rs @@ -0,0 +1,59 @@ +use crate::builders::{CrateBuilder, VersionBuilder}; +use crate::util::insta::{self, assert_yaml_snapshot}; +use crate::util::{RequestHelper, TestApp}; +use diesel::prelude::*; +use serde_json::Value; + +#[test] +fn show_by_crate_name_and_version() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + let v = app.db(|conn| { + let krate = CrateBuilder::new("foo_vers_show", user.id).expect_build(conn); + VersionBuilder::new("2.0.0") + .size(1234) + .checksum("c241cd77c3723ccf1aa453f169ee60c0a888344da504bee0142adb859092acb4") + .expect_build(krate.id, user.id, conn) + }); + + let url = "/api/v1/crates/foo_vers_show/2.0.0"; + let json: Value = anon.get(url).good(); + assert_yaml_snapshot!(json, { + ".version.id" => insta::id_redaction(v.id), + ".version.created_at" => "[datetime]", + ".version.updated_at" => "[datetime]", + ".version.published_by.id" => insta::id_redaction(user.id), + }); +} + +#[test] +fn show_by_crate_name_and_semver_no_published_by() { + use cargo_registry::schema::versions; + use diesel::{update, RunQueryDsl}; + + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + let v = app.db(|conn| { + let krate = CrateBuilder::new("foo_vers_show_no_pb", user.id).expect_build(conn); + let version = VersionBuilder::new("1.0.0").expect_build(krate.id, user.id, conn); + + // 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(); + + version + }); + + let url = "/api/v1/crates/foo_vers_show_no_pb/1.0.0"; + let json: Value = anon.get(url).good(); + assert_yaml_snapshot!(json, { + ".version.id" => insta::id_redaction(v.id), + ".version.created_at" => "[datetime]", + ".version.updated_at" => "[datetime]", + }); +} diff --git a/src/tests/routes/crates/versions/snapshots/all__routes__crates__versions__authors__authors.snap b/src/tests/routes/crates/versions/snapshots/all__routes__crates__versions__authors__authors.snap new file mode 100644 index 00000000000..8aff7f8dc0e --- /dev/null +++ b/src/tests/routes/crates/versions/snapshots/all__routes__crates__versions__authors__authors.snap @@ -0,0 +1,9 @@ +--- +source: src/tests/routes/crates/versions/authors.rs +assertion_line: 19 +expression: json +--- +meta: + names: [] +users: [] + diff --git a/src/tests/snapshots/all__version__show_by_crate_name_and_semver_no_published_by.snap b/src/tests/routes/crates/versions/snapshots/all__routes__crates__versions__read__show_by_crate_name_and_semver_no_published_by.snap similarity index 90% rename from src/tests/snapshots/all__version__show_by_crate_name_and_semver_no_published_by.snap rename to src/tests/routes/crates/versions/snapshots/all__routes__crates__versions__read__show_by_crate_name_and_semver_no_published_by.snap index 627fbce4127..3b3264633b0 100644 --- a/src/tests/snapshots/all__version__show_by_crate_name_and_semver_no_published_by.snap +++ b/src/tests/routes/crates/versions/snapshots/all__routes__crates__versions__read__show_by_crate_name_and_semver_no_published_by.snap @@ -1,6 +1,6 @@ --- -source: src/tests/version.rs -assertion_line: 108 +source: src/tests/routes/crates/versions/read.rs +assertion_line: 54 expression: json --- version: diff --git a/src/tests/snapshots/all__version__show_by_crate_name_and_version.snap b/src/tests/routes/crates/versions/snapshots/all__routes__crates__versions__read__show_by_crate_name_and_version.snap similarity index 91% rename from src/tests/snapshots/all__version__show_by_crate_name_and_version.snap rename to src/tests/routes/crates/versions/snapshots/all__routes__crates__versions__read__show_by_crate_name_and_version.snap index 57e1bd6676b..cffbee1e14a 100644 --- a/src/tests/snapshots/all__version__show_by_crate_name_and_version.snap +++ b/src/tests/routes/crates/versions/snapshots/all__routes__crates__versions__read__show_by_crate_name_and_version.snap @@ -1,6 +1,6 @@ --- -source: src/tests/version.rs -assertion_line: 78 +source: src/tests/routes/crates/versions/read.rs +assertion_line: 22 expression: json --- version: diff --git a/src/tests/routes/keywords/list.rs b/src/tests/routes/keywords/list.rs new file mode 100644 index 00000000000..1b80953f61c --- /dev/null +++ b/src/tests/routes/keywords/list.rs @@ -0,0 +1,32 @@ +use crate::util::{RequestHelper, TestApp}; +use cargo_registry::models::Keyword; +use cargo_registry::views::EncodableKeyword; + +#[derive(Deserialize)] +struct KeywordList { + keywords: Vec, + meta: KeywordMeta, +} + +#[derive(Deserialize)] +struct KeywordMeta { + total: i32, +} + +#[test] +fn index() { + let url = "/api/v1/keywords"; + let (app, anon) = TestApp::init().empty(); + let json: KeywordList = anon.get(url).good(); + assert_eq!(json.keywords.len(), 0); + assert_eq!(json.meta.total, 0); + + app.db(|conn| { + Keyword::find_or_create_all(conn, &["foo"]).unwrap(); + }); + + let json: KeywordList = anon.get(url).good(); + assert_eq!(json.keywords.len(), 1); + assert_eq!(json.meta.total, 1); + assert_eq!(json.keywords[0].keyword.as_str(), "foo"); +} diff --git a/src/tests/routes/keywords/mod.rs b/src/tests/routes/keywords/mod.rs new file mode 100644 index 00000000000..3695ac973cb --- /dev/null +++ b/src/tests/routes/keywords/mod.rs @@ -0,0 +1,2 @@ +mod list; +mod read; diff --git a/src/tests/keyword.rs b/src/tests/routes/keywords/read.rs similarity index 72% rename from src/tests/keyword.rs rename to src/tests/routes/keywords/read.rs index 43e33e57e52..a161f2e5013 100644 --- a/src/tests/keyword.rs +++ b/src/tests/routes/keywords/read.rs @@ -1,38 +1,13 @@ -use crate::{builders::CrateBuilder, RequestHelper, TestApp}; -use cargo_registry::{models::Keyword, views::EncodableKeyword}; +use crate::builders::CrateBuilder; +use crate::util::{RequestHelper, TestApp}; +use cargo_registry::models::Keyword; +use cargo_registry::views::EncodableKeyword; -#[derive(Deserialize)] -struct KeywordList { - keywords: Vec, - meta: KeywordMeta, -} -#[derive(Deserialize)] -struct KeywordMeta { - total: i32, -} #[derive(Deserialize)] struct GoodKeyword { keyword: EncodableKeyword, } -#[test] -fn index() { - let url = "/api/v1/keywords"; - let (app, anon) = TestApp::init().empty(); - let json: KeywordList = anon.get(url).good(); - assert_eq!(json.keywords.len(), 0); - assert_eq!(json.meta.total, 0); - - app.db(|conn| { - Keyword::find_or_create_all(conn, &["foo"]).unwrap(); - }); - - let json: KeywordList = anon.get(url).good(); - assert_eq!(json.keywords.len(), 1); - assert_eq!(json.meta.total, 1); - assert_eq!(json.keywords[0].keyword.as_str(), "foo"); -} - #[test] fn show() { let url = "/api/v1/keywords/foo"; diff --git a/src/tests/routes/me/mod.rs b/src/tests/routes/me/mod.rs new file mode 100644 index 00000000000..5c76635567e --- /dev/null +++ b/src/tests/routes/me/mod.rs @@ -0,0 +1 @@ +pub mod tokens; diff --git a/src/tests/routes/me/tokens/create.rs b/src/tests/routes/me/tokens/create.rs new file mode 100644 index 00000000000..8eb5fafe240 --- /dev/null +++ b/src/tests/routes/me/tokens/create.rs @@ -0,0 +1,121 @@ +use crate::util::{RequestHelper, TestApp}; +use cargo_registry::models::ApiToken; +use cargo_registry::views::EncodableApiTokenWithToken; +use diesel::prelude::*; +use http::StatusCode; + +static NEW_BAR: &[u8] = br#"{ "api_token": { "name": "bar" } }"#; + +#[derive(Deserialize)] +struct NewResponse { + api_token: EncodableApiTokenWithToken, +} + +#[test] +fn create_token_logged_out() { + let (_, anon) = TestApp::init().empty(); + anon.put("/api/v1/me/tokens", NEW_BAR).assert_forbidden(); +} + +#[test] +fn create_token_invalid_request() { + let (_, _, user) = TestApp::init().with_user(); + let invalid = br#"{ "name": "" }"#; + let response = user.put::<()>("/api/v1/me/tokens", invalid); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response.into_json(), + json!({ "errors": [{ "detail": "invalid new token request: Error(\"missing field `api_token`\", line: 1, column: 14)" }] }) + ); +} + +#[test] +fn create_token_no_name() { + let (_, _, user) = TestApp::init().with_user(); + let empty_name = br#"{ "api_token": { "name": "" } }"#; + let response = user.put::<()>("/api/v1/me/tokens", empty_name); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response.into_json(), + json!({ "errors": [{ "detail": "name must have a value" }] }) + ); +} + +#[test] +fn create_token_long_body() { + let (_, _, user) = TestApp::init().with_user(); + let too_big = &[5; 5192]; // Send a request with a 5kB body of 5's + let response = user.put::<()>("/api/v1/me/tokens", too_big); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response.into_json(), + json!({ "errors": [{ "detail": "max content length is: 2000" }] }) + ); +} + +#[test] +fn create_token_exceeded_tokens_per_user() { + let (app, _, user) = TestApp::init().with_user(); + let id = user.as_model().id; + app.db(|conn| { + for i in 0..1000 { + assert_ok!(ApiToken::insert(conn, id, &format!("token {i}"))); + } + }); + let response = user.put::<()>("/api/v1/me/tokens", NEW_BAR); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response.into_json(), + json!({ "errors": [{ "detail": "maximum tokens per user is: 500" }] }) + ); +} + +#[test] +fn create_token_success() { + let (app, _, user) = TestApp::init().with_user(); + + let json: NewResponse = user.put("/api/v1/me/tokens", NEW_BAR).good(); + assert_eq!(json.api_token.name, "bar"); + assert!(!json.api_token.token.is_empty()); + + let tokens: Vec = + app.db(|conn| assert_ok!(ApiToken::belonging_to(user.as_model()).load(conn))); + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0].name, "bar"); + assert!(!tokens[0].revoked); + assert_eq!(tokens[0].last_used_at, None); +} + +#[test] +fn create_token_multiple_have_different_values() { + let (_, _, user) = TestApp::init().with_user(); + let first: NewResponse = user.put("/api/v1/me/tokens", NEW_BAR).good(); + let second: NewResponse = user.put("/api/v1/me/tokens", NEW_BAR).good(); + + assert_ne!(first.api_token.token, second.api_token.token); +} + +#[test] +fn create_token_multiple_users_have_different_values() { + let (app, _, user1) = TestApp::init().with_user(); + let first_token: NewResponse = user1.put("/api/v1/me/tokens", NEW_BAR).good(); + + let user2 = app.db_new_user("bar"); + let second_token: NewResponse = user2.put("/api/v1/me/tokens", NEW_BAR).good(); + + assert_ne!(first_token.api_token.token, second_token.api_token.token); +} + +#[test] +fn cannot_create_token_with_token() { + let (_, _, _, token) = TestApp::init().with_token(); + let response = token.put::<()>( + "/api/v1/me/tokens", + br#"{ "api_token": { "name": "baz" } }"#, + ); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response.into_json(), + json!({ "errors": [{ "detail": "cannot use an API token to create a new API token" }] }) + ); +} diff --git a/src/tests/routes/me/tokens/delete.rs b/src/tests/routes/me/tokens/delete.rs new file mode 100644 index 00000000000..aa0756150c4 --- /dev/null +++ b/src/tests/routes/me/tokens/delete.rs @@ -0,0 +1,66 @@ +use crate::util::{RequestHelper, TestApp}; +use cargo_registry::models::ApiToken; +use cargo_registry::schema::api_tokens; +use diesel::prelude::*; + +#[derive(Deserialize)] +pub struct RevokedResponse {} + +#[test] +fn revoke_token_non_existing() { + let (_, _, user) = TestApp::init().with_user(); + let _json: RevokedResponse = user.delete("/api/v1/me/tokens/5").good(); +} + +#[test] +fn revoke_token_doesnt_revoke_other_users_token() { + let (app, _, user1, token) = TestApp::init().with_token(); + let user1 = user1.as_model(); + let token = token.as_model(); + let user2 = app.db_new_user("baz"); + + // List tokens for first user contains the token + app.db(|conn| { + let tokens: Vec = assert_ok!(ApiToken::belonging_to(user1).load(conn)); + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0].name, token.name); + }); + + // Try revoke the token as second user + let _json: RevokedResponse = user2 + .delete(&format!("/api/v1/me/tokens/{}", token.id)) + .good(); + + // List tokens for first user still contains the token + app.db(|conn| { + let tokens: Vec = assert_ok!(ApiToken::belonging_to(user1).load(conn)); + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0].name, token.name); + }); +} + +#[test] +fn revoke_token_success() { + let (app, _, user, token) = TestApp::init().with_token(); + + // List tokens contains the token + app.db(|conn| { + let tokens: Vec = assert_ok!(ApiToken::belonging_to(user.as_model()).load(conn)); + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0].name, token.as_model().name); + }); + + // Revoke the token + let _json: RevokedResponse = user + .delete(&format!("/api/v1/me/tokens/{}", token.as_model().id)) + .good(); + + // List tokens no longer contains the token + app.db(|conn| { + let count = ApiToken::belonging_to(user.as_model()) + .filter(api_tokens::revoked.eq(false)) + .count() + .get_result(conn); + assert_eq!(count, Ok(0)); + }); +} diff --git a/src/tests/routes/me/tokens/delete_current.rs b/src/tests/routes/me/tokens/delete_current.rs new file mode 100644 index 00000000000..48e78682ad2 --- /dev/null +++ b/src/tests/routes/me/tokens/delete_current.rs @@ -0,0 +1,73 @@ +use crate::util::{RequestHelper, TestApp}; +use cargo_registry::models::ApiToken; +use cargo_registry::schema::api_tokens; +use diesel::prelude::*; +use http::StatusCode; + +#[test] +fn revoke_current_token_success() { + let (app, _, user, token) = TestApp::init().with_token(); + + // Ensure that the token currently exists in the database + app.db(|conn| { + let tokens: Vec = assert_ok!(ApiToken::belonging_to(user.as_model()) + .filter(api_tokens::revoked.eq(false)) + .load(conn)); + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0].name, token.as_model().name); + }); + + // Revoke the token + let response = token.delete::<()>("/api/v1/tokens/current"); + assert_eq!(response.status(), StatusCode::NO_CONTENT); + + // Ensure that the token was removed from the database + app.db(|conn| { + let tokens: Vec = assert_ok!(ApiToken::belonging_to(user.as_model()) + .filter(api_tokens::revoked.eq(false)) + .load(conn)); + assert_eq!(tokens.len(), 0); + }); +} + +#[test] +fn revoke_current_token_without_auth() { + let (_, anon) = TestApp::init().empty(); + + let response = anon.delete::<()>("/api/v1/tokens/current"); + assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!( + response.into_json(), + json!({ "errors": [{ "detail": "must be logged in to perform that action" }] }) + ); +} + +#[test] +fn revoke_current_token_with_cookie_user() { + let (app, _, user, token) = TestApp::init().with_token(); + + // Ensure that the token currently exists in the database + app.db(|conn| { + let tokens: Vec = assert_ok!(ApiToken::belonging_to(user.as_model()) + .filter(api_tokens::revoked.eq(false)) + .load(conn)); + assert_eq!(tokens.len(), 1); + assert_eq!(tokens[0].name, token.as_model().name); + }); + + // Revoke the token + let response = user.delete::<()>("/api/v1/tokens/current"); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response.into_json(), + json!({ "errors": [{ "detail": "token not provided" }] }) + ); + + // Ensure that the token still exists in the database after the failed request + app.db(|conn| { + let tokens: Vec = assert_ok!(ApiToken::belonging_to(user.as_model()) + .filter(api_tokens::revoked.eq(false)) + .load(conn)); + assert_eq!(tokens.len(), 1); + }); +} diff --git a/src/tests/routes/me/tokens/list.rs b/src/tests/routes/me/tokens/list.rs new file mode 100644 index 00000000000..744f381c61f --- /dev/null +++ b/src/tests/routes/me/tokens/list.rs @@ -0,0 +1,87 @@ +use crate::routes::me::tokens::delete::RevokedResponse; +use crate::util::{RequestHelper, TestApp}; +use cargo_registry::models::ApiToken; +use std::collections::HashSet; + +#[derive(Deserialize)] +struct DecodableApiToken { + name: String, +} + +#[derive(Deserialize)] +struct ListResponse { + api_tokens: Vec, +} + +#[test] +fn list_logged_out() { + let (_, anon) = TestApp::init().empty(); + anon.get("/api/v1/me/tokens").assert_forbidden(); +} + +#[test] +fn list_with_api_token_is_forbidden() { + let (_, _, _, token) = TestApp::init().with_token(); + token.get("/api/v1/me/tokens").assert_forbidden(); +} + +#[test] +fn list_empty() { + let (_, _, user) = TestApp::init().with_user(); + let json: ListResponse = user.get("/api/v1/me/tokens").good(); + assert_eq!(json.api_tokens.len(), 0); +} + +#[test] +fn list_tokens() { + let (app, _, user) = TestApp::init().with_user(); + let id = user.as_model().id; + let tokens = app.db(|conn| { + vec![ + assert_ok!(ApiToken::insert(conn, id, "bar")), + assert_ok!(ApiToken::insert(conn, id, "baz")), + ] + }); + + let json: ListResponse = user.get("/api/v1/me/tokens").good(); + assert_eq!(json.api_tokens.len(), tokens.len()); + assert_eq!( + json.api_tokens + .into_iter() + .map(|t| t.name) + .collect::>(), + tokens + .into_iter() + .map(|t| t.model.name) + .collect::>() + ); +} + +#[test] +fn list_tokens_exclude_revoked() { + let (app, _, user) = TestApp::init().with_user(); + let id = user.as_model().id; + let tokens = app.db(|conn| { + vec![ + assert_ok!(ApiToken::insert(conn, id, "bar")), + assert_ok!(ApiToken::insert(conn, id, "baz")), + ] + }); + + // List tokens expecting them all to be there. + let json: ListResponse = user.get("/api/v1/me/tokens").good(); + assert_eq!(json.api_tokens.len(), tokens.len()); + + // Revoke the first token. + let _json: RevokedResponse = user + .delete(&format!("/api/v1/me/tokens/{}", tokens[0].model.id)) + .good(); + + // Check that we now have one less token being listed. + let json: ListResponse = user.get("/api/v1/me/tokens").good(); + assert_eq!(json.api_tokens.len(), tokens.len() - 1); + assert!(!json + .api_tokens + .iter() + .any(|token| token.name == tokens[0].model.name)); +} diff --git a/src/tests/routes/me/tokens/mod.rs b/src/tests/routes/me/tokens/mod.rs new file mode 100644 index 00000000000..32232d19249 --- /dev/null +++ b/src/tests/routes/me/tokens/mod.rs @@ -0,0 +1,4 @@ +pub mod create; +pub mod delete; +pub mod delete_current; +pub mod list; diff --git a/src/tests/metrics.rs b/src/tests/routes/metrics.rs similarity index 100% rename from src/tests/metrics.rs rename to src/tests/routes/metrics.rs diff --git a/src/tests/routes/mod.rs b/src/tests/routes/mod.rs new file mode 100644 index 00000000000..08ee5c0d525 --- /dev/null +++ b/src/tests/routes/mod.rs @@ -0,0 +1,21 @@ +//! This module should contain all tests that test a single webserver route. +//! +//! Each `/api/v1` (or `/api/private`) sub-API should have its own module, with +//! submodules divided by the specific endpoint (e.g. `list`, `create`, `read`, +//! `update`, `delete`). +//! +//! ## Examples +//! +//! - testing all the ways authentication works or fails on a specific route +//! - testing error behavior of a route +//! - testing output serialization of a route +//! - testing query parameter combinations of a route + +pub mod categories; +pub mod category_slugs; +pub mod crates; +pub mod keywords; +pub mod me; +pub mod metrics; +pub mod summary; +pub mod versions; diff --git a/src/tests/krate/summary.rs b/src/tests/routes/summary.rs similarity index 100% rename from src/tests/krate/summary.rs rename to src/tests/routes/summary.rs diff --git a/src/tests/routes/versions/list.rs b/src/tests/routes/versions/list.rs new file mode 100644 index 00000000000..48b25ec6dec --- /dev/null +++ b/src/tests/routes/versions/list.rs @@ -0,0 +1,36 @@ +use crate::builders::{CrateBuilder, VersionBuilder}; +use crate::util::insta::{self, assert_yaml_snapshot}; +use crate::util::{RequestHelper, TestApp}; +use cargo_registry::schema::versions; +use diesel::{QueryDsl, RunQueryDsl}; +use serde_json::Value; + +#[test] +fn index() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + let url = "/api/v1/versions"; + + let json: Value = anon.get(url).good(); + assert_yaml_snapshot!(json); + + let (v1, v2) = app.db(|conn| { + CrateBuilder::new("foo_vers_index", user.id) + .version(VersionBuilder::new("2.0.0").license(Some("MIT"))) + .version(VersionBuilder::new("2.0.1").license(Some("MIT/Apache-2.0"))) + .expect_build(conn); + let ids: Vec = versions::table.select(versions::id).load(conn).unwrap(); + (ids[0], ids[1]) + }); + + let query = format!("ids[]={v1}&ids[]={v2}"); + let json: Value = anon.get_with_query(url, &query).good(); + assert_yaml_snapshot!(json, { + ".versions" => insta::sorted_redaction(), + ".versions[].id" => insta::any_id_redaction(), + ".versions[].created_at" => "[datetime]", + ".versions[].updated_at" => "[datetime]", + ".versions[].published_by.id" => insta::id_redaction(user.id), + }); +} diff --git a/src/tests/routes/versions/mod.rs b/src/tests/routes/versions/mod.rs new file mode 100644 index 00000000000..fb0099e9a7c --- /dev/null +++ b/src/tests/routes/versions/mod.rs @@ -0,0 +1,2 @@ +pub mod list; +pub mod read; diff --git a/src/tests/routes/versions/read.rs b/src/tests/routes/versions/read.rs new file mode 100644 index 00000000000..4400a7fa4df --- /dev/null +++ b/src/tests/routes/versions/read.rs @@ -0,0 +1,26 @@ +use crate::builders::{CrateBuilder, VersionBuilder}; +use crate::util::insta::{self, assert_yaml_snapshot}; +use crate::util::{RequestHelper, TestApp}; +use serde_json::Value; + +#[test] +fn show_by_id() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + let v = app.db(|conn| { + let krate = CrateBuilder::new("foo_vers_show_id", user.id).expect_build(conn); + VersionBuilder::new("2.0.0") + .size(1234) + .expect_build(krate.id, user.id, conn) + }); + + let url = format!("/api/v1/versions/{}", v.id); + let json: Value = anon.get(&url).good(); + assert_yaml_snapshot!(json, { + ".version.id" => insta::id_redaction(v.id), + ".version.created_at" => "[datetime]", + ".version.updated_at" => "[datetime]", + ".version.published_by.id" => insta::id_redaction(user.id), + }); +} diff --git a/src/tests/snapshots/all__version__index-2.snap b/src/tests/routes/versions/snapshots/all__routes__versions__list__index-2.snap similarity index 96% rename from src/tests/snapshots/all__version__index-2.snap rename to src/tests/routes/versions/snapshots/all__routes__versions__list__index-2.snap index f9113a1c353..f827a88ae84 100644 --- a/src/tests/snapshots/all__version__index-2.snap +++ b/src/tests/routes/versions/snapshots/all__routes__versions__list__index-2.snap @@ -1,6 +1,6 @@ --- -source: src/tests/version.rs -assertion_line: 32 +source: src/tests/routes/versions/list.rs +assertion_line: 29 expression: json --- versions: diff --git a/src/tests/routes/versions/snapshots/all__routes__versions__list__index.snap b/src/tests/routes/versions/snapshots/all__routes__versions__list__index.snap new file mode 100644 index 00000000000..06425325720 --- /dev/null +++ b/src/tests/routes/versions/snapshots/all__routes__versions__list__index.snap @@ -0,0 +1,7 @@ +--- +source: src/tests/routes/versions/list.rs +assertion_line: 16 +expression: json +--- +versions: [] + diff --git a/src/tests/snapshots/all__version__show_by_id.snap b/src/tests/routes/versions/snapshots/all__routes__versions__read__show_by_id.snap similarity index 92% rename from src/tests/snapshots/all__version__show_by_id.snap rename to src/tests/routes/versions/snapshots/all__routes__versions__read__show_by_id.snap index bf7e5fc453a..31dc27d74f8 100644 --- a/src/tests/snapshots/all__version__show_by_id.snap +++ b/src/tests/routes/versions/snapshots/all__routes__versions__read__show_by_id.snap @@ -1,6 +1,6 @@ --- -source: src/tests/version.rs -assertion_line: 55 +source: src/tests/routes/versions/read.rs +assertion_line: 20 expression: json --- version: diff --git a/src/tests/snapshots/all__category__index.snap b/src/tests/snapshots/all__category__index.snap deleted file mode 100644 index d7774364a14..00000000000 --- a/src/tests/snapshots/all__category__index.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: src/tests/category.rs -assertion_line: 19 -expression: json ---- -categories: [] -meta: - total: 0 - diff --git a/src/tests/snapshots/all__version__authors.snap b/src/tests/snapshots/all__version__authors.snap deleted file mode 100644 index e80f460a822..00000000000 --- a/src/tests/snapshots/all__version__authors.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: src/tests/version.rs -assertion_line: 139 -expression: json ---- -meta: - names: [] -users: [] - diff --git a/src/tests/snapshots/all__version__index.snap b/src/tests/snapshots/all__version__index.snap deleted file mode 100644 index ccb20f095d1..00000000000 --- a/src/tests/snapshots/all__version__index.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: src/tests/version.rs -assertion_line: 23 -expression: json ---- -versions: [] - diff --git a/src/tests/token.rs b/src/tests/token.rs index afa47ff49fb..98a43f86940 100644 --- a/src/tests/token.rs +++ b/src/tests/token.rs @@ -1,344 +1,8 @@ use crate::{RequestHelper, TestApp}; -use cargo_registry::{ - models::ApiToken, - schema::api_tokens, - util::errors::TOKEN_FORMAT_ERROR, - views::{EncodableApiTokenWithToken, EncodableMe}, -}; -use std::collections::HashSet; - +use cargo_registry::{models::ApiToken, util::errors::TOKEN_FORMAT_ERROR, views::EncodableMe}; use conduit::{header, StatusCode}; use diesel::prelude::*; -#[derive(Deserialize)] -struct DecodableApiToken { - name: String, -} - -#[derive(Deserialize)] -struct ListResponse { - api_tokens: Vec, -} -#[derive(Deserialize)] -struct NewResponse { - api_token: EncodableApiTokenWithToken, -} -#[derive(Deserialize)] -struct RevokedResponse {} - -// Default values used by many tests -static URL: &str = "/api/v1/me/tokens"; -static NEW_BAR: &[u8] = br#"{ "api_token": { "name": "bar" } }"#; - -#[test] -fn list_logged_out() { - let (_, anon) = TestApp::init().empty(); - anon.get(URL).assert_forbidden(); -} - -#[test] -fn list_with_api_token_is_forbidden() { - let (_, _, _, token) = TestApp::init().with_token(); - token.get(URL).assert_forbidden(); -} - -#[test] -fn list_empty() { - let (_, _, user) = TestApp::init().with_user(); - let json: ListResponse = user.get(URL).good(); - assert_eq!(json.api_tokens.len(), 0); -} - -#[test] -fn list_tokens() { - let (app, _, user) = TestApp::init().with_user(); - let id = user.as_model().id; - let tokens = app.db(|conn| { - vec![ - assert_ok!(ApiToken::insert(conn, id, "bar")), - assert_ok!(ApiToken::insert(conn, id, "baz")), - ] - }); - - let json: ListResponse = user.get(URL).good(); - assert_eq!(json.api_tokens.len(), tokens.len()); - assert_eq!( - json.api_tokens - .into_iter() - .map(|t| t.name) - .collect::>(), - tokens - .into_iter() - .map(|t| t.model.name) - .collect::>() - ); -} - -#[test] -fn list_tokens_exclude_revoked() { - let (app, _, user) = TestApp::init().with_user(); - let id = user.as_model().id; - let tokens = app.db(|conn| { - vec![ - assert_ok!(ApiToken::insert(conn, id, "bar")), - assert_ok!(ApiToken::insert(conn, id, "baz")), - ] - }); - - // List tokens expecting them all to be there. - let json: ListResponse = user.get(URL).good(); - assert_eq!(json.api_tokens.len(), tokens.len()); - - // Revoke the first token. - let _json: RevokedResponse = user - .delete(&format!("/api/v1/me/tokens/{}", tokens[0].model.id)) - .good(); - - // Check that we now have one less token being listed. - let json: ListResponse = user.get(URL).good(); - assert_eq!(json.api_tokens.len(), tokens.len() - 1); - assert!(!json - .api_tokens - .iter() - .any(|token| token.name == tokens[0].model.name)); -} - -#[test] -fn create_token_logged_out() { - let (_, anon) = TestApp::init().empty(); - anon.put(URL, NEW_BAR).assert_forbidden(); -} - -#[test] -fn create_token_invalid_request() { - let (_, _, user) = TestApp::init().with_user(); - let invalid = br#"{ "name": "" }"#; - let response = user.put::<()>(URL, invalid); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response.into_json(), - json!({ "errors": [{ "detail": "invalid new token request: Error(\"missing field `api_token`\", line: 1, column: 14)" }] }) - ); -} - -#[test] -fn create_token_no_name() { - let (_, _, user) = TestApp::init().with_user(); - let empty_name = br#"{ "api_token": { "name": "" } }"#; - let response = user.put::<()>(URL, empty_name); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response.into_json(), - json!({ "errors": [{ "detail": "name must have a value" }] }) - ); -} - -#[test] -fn create_token_long_body() { - let (_, _, user) = TestApp::init().with_user(); - let too_big = &[5; 5192]; // Send a request with a 5kB body of 5's - let response = user.put::<()>(URL, too_big); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response.into_json(), - json!({ "errors": [{ "detail": "max content length is: 2000" }] }) - ); -} - -#[test] -fn create_token_exceeded_tokens_per_user() { - let (app, _, user) = TestApp::init().with_user(); - let id = user.as_model().id; - app.db(|conn| { - for i in 0..1000 { - assert_ok!(ApiToken::insert(conn, id, &format!("token {i}"))); - } - }); - let response = user.put::<()>(URL, NEW_BAR); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response.into_json(), - json!({ "errors": [{ "detail": "maximum tokens per user is: 500" }] }) - ); -} - -#[test] -fn create_token_success() { - let (app, _, user) = TestApp::init().with_user(); - - let json: NewResponse = user.put(URL, NEW_BAR).good(); - assert_eq!(json.api_token.name, "bar"); - assert!(!json.api_token.token.is_empty()); - - let tokens: Vec = - app.db(|conn| assert_ok!(ApiToken::belonging_to(user.as_model()).load(conn))); - assert_eq!(tokens.len(), 1); - assert_eq!(tokens[0].name, "bar"); - assert!(!tokens[0].revoked); - assert_eq!(tokens[0].last_used_at, None); -} - -#[test] -fn create_token_multiple_have_different_values() { - let (_, _, user) = TestApp::init().with_user(); - let first: NewResponse = user.put(URL, NEW_BAR).good(); - let second: NewResponse = user.put(URL, NEW_BAR).good(); - - assert_ne!(first.api_token.token, second.api_token.token); -} - -#[test] -fn create_token_multiple_users_have_different_values() { - let (app, _, user1) = TestApp::init().with_user(); - let first_token: NewResponse = user1.put(URL, NEW_BAR).good(); - - let user2 = app.db_new_user("bar"); - let second_token: NewResponse = user2.put(URL, NEW_BAR).good(); - - assert_ne!(first_token.api_token.token, second_token.api_token.token); -} - -#[test] -fn cannot_create_token_with_token() { - let (_, _, _, token) = TestApp::init().with_token(); - let response = token.put::<()>( - "/api/v1/me/tokens", - br#"{ "api_token": { "name": "baz" } }"#, - ); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response.into_json(), - json!({ "errors": [{ "detail": "cannot use an API token to create a new API token" }] }) - ); -} - -#[test] -fn revoke_token_non_existing() { - let (_, _, user) = TestApp::init().with_user(); - let _json: RevokedResponse = user.delete("/api/v1/me/tokens/5").good(); -} - -#[test] -fn revoke_token_doesnt_revoke_other_users_token() { - let (app, _, user1, token) = TestApp::init().with_token(); - let user1 = user1.as_model(); - let token = token.as_model(); - let user2 = app.db_new_user("baz"); - - // List tokens for first user contains the token - app.db(|conn| { - let tokens: Vec = assert_ok!(ApiToken::belonging_to(user1).load(conn)); - assert_eq!(tokens.len(), 1); - assert_eq!(tokens[0].name, token.name); - }); - - // Try revoke the token as second user - let _json: RevokedResponse = user2 - .delete(&format!("/api/v1/me/tokens/{}", token.id)) - .good(); - - // List tokens for first user still contains the token - app.db(|conn| { - let tokens: Vec = assert_ok!(ApiToken::belonging_to(user1).load(conn)); - assert_eq!(tokens.len(), 1); - assert_eq!(tokens[0].name, token.name); - }); -} - -#[test] -fn revoke_token_success() { - let (app, _, user, token) = TestApp::init().with_token(); - - // List tokens contains the token - app.db(|conn| { - let tokens: Vec = assert_ok!(ApiToken::belonging_to(user.as_model()).load(conn)); - assert_eq!(tokens.len(), 1); - assert_eq!(tokens[0].name, token.as_model().name); - }); - - // Revoke the token - let _json: RevokedResponse = user - .delete(&format!("/api/v1/me/tokens/{}", token.as_model().id)) - .good(); - - // List tokens no longer contains the token - app.db(|conn| { - let count = ApiToken::belonging_to(user.as_model()) - .filter(api_tokens::revoked.eq(false)) - .count() - .get_result(conn); - assert_eq!(count, Ok(0)); - }); -} - -#[test] -fn revoke_current_token_success() { - let (app, _, user, token) = TestApp::init().with_token(); - - // Ensure that the token currently exists in the database - app.db(|conn| { - let tokens: Vec = assert_ok!(ApiToken::belonging_to(user.as_model()) - .filter(api_tokens::revoked.eq(false)) - .load(conn)); - assert_eq!(tokens.len(), 1); - assert_eq!(tokens[0].name, token.as_model().name); - }); - - // Revoke the token - let response = token.delete::<()>("/api/v1/tokens/current"); - assert_eq!(response.status(), StatusCode::NO_CONTENT); - - // Ensure that the token was removed from the database - app.db(|conn| { - let tokens: Vec = assert_ok!(ApiToken::belonging_to(user.as_model()) - .filter(api_tokens::revoked.eq(false)) - .load(conn)); - assert_eq!(tokens.len(), 0); - }); -} - -#[test] -fn revoke_current_token_without_auth() { - let (_, anon) = TestApp::init().empty(); - - let response = anon.delete::<()>("/api/v1/tokens/current"); - assert_eq!(response.status(), StatusCode::FORBIDDEN); - assert_eq!( - response.into_json(), - json!({ "errors": [{ "detail": "must be logged in to perform that action" }] }) - ); -} - -#[test] -fn revoke_current_token_with_cookie_user() { - let (app, _, user, token) = TestApp::init().with_token(); - - // Ensure that the token currently exists in the database - app.db(|conn| { - let tokens: Vec = assert_ok!(ApiToken::belonging_to(user.as_model()) - .filter(api_tokens::revoked.eq(false)) - .load(conn)); - assert_eq!(tokens.len(), 1); - assert_eq!(tokens[0].name, token.as_model().name); - }); - - // Revoke the token - let response = user.delete::<()>("/api/v1/tokens/current"); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - assert_eq!( - response.into_json(), - json!({ "errors": [{ "detail": "token not provided" }] }) - ); - - // Ensure that the token still exists in the database after the failed request - app.db(|conn| { - let tokens: Vec = assert_ok!(ApiToken::belonging_to(user.as_model()) - .filter(api_tokens::revoked.eq(false)) - .load(conn)); - assert_eq!(tokens.len(), 1); - }); -} - #[test] fn using_token_updates_last_used_at() { let url = "/api/v1/me"; diff --git a/src/tests/version.rs b/src/tests/version.rs index 1879184fe69..3fdef44f6f7 100644 --- a/src/tests/version.rs +++ b/src/tests/version.rs @@ -1,133 +1,6 @@ -use crate::{ - builders::{CrateBuilder, PublishBuilder, VersionBuilder}, - RequestHelper, TestApp, -}; -use cargo_registry::{models::Version, schema::versions}; - -use crate::util::insta::{self, assert_yaml_snapshot}; -use diesel::prelude::*; -use serde_json::Value; - -#[test] -fn index() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - let url = "/api/v1/versions"; - - let json: Value = anon.get(url).good(); - assert_yaml_snapshot!(json); - - let (v1, v2) = app.db(|conn| { - CrateBuilder::new("foo_vers_index", user.id) - .version(VersionBuilder::new("2.0.0").license(Some("MIT"))) - .version(VersionBuilder::new("2.0.1").license(Some("MIT/Apache-2.0"))) - .expect_build(conn); - let ids: Vec = versions::table.select(versions::id).load(conn).unwrap(); - (ids[0], ids[1]) - }); - - let query = format!("ids[]={v1}&ids[]={v2}"); - let json: Value = anon.get_with_query(url, &query).good(); - assert_yaml_snapshot!(json, { - ".versions" => insta::sorted_redaction(), - ".versions[].id" => insta::any_id_redaction(), - ".versions[].created_at" => "[datetime]", - ".versions[].updated_at" => "[datetime]", - ".versions[].published_by.id" => insta::id_redaction(user.id), - }); -} - -#[test] -fn show_by_id() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - let v = app.db(|conn| { - let krate = CrateBuilder::new("foo_vers_show_id", user.id).expect_build(conn); - VersionBuilder::new("2.0.0") - .size(1234) - .expect_build(krate.id, user.id, conn) - }); - - let url = format!("/api/v1/versions/{}", v.id); - let json: Value = anon.get(&url).good(); - assert_yaml_snapshot!(json, { - ".version.id" => insta::id_redaction(v.id), - ".version.created_at" => "[datetime]", - ".version.updated_at" => "[datetime]", - ".version.published_by.id" => insta::id_redaction(user.id), - }); -} - -#[test] -fn show_by_crate_name_and_version() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - let v = app.db(|conn| { - let krate = CrateBuilder::new("foo_vers_show", user.id).expect_build(conn); - VersionBuilder::new("2.0.0") - .size(1234) - .checksum("c241cd77c3723ccf1aa453f169ee60c0a888344da504bee0142adb859092acb4") - .expect_build(krate.id, user.id, conn) - }); - - let url = "/api/v1/crates/foo_vers_show/2.0.0"; - let json: Value = anon.get(url).good(); - assert_yaml_snapshot!(json, { - ".version.id" => insta::id_redaction(v.id), - ".version.created_at" => "[datetime]", - ".version.updated_at" => "[datetime]", - ".version.published_by.id" => insta::id_redaction(user.id), - }); -} - -#[test] -fn show_by_crate_name_and_semver_no_published_by() { - use diesel::update; - - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - let v = app.db(|conn| { - let krate = CrateBuilder::new("foo_vers_show_no_pb", user.id).expect_build(conn); - let version = VersionBuilder::new("1.0.0").expect_build(krate.id, user.id, conn); - - // 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(); - - version - }); - - let url = "/api/v1/crates/foo_vers_show_no_pb/1.0.0"; - let json: Value = anon.get(url).good(); - assert_yaml_snapshot!(json, { - ".version.id" => insta::id_redaction(v.id), - ".version.created_at" => "[datetime]", - ".version.updated_at" => "[datetime]", - }); -} - -#[test] -fn authors() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - CrateBuilder::new("foo_authors", user.id) - .version("1.0.0") - .expect_build(conn); - }); - - let json: Value = anon.get("/api/v1/crates/foo_authors/1.0.0/authors").good(); - let json = json.as_object().unwrap(); - assert_yaml_snapshot!(json); -} +use crate::builders::{CrateBuilder, VersionBuilder}; +use crate::TestApp; +use cargo_registry::models::Version; #[test] fn record_rerendered_readme_time() { @@ -142,59 +15,3 @@ fn record_rerendered_readme_time() { Version::record_readme_rendering(version.id, conn).unwrap(); }); } - -#[test] -fn version_size() { - let (_, _, user) = TestApp::full().with_user(); - - let crate_to_publish = PublishBuilder::new("foo_version_size").version("1.0.0"); - user.publish_crate(crate_to_publish).good(); - - // Add a file to version 2 so that it's a different size than version 1 - let files = [("foo_version_size-2.0.0/big", &[b'a'; 1] as &[_])]; - let crate_to_publish = PublishBuilder::new("foo_version_size") - .version("2.0.0") - .files(&files); - user.publish_crate(crate_to_publish).good(); - - let crate_json = user.show_crate("foo_version_size"); - - let version1 = crate_json - .versions - .as_ref() - .unwrap() - .iter() - .find(|v| v.num == "1.0.0") - .expect("Could not find v1.0.0"); - assert_eq!(version1.crate_size, Some(35)); - - let version2 = crate_json - .versions - .as_ref() - .unwrap() - .iter() - .find(|v| v.num == "2.0.0") - .expect("Could not find v2.0.0"); - assert_eq!(version2.crate_size, Some(91)); -} - -#[test] -fn daily_limit() { - let (app, _, user) = TestApp::full().with_user(); - - let max_daily_versions = app.as_inner().config.new_version_rate_limit.unwrap(); - for version in 1..=max_daily_versions { - let crate_to_publish = - PublishBuilder::new("foo_daily_limit").version(&format!("0.0.{}", version)); - user.publish_crate(crate_to_publish).good(); - } - - let crate_to_publish = PublishBuilder::new("foo_daily_limit").version("1.0.0"); - let response = user.publish_crate(crate_to_publish); - assert!(response.status().is_success()); - let json = response.into_json(); - assert_eq!( - json["errors"][0]["detail"], - "You have published too many versions of this crate in the last 24 hours" - ); -}