diff --git a/src/tests/http-data/krate_good_badges b/src/tests/http-data/krate_publish_good_badges similarity index 100% rename from src/tests/http-data/krate_good_badges rename to src/tests/http-data/krate_publish_good_badges diff --git a/src/tests/http-data/krate_good_categories b/src/tests/http-data/krate_publish_good_categories similarity index 100% rename from src/tests/http-data/krate_good_categories rename to src/tests/http-data/krate_publish_good_categories diff --git a/src/tests/http-data/krate_ignored_badges b/src/tests/http-data/krate_publish_ignored_badges similarity index 100% rename from src/tests/http-data/krate_ignored_badges rename to src/tests/http-data/krate_publish_ignored_badges diff --git a/src/tests/http-data/krate_ignored_categories b/src/tests/http-data/krate_publish_ignored_categories similarity index 100% rename from src/tests/http-data/krate_ignored_categories rename to src/tests/http-data/krate_publish_ignored_categories diff --git a/src/tests/http-data/krate_new_crate_allow_empty_alternative_registry_dependency b/src/tests/http-data/krate_publish_new_crate_allow_empty_alternative_registry_dependency similarity index 100% rename from src/tests/http-data/krate_new_crate_allow_empty_alternative_registry_dependency rename to src/tests/http-data/krate_publish_new_crate_allow_empty_alternative_registry_dependency diff --git a/src/tests/http-data/krate_new_krate b/src/tests/http-data/krate_publish_new_krate similarity index 100% rename from src/tests/http-data/krate_new_krate rename to src/tests/http-data/krate_publish_new_krate diff --git a/src/tests/http-data/krate_new_krate_git_upload b/src/tests/http-data/krate_publish_new_krate_git_upload similarity index 100% rename from src/tests/http-data/krate_new_krate_git_upload rename to src/tests/http-data/krate_publish_new_krate_git_upload diff --git a/src/tests/http-data/krate_new_krate_git_upload_appends b/src/tests/http-data/krate_publish_new_krate_git_upload_appends similarity index 100% rename from src/tests/http-data/krate_new_krate_git_upload_appends rename to src/tests/http-data/krate_publish_new_krate_git_upload_appends diff --git a/src/tests/http-data/krate_new_krate_git_upload_with_conflicts b/src/tests/http-data/krate_publish_new_krate_git_upload_with_conflicts similarity index 100% rename from src/tests/http-data/krate_new_krate_git_upload_with_conflicts rename to src/tests/http-data/krate_publish_new_krate_git_upload_with_conflicts diff --git a/src/tests/http-data/krate_new_krate_records_verified_email b/src/tests/http-data/krate_publish_new_krate_records_verified_email similarity index 100% rename from src/tests/http-data/krate_new_krate_records_verified_email rename to src/tests/http-data/krate_publish_new_krate_records_verified_email diff --git a/src/tests/http-data/krate_new_krate_too_big_but_whitelisted b/src/tests/http-data/krate_publish_new_krate_too_big_but_whitelisted similarity index 100% rename from src/tests/http-data/krate_new_krate_too_big_but_whitelisted rename to src/tests/http-data/krate_publish_new_krate_too_big_but_whitelisted diff --git a/src/tests/http-data/krate_new_krate_twice b/src/tests/http-data/krate_publish_new_krate_twice similarity index 100% rename from src/tests/http-data/krate_new_krate_twice rename to src/tests/http-data/krate_publish_new_krate_twice diff --git a/src/tests/http-data/krate_new_krate_weird_version b/src/tests/http-data/krate_publish_new_krate_weird_version similarity index 100% rename from src/tests/http-data/krate_new_krate_weird_version rename to src/tests/http-data/krate_publish_new_krate_weird_version diff --git a/src/tests/http-data/krate_new_krate_with_dependency b/src/tests/http-data/krate_publish_new_krate_with_dependency similarity index 100% rename from src/tests/http-data/krate_new_krate_with_dependency rename to src/tests/http-data/krate_publish_new_krate_with_dependency diff --git a/src/tests/http-data/krate_new_krate_with_readme b/src/tests/http-data/krate_publish_new_krate_with_readme similarity index 100% rename from src/tests/http-data/krate_new_krate_with_readme rename to src/tests/http-data/krate_publish_new_krate_with_readme diff --git a/src/tests/http-data/krate_new_krate_with_token b/src/tests/http-data/krate_publish_new_krate_with_token similarity index 100% rename from src/tests/http-data/krate_new_krate_with_token rename to src/tests/http-data/krate_publish_new_krate_with_token diff --git a/src/tests/http-data/krate_new_with_renamed_dependency b/src/tests/http-data/krate_publish_new_with_renamed_dependency similarity index 100% rename from src/tests/http-data/krate_new_with_renamed_dependency rename to src/tests/http-data/krate_publish_new_with_renamed_dependency diff --git a/src/tests/http-data/krate_publish_after_removing_documentation b/src/tests/http-data/krate_publish_publish_after_removing_documentation similarity index 100% rename from src/tests/http-data/krate_publish_after_removing_documentation rename to src/tests/http-data/krate_publish_publish_after_removing_documentation diff --git a/src/tests/http-data/krate_publish_new_crate_rate_limited b/src/tests/http-data/krate_publish_publish_new_crate_rate_limited similarity index 100% rename from src/tests/http-data/krate_publish_new_crate_rate_limited rename to src/tests/http-data/krate_publish_publish_new_crate_rate_limited diff --git a/src/tests/http-data/krate_publish_rate_limit_doesnt_affect_existing_crates b/src/tests/http-data/krate_publish_publish_rate_limit_doesnt_affect_existing_crates similarity index 100% rename from src/tests/http-data/krate_publish_rate_limit_doesnt_affect_existing_crates rename to src/tests/http-data/krate_publish_publish_rate_limit_doesnt_affect_existing_crates diff --git a/src/tests/http-data/krate_publish_records_an_audit_action b/src/tests/http-data/krate_publish_publish_records_an_audit_action similarity index 100% rename from src/tests/http-data/krate_publish_records_an_audit_action rename to src/tests/http-data/krate_publish_publish_records_an_audit_action diff --git a/src/tests/http-data/krate_uploading_new_version_touches_crate b/src/tests/http-data/krate_publish_uploading_new_version_touches_crate similarity index 100% rename from src/tests/http-data/krate_uploading_new_version_touches_crate rename to src/tests/http-data/krate_publish_uploading_new_version_touches_crate diff --git a/src/tests/http-data/krate_publish_after_yank_max_version b/src/tests/http-data/krate_yanking_publish_after_yank_max_version similarity index 100% rename from src/tests/http-data/krate_publish_after_yank_max_version rename to src/tests/http-data/krate_yanking_publish_after_yank_max_version diff --git a/src/tests/http-data/krate_unyank_records_an_audit_action b/src/tests/http-data/krate_yanking_unyank_records_an_audit_action similarity index 100% rename from src/tests/http-data/krate_unyank_records_an_audit_action rename to src/tests/http-data/krate_yanking_unyank_records_an_audit_action diff --git a/src/tests/http-data/krate_yank_max_version b/src/tests/http-data/krate_yanking_yank_max_version similarity index 100% rename from src/tests/http-data/krate_yank_max_version rename to src/tests/http-data/krate_yanking_yank_max_version diff --git a/src/tests/http-data/krate_yank_records_an_audit_action b/src/tests/http-data/krate_yanking_yank_records_an_audit_action similarity index 100% rename from src/tests/http-data/krate_yank_records_an_audit_action rename to src/tests/http-data/krate_yanking_yank_records_an_audit_action diff --git a/src/tests/http-data/krate_yank_works_as_intended b/src/tests/http-data/krate_yanking_yank_works_as_intended similarity index 100% rename from src/tests/http-data/krate_yank_works_as_intended rename to src/tests/http-data/krate_yanking_yank_works_as_intended diff --git a/src/tests/krate.rs b/src/tests/krate.rs deleted file mode 100644 index 45adb68caba..00000000000 --- a/src/tests/krate.rs +++ /dev/null @@ -1,2514 +0,0 @@ -use crate::{ - builders::{CrateBuilder, DependencyBuilder, PublishBuilder, VersionBuilder}, - new_category, new_dependency, new_user, CrateMeta, CrateResponse, GoodCrate, OkBool, - RequestHelper, TestApp, -}; -use cargo_registry::{ - models::{krate::MAX_NAME_LENGTH, Category}, - schema::{api_tokens, crates, emails, metadata, versions, versions_published_by}, - views::{ - EncodableCategory, EncodableCrate, EncodableDependency, EncodableKeyword, EncodableVersion, - EncodableVersionDownload, - }, -}; -use std::{ - collections::HashMap, - io::{self, prelude::*}, - thread, - time::Duration, -}; - -use chrono::Utc; -use conduit::StatusCode; -use diesel::{dsl::*, prelude::*, update}; -use flate2::{write::GzEncoder, Compression}; - -#[derive(Deserialize)] -struct VersionsList { - versions: Vec, -} -#[derive(Deserialize)] -struct Deps { - dependencies: Vec, -} -#[derive(Deserialize)] -struct RevDeps { - dependencies: Vec, - versions: Vec, - meta: CrateMeta, -} -#[derive(Deserialize)] -struct Downloads { - version_downloads: Vec, -} - -#[derive(Deserialize)] -struct SummaryResponse { - num_downloads: i64, - num_crates: i64, - new_crates: Vec, - most_downloaded: Vec, - most_recently_downloaded: Vec, - just_updated: Vec, - popular_keywords: Vec, - popular_categories: Vec, -} - -impl crate::util::MockAnonymousUser { - fn reverse_dependencies(&self, krate_name: &str) -> RevDeps { - let url = format!("/api/v1/crates/{}/reverse_dependencies", krate_name); - self.get(&url).good() - } -} - -impl crate::util::MockTokenUser { - /// Yank the specified version of the specified crate and run all pending background jobs - fn yank(&self, krate_name: &str, version: &str) -> crate::util::Response { - let url = format!("/api/v1/crates/{}/{}/yank", krate_name, version); - let response = self.delete(&url); - self.app().run_pending_background_jobs(); - response - } - - /// Unyank the specified version of the specified crate and run all pending background jobs - fn unyank(&self, krate_name: &str, version: &str) -> crate::util::Response { - let url = format!("/api/v1/crates/{}/{}/unyank", krate_name, version); - let response = self.put(&url, &[]); - self.app().run_pending_background_jobs(); - response - } -} - -impl crate::util::MockCookieUser { - /// Yank the specified version of the specified crate and run all pending background jobs - fn yank(&self, krate_name: &str, version: &str) -> crate::util::Response { - let url = format!("/api/v1/crates/{}/{}/yank", krate_name, version); - let response = self.delete(&url); - self.app().run_pending_background_jobs(); - response - } - - /// Unyank the specified version of the specified crate and run all pending background jobs - fn unyank(&self, krate_name: &str, version: &str) -> crate::util::Response { - let url = format!("/api/v1/crates/{}/{}/unyank", krate_name, version); - let response = self.put(&url, &[]); - self.app().run_pending_background_jobs(); - response - } -} - -#[test] -fn index() { - let (app, anon) = TestApp::init().empty(); - let json = anon.search(""); - assert_eq!(json.crates.len(), 0); - assert_eq!(json.meta.total, 0); - - let krate = app.db(|conn| { - let u = new_user("foo").create_or_update(None, conn).unwrap(); - CrateBuilder::new("fooindex", u.id).expect_build(conn) - }); - - let json = anon.search(""); - assert_eq!(json.crates.len(), 1); - assert_eq!(json.meta.total, 1); - assert_eq!(json.crates[0].name, krate.name); - assert_eq!(json.crates[0].id, krate.name); -} - -#[test] -#[allow(clippy::cognitive_complexity)] -fn index_queries() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - let (krate, krate2) = app.db(|conn| { - let krate = CrateBuilder::new("foo_index_queries", user.id) - .readme("readme") - .description("description") - .keyword("kw1") - .expect_build(conn); - - let krate2 = CrateBuilder::new("BAR_INDEX_QUERIES", user.id) - .keyword("KW1") - .expect_build(conn); - - CrateBuilder::new("foo", user.id) - .keyword("kw3") - .expect_build(conn); - - CrateBuilder::new("two-keywords", user.id) - .keyword("kw1") - .keyword("kw3") - .expect_build(conn); - (krate, krate2) - }); - - assert_eq!(anon.search("q=baz").meta.total, 0); - - // All of these fields should be indexed/searched by the queries - assert_eq!(anon.search("q=foo").meta.total, 2); - assert_eq!(anon.search("q=kw1").meta.total, 3); - assert_eq!(anon.search("q=readme").meta.total, 1); - assert_eq!(anon.search("q=description").meta.total, 1); - - assert_eq!(anon.search_by_user_id(user.id).crates.len(), 4); - assert_eq!(anon.search_by_user_id(0).crates.len(), 0); - - assert_eq!(anon.search("letter=F").crates.len(), 2); - assert_eq!(anon.search("letter=B").crates.len(), 1); - assert_eq!(anon.search("letter=b").crates.len(), 1); - assert_eq!(anon.search("letter=c").crates.len(), 0); - - assert_eq!(anon.search("keyword=kw1").crates.len(), 3); - assert_eq!(anon.search("keyword=KW1").crates.len(), 3); - assert_eq!(anon.search("keyword=kw2").crates.len(), 0); - assert_eq!(anon.search("all_keywords=kw1 kw3").crates.len(), 1); - - assert_eq!(anon.search("q=foo&keyword=kw1").crates.len(), 1); - assert_eq!(anon.search("q=foo2&keyword=kw1").crates.len(), 0); - - app.db(|conn| { - new_category("Category 1", "cat1", "Category 1 crates") - .create_or_update(conn) - .unwrap(); - new_category("Category 1::Ba'r", "cat1::bar", "Ba'r crates") - .create_or_update(conn) - .unwrap(); - Category::update_crate(conn, &krate, &["cat1"]).unwrap(); - Category::update_crate(conn, &krate2, &["cat1::bar"]).unwrap(); - }); - - let cl = anon.search("category=cat1"); - assert_eq!(cl.crates.len(), 2); - assert_eq!(cl.meta.total, 2); - - let cl = anon.search("category=cat1::bar"); - assert_eq!(cl.crates.len(), 1); - assert_eq!(cl.meta.total, 1); - - let cl = anon.search("keyword=cat2"); - assert_eq!(cl.crates.len(), 0); - assert_eq!(cl.meta.total, 0); - - let cl = anon.search("q=readme&category=cat1"); - assert_eq!(cl.crates.len(), 1); - assert_eq!(cl.meta.total, 1); - - let cl = anon.search("keyword=kw1&category=cat1"); - assert_eq!(cl.crates.len(), 2); - assert_eq!(cl.meta.total, 2); - - let cl = anon.search("keyword=kw3&category=cat1"); - assert_eq!(cl.crates.len(), 0); - assert_eq!(cl.meta.total, 0); -} - -#[test] -fn search_includes_crates_where_name_is_stopword() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - app.db(|conn| { - CrateBuilder::new("which", user.id).expect_build(conn); - CrateBuilder::new("should_be_excluded", user.id) - .readme("crate which does things") - .expect_build(conn); - }); - let json = anon.search("q=which"); - assert_eq!(json.crates.len(), 1); - assert_eq!(json.meta.total, 1); -} - -#[test] -fn exact_match_first_on_queries() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - CrateBuilder::new("foo_exact", user.id) - .description("bar_exact baz_exact") - .expect_build(conn); - - CrateBuilder::new("bar-exact", user.id) - .description("foo_exact baz_exact foo-exact baz_exact") - .expect_build(conn); - - CrateBuilder::new("baz_exact", user.id) - .description("foo-exact bar_exact foo-exact bar_exact foo_exact bar_exact") - .expect_build(conn); - - CrateBuilder::new("other_exact", user.id) - .description("other_exact") - .expect_build(conn); - }); - - let json = anon.search("q=foo-exact"); - assert_eq!(json.meta.total, 3); - assert_eq!(json.crates[0].name, "foo_exact"); - assert_eq!(json.crates[1].name, "baz_exact"); - assert_eq!(json.crates[2].name, "bar-exact"); - - let json = anon.search("q=bar_exact"); - assert_eq!(json.meta.total, 3); - assert_eq!(json.crates[0].name, "bar-exact"); - assert_eq!(json.crates[1].name, "baz_exact"); - assert_eq!(json.crates[2].name, "foo_exact"); - - let json = anon.search("q=baz_exact"); - assert_eq!(json.meta.total, 3); - assert_eq!(json.crates[0].name, "baz_exact"); - assert_eq!(json.crates[1].name, "bar-exact"); - assert_eq!(json.crates[2].name, "foo_exact"); -} - -#[test] -#[allow(clippy::cognitive_complexity)] -fn index_sorting() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - let krate1 = CrateBuilder::new("foo_sort", user.id) - .description("bar_sort baz_sort const") - .downloads(50) - .recent_downloads(50) - .expect_build(conn); - - let krate2 = CrateBuilder::new("bar_sort", user.id) - .description("foo_sort baz_sort foo_sort baz_sort const") - .downloads(3333) - .recent_downloads(0) - .expect_build(conn); - - let krate3 = CrateBuilder::new("baz_sort", user.id) - .description("foo_sort bar_sort foo_sort bar_sort foo_sort bar_sort const") - .downloads(100_000) - .recent_downloads(10) - .expect_build(conn); - - let krate4 = CrateBuilder::new("other_sort", user.id) - .description("other_sort const") - .downloads(999_999) - .expect_build(conn); - - // Set the created at column for each crate - update(&krate1) - .set(crates::created_at.eq(now - 4.weeks())) - .execute(conn) - .unwrap(); - update(&krate2) - .set(crates::created_at.eq(now - 1.weeks())) - .execute(conn) - .unwrap(); - update(&krate3) - .set(crates::created_at.eq(now - 2.weeks())) - .execute(conn) - .unwrap(); - update(&krate4) - .set(crates::created_at.eq(now - 3.weeks())) - .execute(conn) - .unwrap(); - - // Set the updated at column for each crate - update(&krate1) - .set(crates::updated_at.eq(now - 3.weeks())) - .execute(conn) - .unwrap(); - update(&krate2) - .set(crates::updated_at.eq(now - 5.days())) - .execute(conn) - .unwrap(); - update(&krate3) - .set(crates::updated_at.eq(now - 10.seconds())) - .execute(conn) - .unwrap(); - update(&krate4) - .set(crates::updated_at.eq(now)) - .execute(conn) - .unwrap(); - }); - - // Sort by downloads - let json = anon.search("sort=downloads"); - assert_eq!(json.meta.total, 4); - assert_eq!(json.crates[0].name, "other_sort"); - assert_eq!(json.crates[1].name, "baz_sort"); - assert_eq!(json.crates[2].name, "bar_sort"); - assert_eq!(json.crates[3].name, "foo_sort"); - - // Sort by recent-downloads - let json = anon.search("sort=recent-downloads"); - assert_eq!(json.meta.total, 4); - assert_eq!(json.crates[0].name, "foo_sort"); - assert_eq!(json.crates[1].name, "baz_sort"); - assert_eq!(json.crates[2].name, "bar_sort"); - assert_eq!(json.crates[3].name, "other_sort"); - - // Sort by recent-updates - let json = anon.search("sort=recent-updates"); - assert_eq!(json.meta.total, 4); - assert_eq!(json.crates[0].name, "other_sort"); - assert_eq!(json.crates[1].name, "baz_sort"); - assert_eq!(json.crates[2].name, "bar_sort"); - assert_eq!(json.crates[3].name, "foo_sort"); - - // Sort by new - let json = anon.search("sort=new"); - assert_eq!(json.meta.total, 4); - assert_eq!(json.crates[0].name, "bar_sort"); - assert_eq!(json.crates[1].name, "baz_sort"); - assert_eq!(json.crates[2].name, "other_sort"); - assert_eq!(json.crates[3].name, "foo_sort"); - - // Test for bug with showing null results first when sorting - // by descending downloads - let json = anon.search("sort=recent-downloads"); - assert_eq!(json.meta.total, 4); - assert_eq!(json.crates[0].name, "foo_sort"); - assert_eq!(json.crates[1].name, "baz_sort"); - assert_eq!(json.crates[2].name, "bar_sort"); - assert_eq!(json.crates[3].name, "other_sort"); -} - -#[test] -#[allow(clippy::cognitive_complexity)] -fn exact_match_on_queries_with_sort() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - let krate1 = CrateBuilder::new("foo_sort", user.id) - .description("bar_sort baz_sort const") - .downloads(50) - .recent_downloads(50) - .expect_build(conn); - - let krate2 = CrateBuilder::new("bar_sort", user.id) - .description("foo_sort baz_sort foo_sort baz_sort const") - .downloads(3333) - .recent_downloads(0) - .expect_build(conn); - - let krate3 = CrateBuilder::new("baz_sort", user.id) - .description("foo_sort bar_sort foo_sort bar_sort foo_sort bar_sort const") - .downloads(100_000) - .recent_downloads(10) - .expect_build(conn); - - let krate4 = CrateBuilder::new("other_sort", user.id) - .description("other_sort const") - .downloads(999_999) - .expect_build(conn); - - // Set the created at column for each crate - update(&krate1) - .set(crates::created_at.eq(now - 4.weeks())) - .execute(conn) - .unwrap(); - update(&krate2) - .set(crates::created_at.eq(now - 1.weeks())) - .execute(conn) - .unwrap(); - update(&krate3) - .set(crates::created_at.eq(now - 2.weeks())) - .execute(conn) - .unwrap(); - update(&krate4) - .set(crates::created_at.eq(now - 3.weeks())) - .execute(conn) - .unwrap(); - - // Set the updated at column for each crate - update(&krate1) - .set(crates::updated_at.eq(now - 3.weeks())) - .execute(&*conn) - .unwrap(); - update(&krate2) - .set(crates::updated_at.eq(now - 5.days())) - .execute(&*conn) - .unwrap(); - update(&krate3) - .set(crates::updated_at.eq(now - 10.seconds())) - .execute(&*conn) - .unwrap(); - update(&krate4) - .set(crates::updated_at.eq(now)) - .execute(&*conn) - .unwrap(); - }); - - // Sort by downloads - let json = anon.search("q=foo_sort&sort=downloads"); - assert_eq!(json.meta.total, 3); - assert_eq!(json.crates[0].name, "foo_sort"); - assert_eq!(json.crates[1].name, "baz_sort"); - assert_eq!(json.crates[2].name, "bar_sort"); - - let json = anon.search("q=bar_sort&sort=downloads"); - assert_eq!(json.meta.total, 3); - assert_eq!(json.crates[0].name, "bar_sort"); - assert_eq!(json.crates[1].name, "baz_sort"); - assert_eq!(json.crates[2].name, "foo_sort"); - - let json = anon.search("q=baz_sort&sort=downloads"); - assert_eq!(json.meta.total, 3); - assert_eq!(json.crates[0].name, "baz_sort"); - assert_eq!(json.crates[1].name, "bar_sort"); - assert_eq!(json.crates[2].name, "foo_sort"); - - let json = anon.search("q=const&sort=downloads"); - assert_eq!(json.meta.total, 4); - assert_eq!(json.crates[0].name, "other_sort"); - assert_eq!(json.crates[1].name, "baz_sort"); - assert_eq!(json.crates[2].name, "bar_sort"); - assert_eq!(json.crates[3].name, "foo_sort"); - - // Sort by recent-downloads - let json = anon.search("q=bar_sort&sort=recent-downloads"); - assert_eq!(json.meta.total, 3); - assert_eq!(json.crates[0].name, "bar_sort"); - assert_eq!(json.crates[1].name, "foo_sort"); - assert_eq!(json.crates[2].name, "baz_sort"); - - // Sort by recent-updates - let json = anon.search("q=bar_sort&sort=recent-updates"); - assert_eq!(json.meta.total, 3); - assert_eq!(json.crates[0].name, "baz_sort"); - assert_eq!(json.crates[1].name, "bar_sort"); - assert_eq!(json.crates[2].name, "foo_sort"); - - // Sort by new - let json = anon.search("q=bar_sort&sort=new"); - assert_eq!(json.meta.total, 3); - assert_eq!(json.crates[0].name, "bar_sort"); - assert_eq!(json.crates[1].name, "baz_sort"); - assert_eq!(json.crates[2].name, "foo_sort"); - - // Test for bug with showing null results first when sorting - // by descending downloads - let json = anon.search("sort=recent-downloads"); - assert_eq!(json.meta.total, 4); - assert_eq!(json.crates[0].name, "foo_sort"); - assert_eq!(json.crates[1].name, "baz_sort"); - assert_eq!(json.crates[2].name, "bar_sort"); - assert_eq!(json.crates[3].name, "other_sort"); -} - -#[test] -fn loose_search_order() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - let ordered = app.db(|conn| { - // exact match should be first - let one = CrateBuilder::new("temp", user.id) - .readme("readme") - .description("description") - .keyword("kw1") - .expect_build(conn); - // temp_udp should match second because of _ - let two = CrateBuilder::new("temp_utp", user.id) - .readme("readme") - .description("description") - .keyword("kw1") - .expect_build(conn); - // evalrs should match 3rd because of readme - let three = CrateBuilder::new("evalrs", user.id) - .readme("evalrs_temp evalrs_temp evalrs_temp") - .description("description") - .keyword("kw1") - .expect_build(conn); - // tempfile should appear 4th - let four = CrateBuilder::new("tempfile", user.id) - .readme("readme") - .description("description") - .keyword("kw1") - .expect_build(conn); - vec![one, two, three, four] - }); - let search_temp = anon.search("q=temp"); - assert_eq!(search_temp.meta.total, 4); - assert_eq!(search_temp.crates.len(), 4); - for (lhs, rhs) in search_temp.crates.iter().zip(ordered) { - assert_eq!(lhs.name, rhs.name); - } - - let search_temp = anon.search("q=te"); - assert_eq!(search_temp.meta.total, 3); - assert_eq!(search_temp.crates.len(), 3); -} - -#[test] -fn index_include_yanked() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - CrateBuilder::new("unyanked", user.id) - .version(VersionBuilder::new("1.0.0")) - .version(VersionBuilder::new("2.0.0")) - .expect_build(conn); - - CrateBuilder::new("newest_yanked", user.id) - .version(VersionBuilder::new("1.0.0")) - .version(VersionBuilder::new("2.0.0").yanked(true)) - .expect_build(conn); - - CrateBuilder::new("oldest_yanked", user.id) - .version(VersionBuilder::new("1.0.0").yanked(true)) - .version(VersionBuilder::new("2.0.0")) - .expect_build(conn); - - CrateBuilder::new("all_yanked", user.id) - .version(VersionBuilder::new("1.0.0").yanked(true)) - .version(VersionBuilder::new("2.0.0").yanked(true)) - .expect_build(conn); - }); - - // Include fully yanked (all versions were yanked) crates - let json = anon.search("include_yanked=yes&sort=alphabetical"); - assert_eq!(json.meta.total, 4); - assert_eq!(json.crates[0].name, "all_yanked"); - assert_eq!(json.crates[1].name, "newest_yanked"); - assert_eq!(json.crates[2].name, "oldest_yanked"); - assert_eq!(json.crates[3].name, "unyanked"); - - // Do not include fully yanked (all versions were yanked) crates - let json = anon.search("include_yanked=no&sort=alphabetical"); - assert_eq!(json.meta.total, 3); - assert_eq!(json.crates[0].name, "newest_yanked"); - assert_eq!(json.crates[1].name, "oldest_yanked"); - assert_eq!(json.crates[2].name, "unyanked"); -} - -#[test] -fn show() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - let krate = app.db(|conn| { - let krate = CrateBuilder::new("foo_show", user.id) - .description("description") - .documentation("https://example.com") - .homepage("http://example.com") - .version(VersionBuilder::new("1.0.0")) - .version(VersionBuilder::new("0.5.0")) - .version(VersionBuilder::new("0.5.1")) - .keyword("kw1") - .downloads(20) - .recent_downloads(10) - .expect_build(conn); - - // Make version 1.0.0 mimic a version published before we started recording who published - // versions - let none: Option = None; - update(versions::table) - .filter(versions::num.eq("1.0.0")) - .set(versions::published_by.eq(none)) - .execute(conn) - .unwrap(); - - krate - }); - - let json = anon.show_crate("foo_show"); - assert_eq!(json.krate.name, krate.name); - assert_eq!(json.krate.id, krate.name); - assert_eq!(json.krate.description, krate.description); - assert_eq!(json.krate.homepage, krate.homepage); - assert_eq!(json.krate.documentation, krate.documentation); - assert_eq!(json.krate.keywords, Some(vec!["kw1".into()])); - assert_eq!(json.krate.recent_downloads, Some(10)); - let versions = json.krate.versions.as_ref().unwrap(); - assert_eq!(versions.len(), 3); - assert_eq!(json.versions.len(), 3); - - assert_eq!(json.versions[0].id, versions[0]); - assert_eq!(json.versions[0].krate, json.krate.id); - assert_eq!(json.versions[0].num, "1.0.0"); - assert_none!(&json.versions[0].published_by); - let suffix = "/api/v1/crates/foo_show/1.0.0/download"; - assert!( - json.versions[0].dl_path.ends_with(suffix), - "bad suffix {}", - json.versions[0].dl_path - ); - assert_eq!(1, json.keywords.len()); - assert_eq!("kw1", json.keywords[0].id); - - assert_eq!(json.versions[1].num, "0.5.1"); - assert_eq!(json.versions[2].num, "0.5.0"); - assert_eq!( - json.versions[1].published_by.as_ref().unwrap().login, - user.gh_login - ); -} - -#[test] -fn yanked_versions_are_not_considered_for_max_version() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - CrateBuilder::new("foo_yanked_version", user.id) - .description("foo") - .version("1.0.0") - .version(VersionBuilder::new("1.1.0").yanked(true)) - .expect_build(conn); - }); - - let json = anon.search("q=foo"); - assert_eq!(json.meta.total, 1); - assert_eq!(json.crates[0].max_version, "1.0.0"); -} - -#[test] -fn versions() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - app.db(|conn| { - CrateBuilder::new("foo_versions", user.id) - .version("0.5.1") - .version("1.0.0") - .version("0.5.0") - .expect_build(conn); - // Make version 1.0.0 mimic a version published before we started recording who published - // versions - let none: Option = None; - update(versions::table) - .filter(versions::num.eq("1.0.0")) - .set(versions::published_by.eq(none)) - .execute(conn) - .unwrap(); - }); - - let json: VersionsList = anon.get("/api/v1/crates/foo_versions/versions").good(); - - assert_eq!(json.versions.len(), 3); - assert_eq!(json.versions[0].num, "1.0.0"); - assert_eq!(json.versions[1].num, "0.5.1"); - assert_eq!(json.versions[2].num, "0.5.0"); - assert_none!(&json.versions[0].published_by); - assert_eq!( - json.versions[1].published_by.as_ref().unwrap().login, - user.gh_login - ); -} - -#[test] -fn uploading_new_version_touches_crate() { - use diesel::dsl::*; - - let (app, _, user) = TestApp::full().with_user(); - - let crate_to_publish = PublishBuilder::new("foo_versions_updated_at").version("1.0.0"); - user.enqueue_publish(crate_to_publish).good(); - - app.db(|conn| { - diesel::update(crates::table) - .set(crates::updated_at.eq(crates::updated_at - 1.hour())) - .execute(&*conn) - .unwrap(); - }); - - let json: CrateResponse = user.show_crate("foo_versions_updated_at"); - let updated_at_before = json.krate.updated_at; - - let crate_to_publish = PublishBuilder::new("foo_versions_updated_at").version("2.0.0"); - user.enqueue_publish(crate_to_publish).good(); - - let json: CrateResponse = user.show_crate("foo_versions_updated_at"); - let updated_at_after = json.krate.updated_at; - - assert_ne!(updated_at_before, updated_at_after); -} - -#[test] -fn new_wrong_token() { - let (app, anon, _, token) = TestApp::init().with_token(); - - // Try to publish without a token - let crate_to_publish = PublishBuilder::new("foo"); - let json = anon - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::FORBIDDEN); - assert!( - json.errors[0] - .detail - .contains("must be logged in to perform that action"), - "{:?}", - json.errors - ); - - // Try to publish with the wrong token (by changing the token in the database) - app.db(|conn| { - diesel::update(api_tokens::table) - .set(api_tokens::token.eq(b"bad" as &[u8])) - .execute(conn) - .unwrap(); - }); - - let crate_to_publish = PublishBuilder::new("foo"); - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::FORBIDDEN); - assert!( - json.errors[0] - .detail - .contains("must be logged in to perform that action"), - "{:?}", - json.errors - ); -} - -#[test] -fn invalid_names() { - let (_, _, _, token) = TestApp::init().with_token(); - - let bad_name = |name: &str, error_message: &str| { - let crate_to_publish = PublishBuilder::new(name).version("1.0.0"); - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - - assert!( - json.errors[0].detail.contains(error_message,), - "{:?}", - json.errors - ); - }; - - let error_message = "expected a valid crate name"; - bad_name("", error_message); - bad_name("foo bar", error_message); - bad_name(&"a".repeat(MAX_NAME_LENGTH + 1), error_message); - bad_name("snow☃", error_message); - bad_name("áccênts", error_message); - - let error_message = "cannot upload a crate with a reserved name"; - bad_name("std", error_message); - bad_name("STD", error_message); - bad_name("compiler-rt", error_message); - bad_name("compiler_rt", error_message); - bad_name("coMpiLer_Rt", error_message); -} - -#[test] -fn new_krate() { - let (_, _, user) = TestApp::full().with_user(); - - let crate_to_publish = PublishBuilder::new("foo_new").version("1.0.0"); - let json: GoodCrate = user.enqueue_publish(crate_to_publish).good(); - - assert_eq!(json.krate.name, "foo_new"); - assert_eq!(json.krate.max_version, "1.0.0"); -} - -#[test] -fn new_krate_with_token() { - let (_, _, _, token) = TestApp::full().with_token(); - - let crate_to_publish = PublishBuilder::new("foo_new").version("1.0.0"); - let json: GoodCrate = token.enqueue_publish(crate_to_publish).good(); - - assert_eq!(json.krate.name, "foo_new"); - assert_eq!(json.krate.max_version, "1.0.0"); -} - -#[test] -fn new_krate_weird_version() { - let (_, _, _, token) = TestApp::full().with_token(); - - let crate_to_publish = PublishBuilder::new("foo_weird").version("0.0.0-pre"); - let json: GoodCrate = token.enqueue_publish(crate_to_publish).good(); - - assert_eq!(json.krate.name, "foo_weird"); - assert_eq!(json.krate.max_version, "0.0.0-pre"); -} - -#[test] -fn new_with_renamed_dependency() { - let (app, _, user, token) = TestApp::full().with_token(); - - app.db(|conn| { - // Insert a crate directly into the database so that new-krate can depend on it - CrateBuilder::new("package-name", user.as_model().id).expect_build(conn); - }); - - let dependency = DependencyBuilder::new("package-name").rename("my-name"); - - let crate_to_publish = PublishBuilder::new("new-krate") - .version("1.0.0") - .dependency(dependency); - token.enqueue_publish(crate_to_publish).good(); - app.run_pending_background_jobs(); - - let crates = app.crates_from_index_head("ne/w-/new-krate"); - assert_eq!(crates.len(), 1); - assert_eq!(crates[0].name, "new-krate"); - assert_eq!(crates[0].vers, "1.0.0"); - assert_eq!(crates[0].deps.len(), 1); - assert_eq!(crates[0].deps[0].name, "my-name"); - assert_eq!(crates[0].deps[0].package.as_ref().unwrap(), "package-name"); -} - -#[test] -fn new_krate_with_dependency() { - let (app, _, user, token) = TestApp::full().with_token(); - - app.db(|conn| { - // Insert a crate directly into the database so that new_dep can depend on it - // The name choice of `foo-dep` is important! It has the property of - // name != canon_crate_name(name) and is a regression test for - // https://github.com/rust-lang/crates.io/issues/651 - CrateBuilder::new("foo-dep", user.as_model().id).expect_build(conn); - }); - - let dependency = DependencyBuilder::new("foo-dep"); - - let crate_to_publish = PublishBuilder::new("new_dep") - .version("1.0.0") - .dependency(dependency); - token.enqueue_publish(crate_to_publish).good(); -} - -#[test] -fn reject_new_krate_with_non_exact_dependency() { - let (app, _, user, token) = TestApp::init().with_token(); - - app.db(|conn| { - CrateBuilder::new("foo-dep", user.as_model().id).expect_build(conn); - }); - - // Use non-exact name for the dependency - let dependency = DependencyBuilder::new("foo_dep"); - - let crate_to_publish = PublishBuilder::new("new_dep") - .version("1.0.0") - .dependency(dependency); - token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); -} - -#[test] -fn new_crate_allow_empty_alternative_registry_dependency() { - let (app, _, user, token) = TestApp::full().with_token(); - - app.db(|conn| { - CrateBuilder::new("foo-dep", user.as_model().id).expect_build(conn); - }); - - let dependency = DependencyBuilder::new("foo-dep").registry(""); - let crate_to_publish = PublishBuilder::new("foo").dependency(dependency); - token.enqueue_publish(crate_to_publish).good(); -} - -#[test] -fn reject_new_crate_with_alternative_registry_dependency() { - let (_, _, _, token) = TestApp::init().with_token(); - - let dependency = - DependencyBuilder::new("dep").registry("https://server.example/path/to/registry"); - - let crate_to_publish = PublishBuilder::new("depends-on-alt-registry").dependency(dependency); - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - assert!( - json.errors[0] - .detail - .contains("Cross-registry dependencies are not permitted on crates.io."), - "{:?}", - json.errors - ); -} - -#[test] -fn new_krate_with_wildcard_dependency() { - let (app, _, user, token) = TestApp::init().with_token(); - - app.db(|conn| { - // Insert a crate directly into the database so that new_wild can depend on it - CrateBuilder::new("foo_wild", user.as_model().id).expect_build(conn); - }); - - let dependency = DependencyBuilder::new("foo_wild").version_req("*"); - - let crate_to_publish = PublishBuilder::new("new_wild") - .version("1.0.0") - .dependency(dependency); - - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - assert!( - json.errors[0].detail.contains("dependency constraints"), - "{:?}", - json.errors - ); -} - -#[test] -fn new_krate_twice() { - let (app, _, user, token) = TestApp::full().with_token(); - - app.db(|conn| { - // Insert a crate directly into the database and then we'll try to publish another version - CrateBuilder::new("foo_twice", user.as_model().id).expect_build(conn); - }); - - let crate_to_publish = PublishBuilder::new("foo_twice") - .version("2.0.0") - .description("2.0.0 description"); - let json = token.enqueue_publish(crate_to_publish).good(); - - assert_eq!(json.krate.name, "foo_twice"); - assert_eq!(json.krate.description.unwrap(), "2.0.0 description"); -} - -#[test] -fn new_krate_wrong_user() { - let (app, _, user) = TestApp::init().with_user(); - - app.db(|conn| { - // Create the foo_wrong crate with one user - CrateBuilder::new("foo_wrong", user.as_model().id).expect_build(conn); - }); - - // Then try to publish with a different user - let another_user = app.db_new_user("another").db_new_token("bar"); - let crate_to_publish = PublishBuilder::new("foo_wrong").version("2.0.0"); - - let json = another_user - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - assert!( - json.errors[0] - .detail - .contains("this crate exists but you don't seem to be an owner."), - "{:?}", - json.errors - ); -} - -#[test] -fn new_krate_too_big() { - let (_, _, user) = TestApp::init().with_user(); - - let files = [("foo_big-1.0.0/big", &[b'a'; 2000] as &[_])]; - let builder = PublishBuilder::new("foo_big").files(&files); - - let json = user - .enqueue_publish(builder) - .bad_with_status(StatusCode::OK); - assert!( - json.errors[0] - .detail - .contains("uploaded tarball is malformed or too large when decompressed"), - "{:?}", - json.errors - ); -} - -#[test] -fn new_krate_too_big_but_whitelisted() { - let (app, _, user, token) = TestApp::full().with_token(); - - app.db(|conn| { - CrateBuilder::new("foo_whitelist", user.as_model().id) - .max_upload_size(2_000_000) - .expect_build(conn); - }); - - let files = [("foo_whitelist-1.1.0/big", &[b'a'; 2000] as &[_])]; - let crate_to_publish = PublishBuilder::new("foo_whitelist") - .version("1.1.0") - .files(&files); - - token.enqueue_publish(crate_to_publish).good(); -} - -#[test] -fn new_krate_wrong_files() { - let (_, _, user) = TestApp::init().with_user(); - let data: &[u8] = &[1]; - let files = [("foo-1.0.0/a", data), ("bar-1.0.0/a", data)]; - let builder = PublishBuilder::new("foo").files(&files); - - let json = user - .enqueue_publish(builder) - .bad_with_status(StatusCode::OK); - assert!( - json.errors[0].detail.contains("invalid tarball uploaded"), - "{:?}", - json.errors - ); -} - -#[test] -fn new_krate_gzip_bomb() { - let (_, _, _, token) = TestApp::init().with_token(); - - let len = 512 * 1024; - let mut body = io::repeat(0).take(len); - - let crate_to_publish = PublishBuilder::new("foo") - .version("1.1.0") - .files_with_io(&mut [("foo-1.1.0/a", &mut body, len)]); - - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - - assert!( - json.errors[0] - .detail - .contains("too large when decompressed"), - "{:?}", - json.errors - ); -} - -#[test] -fn new_krate_duplicate_version() { - let (app, _, user, token) = TestApp::init().with_token(); - - app.db(|conn| { - // Insert a crate directly into the database and then we'll try to publish the same version - CrateBuilder::new("foo_dupe", user.as_model().id) - .version("1.0.0") - .expect_build(conn); - }); - - let crate_to_publish = PublishBuilder::new("foo_dupe").version("1.0.0"); - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - - assert!( - json.errors[0].detail.contains("already uploaded"), - "{:?}", - json.errors - ); -} - -#[test] -fn new_crate_similar_name() { - let (app, _, user, token) = TestApp::init().with_token(); - - app.db(|conn| { - CrateBuilder::new("Foo_similar", user.as_model().id) - .version("1.0.0") - .expect_build(conn); - }); - - let crate_to_publish = PublishBuilder::new("foo_similar").version("1.1.0"); - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - - assert!( - json.errors[0].detail.contains("previously named"), - "{:?}", - json.errors - ); -} - -#[test] -fn new_crate_similar_name_hyphen() { - let (app, _, user, token) = TestApp::init().with_token(); - - app.db(|conn| { - CrateBuilder::new("foo_bar_hyphen", user.as_model().id) - .version("1.0.0") - .expect_build(conn); - }); - - let crate_to_publish = PublishBuilder::new("foo-bar-hyphen").version("1.1.0"); - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - - assert!( - json.errors[0].detail.contains("previously named"), - "{:?}", - json.errors - ); -} - -#[test] -fn new_crate_similar_name_underscore() { - let (app, _, user, token) = TestApp::init().with_token(); - - app.db(|conn| { - CrateBuilder::new("foo-bar-underscore", user.as_model().id) - .version("1.0.0") - .expect_build(conn); - }); - - let crate_to_publish = PublishBuilder::new("foo_bar_underscore").version("1.1.0"); - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - - assert!( - json.errors[0].detail.contains("previously named"), - "{:?}", - json.errors - ); -} - -#[test] -fn new_krate_git_upload() { - let (app, _, _, token) = TestApp::full().with_token(); - - let crate_to_publish = PublishBuilder::new("fgt"); - token.enqueue_publish(crate_to_publish).good(); - app.run_pending_background_jobs(); - - let crates = app.crates_from_index_head("3/f/fgt"); - assert_eq!(crates.len(), 1); - assert_eq!(crates[0].name, "fgt"); - assert_eq!(crates[0].vers, "1.0.0"); - assert!(crates[0].deps.is_empty()); - assert_eq!( - crates[0].cksum, - "acb5604b126ac894c1eb11c4575bf2072fea61232a888e453770c79d7ed56419" - ); -} - -#[test] -fn new_krate_git_upload_appends() { - let (app, _, _, token) = TestApp::full().with_token(); - - let crate_to_publish = PublishBuilder::new("FPP").version("0.0.1"); - token.enqueue_publish(crate_to_publish).good(); - let crate_to_publish = PublishBuilder::new("FPP").version("1.0.0"); - token.enqueue_publish(crate_to_publish).good(); - app.run_pending_background_jobs(); - - let crates = app.crates_from_index_head("3/f/fpp"); - assert!(crates.len() == 2); - assert_eq!(crates[0].name, "FPP"); - assert_eq!(crates[0].vers, "0.0.1"); - assert!(crates[0].deps.is_empty()); - assert_eq!(crates[1].name, "FPP"); - assert_eq!(crates[1].vers, "1.0.0"); - assert!(crates[1].deps.is_empty()); -} - -#[test] -fn new_krate_git_upload_with_conflicts() { - let (app, _, _, token) = TestApp::full().with_token(); - - let index = app.upstream_repository(); - let target = index.head().unwrap().target().unwrap(); - let sig = index.signature().unwrap(); - let parent = index.find_commit(target).unwrap(); - let tree = index.find_tree(parent.tree_id()).unwrap(); - index - .commit(Some("HEAD"), &sig, &sig, "empty commit", &tree, &[&parent]) - .unwrap(); - - let crate_to_publish = PublishBuilder::new("foo_conflicts"); - token.enqueue_publish(crate_to_publish).good(); -} - -#[test] -fn new_krate_dependency_missing() { - let (_, _, _, token) = TestApp::init().with_token(); - - // Deliberately not inserting this crate in the database to test behavior when a dependency - // doesn't exist! - let dependency = DependencyBuilder::new("bar_missing"); - let crate_to_publish = PublishBuilder::new("foo_missing").dependency(dependency); - - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - assert!( - json.errors[0] - .detail - .contains("no known crate named `bar_missing`"), - "{:?}", - json.errors - ); -} - -#[test] -fn new_krate_with_readme() { - let (_, _, _, token) = TestApp::full().with_token(); - - let crate_to_publish = PublishBuilder::new("foo_readme").readme(""); - let json = token.enqueue_publish(crate_to_publish).good(); - - assert_eq!(json.krate.name, "foo_readme"); - assert_eq!(json.krate.max_version, "1.0.0"); -} - -#[test] -fn new_krate_without_any_email_fails() { - let (app, _, _, token) = TestApp::init().with_token(); - - app.db(|conn| { - delete(emails::table).execute(conn).unwrap(); - }); - - let crate_to_publish = PublishBuilder::new("foo_no_email"); - - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - - assert!( - json.errors[0] - .detail - .contains("A verified email address is required to publish crates to crates.io"), - "{:?}", - json.errors - ); -} - -#[test] -fn new_krate_with_unverified_email_fails() { - let (app, _, _, token) = TestApp::init().with_token(); - - app.db(|conn| { - update(emails::table) - .set((emails::verified.eq(false),)) - .execute(conn) - .unwrap(); - }); - - let crate_to_publish = PublishBuilder::new("foo_unverified_email"); - - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - - assert!( - json.errors[0] - .detail - .contains("A verified email address is required to publish crates to crates.io"), - "{:?}", - json.errors - ); -} - -#[test] -fn new_krate_records_verified_email() { - let (app, _, _, token) = TestApp::full().with_token(); - - let crate_to_publish = PublishBuilder::new("foo_verified_email"); - - token.enqueue_publish(crate_to_publish).good(); - - app.db(|conn| { - let email: String = versions_published_by::table - .select(versions_published_by::email) - .first(conn) - .unwrap(); - assert_eq!(email, "something@example.com"); - }); -} - -#[test] -fn summary_doesnt_die() { - let (_, anon) = TestApp::init().empty(); - anon.get::("/api/v1/summary").good(); -} - -#[test] -fn summary_new_crates() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - app.db(|conn| { - let now_ = Utc::now().naive_utc(); - let now_plus_two = now_ + chrono::Duration::seconds(2); - - new_category("Category 1", "cat1", "Category 1 crates") - .create_or_update(conn) - .unwrap(); - - CrateBuilder::new("some_downloads", user.id) - .version(VersionBuilder::new("0.1.0")) - .description("description") - .keyword("popular") - .category("cat1") - .downloads(20) - .recent_downloads(10) - .expect_build(conn); - - CrateBuilder::new("most_recent_downloads", user.id) - .version(VersionBuilder::new("0.2.0")) - .keyword("popular") - .category("cat1") - .downloads(5000) - .recent_downloads(50) - .expect_build(conn); - - CrateBuilder::new("just_updated", user.id) - .version(VersionBuilder::new("0.1.0")) - .version(VersionBuilder::new("0.1.2")) - // update 'just_updated' krate. Others won't appear because updated_at == created_at. - .updated_at(now_) - .expect_build(conn); - - CrateBuilder::new("just_updated_patch", user.id) - .version(VersionBuilder::new("0.1.0")) - .version(VersionBuilder::new("0.2.0")) - // Add a patch version be newer than the other versions, including the higher one. - .version(VersionBuilder::new("0.1.1").created_at(now_plus_two)) - .updated_at(now_plus_two) - .expect_build(conn); - - CrateBuilder::new("with_downloads", user.id) - .version(VersionBuilder::new("0.3.0")) - .keyword("popular") - .downloads(1000) - .expect_build(conn); - - // set total_downloads global value for `num_downloads` prop - update(metadata::table) - .set(metadata::total_downloads.eq(6000)) - .execute(&*conn) - .unwrap(); - }); - - let json: SummaryResponse = anon.get("/api/v1/summary").good(); - - assert_eq!(json.num_crates, 5); - assert_eq!(json.num_downloads, 6000); - assert_eq!(json.most_downloaded[0].name, "most_recent_downloads"); - assert_eq!(json.most_downloaded[0].downloads, 5000); - assert_eq!(json.most_downloaded[0].recent_downloads, Some(50)); - assert_eq!( - json.most_recently_downloaded[0].name, - "most_recent_downloads" - ); - assert_eq!(json.most_recently_downloaded[0].recent_downloads, Some(50)); - assert_eq!(json.popular_keywords[0].keyword, "popular"); - assert_eq!(json.popular_categories[0].category, "Category 1"); - assert_eq!(json.just_updated.len(), 2); - - assert_eq!(json.just_updated[0].name, "just_updated_patch"); - assert_eq!(json.just_updated[0].max_version, "0.2.0"); - assert_eq!(json.just_updated[0].newest_version, "0.1.1"); - - assert_eq!(json.just_updated[1].name, "just_updated"); - assert_eq!(json.just_updated[1].max_version, "0.1.2"); - assert_eq!(json.just_updated[1].newest_version, "0.1.2"); - - assert_eq!(json.new_crates.len(), 5); -} - -#[test] -fn download() { - use chrono::{Duration, Utc}; - 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 assert_dl_count = |name_and_version: &str, query: Option<&str>, count: i32| { - let url = format!("/api/v1/crates/{}/downloads", name_and_version); - 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); - }; - - let download = |name_and_version: &str| { - let url = format!("/api/v1/crates/{}/download", name_and_version); - anon.get::<()>(&url).assert_status(StatusCode::FOUND); - // TODO: test the with_json code path - }; - - download("foo_download/1.0.0"); - assert_dl_count("foo_download/1.0.0", None, 1); - assert_dl_count("foo_download", None, 1); - - download("FOO_DOWNLOAD/1.0.0"); - assert_dl_count("FOO_DOWNLOAD/1.0.0", None, 2); - assert_dl_count("FOO_DOWNLOAD", None, 2); - - let yesterday = (Utc::today() + Duration::days(-1)).format("%F"); - let query = format!("before_date={}", yesterday); - assert_dl_count("FOO_DOWNLOAD/1.0.0", Some(&query), 0); - // crate/downloads always returns the last 90 days and ignores date params - assert_dl_count("FOO_DOWNLOAD", Some(&query), 2); - - let tomorrow = (Utc::today() + Duration::days(1)).format("%F"); - let query = format!("before_date={}", tomorrow); - assert_dl_count("FOO_DOWNLOAD/1.0.0", Some(&query), 2); - assert_dl_count("FOO_DOWNLOAD", Some(&query), 2); -} - -#[test] -fn download_nonexistent_version_of_existing_crate_404s() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - CrateBuilder::new("foo_bad", user.id).expect_build(conn); - }); - - anon.get("/api/v1/crates/foo_bad/0.1.0/download") - .assert_not_found(); -} - -#[test] -fn download_noncanonical_crate_name() { - 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); - }); - - // Request download for "foo-download" with a dash instead of an underscore, - // and assert that the correct download link is returned. - anon.get::<()>("/api/v1/crates/foo-download/1.0.0/download") - .assert_redirect_ends_with("/crates/foo_download/foo_download-1.0.0.crate"); -} - -#[test] -fn dependencies() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - let c1 = CrateBuilder::new("foo_deps", user.id).expect_build(conn); - let v = VersionBuilder::new("1.0.0").expect_build(c1.id, user.id, conn); - let c2 = CrateBuilder::new("bar_deps", user.id).expect_build(conn); - new_dependency(conn, &v, &c2); - }); - - let deps: Deps = anon - .get("/api/v1/crates/foo_deps/1.0.0/dependencies") - .good(); - assert_eq!(deps.dependencies[0].crate_id, "bar_deps"); - - anon.get::<()>("/api/v1/crates/foo_deps/1.0.2/dependencies") - .bad_with_status(StatusCode::OK); -} - -#[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? - let (app, _, user) = TestApp::init().with_user(); - - app.db(|conn| { - CrateBuilder::new("foo_following", user.as_model().id).expect_build(conn); - }); - - let is_following = || -> bool { - #[derive(Deserialize)] - struct F { - following: bool, - } - - user.get::("/api/v1/crates/foo_following/following") - .good() - .following - }; - - let follow = || { - assert!( - user.put::("/api/v1/crates/foo_following/follow", b"") - .good() - .ok - ); - }; - - let unfollow = || { - assert!( - user.delete::("api/v1/crates/foo_following/follow") - .good() - .ok - ); - }; - - assert!(!is_following()); - follow(); - follow(); - assert!(is_following()); - assert_eq!(user.search("following=1").crates.len(), 1); - - unfollow(); - unfollow(); - assert!(!is_following()); - assert_eq!(user.search("following=1").crates.len(), 0); -} - -#[test] -fn yank_works_as_intended() { - let (app, anon, cookie, token) = TestApp::full().with_token(); - - // Upload a new crate, putting it in the git index - let crate_to_publish = PublishBuilder::new("fyk"); - token.enqueue_publish(crate_to_publish).good(); - app.run_pending_background_jobs(); - - let crates = app.crates_from_index_head("3/f/fyk"); - assert_eq!(crates.len(), 1); - assert_some_eq!(crates[0].yanked, false); - - // make sure it's not yanked - let json = anon.show_version("fyk", "1.0.0"); - assert!(!json.version.yanked); - - // yank it - token.yank("fyk", "1.0.0").good(); - - let crates = app.crates_from_index_head("3/f/fyk"); - assert_eq!(crates.len(), 1); - assert_some_eq!(crates[0].yanked, true); - - let json = anon.show_version("fyk", "1.0.0"); - assert!(json.version.yanked); - - // un-yank it - token.unyank("fyk", "1.0.0").good(); - - let crates = app.crates_from_index_head("3/f/fyk"); - assert_eq!(crates.len(), 1); - assert_some_eq!(crates[0].yanked, false); - - let json = anon.show_version("fyk", "1.0.0"); - assert!(!json.version.yanked); - - // yank it - cookie.yank("fyk", "1.0.0").good(); - - let crates = app.crates_from_index_head("3/f/fyk"); - assert_eq!(crates.len(), 1); - assert_some_eq!(crates[0].yanked, true); - - let json = anon.show_version("fyk", "1.0.0"); - assert!(json.version.yanked); - - // un-yank it - cookie.unyank("fyk", "1.0.0").good(); - - let crates = app.crates_from_index_head("3/f/fyk"); - assert_eq!(crates.len(), 1); - assert_some_eq!(crates[0].yanked, false); - - let json = anon.show_version("fyk", "1.0.0"); - assert!(!json.version.yanked); -} - -#[test] -fn yank_by_a_non_owner_fails() { - let (app, _, _, token) = TestApp::full().with_token(); - - let another_user = app.db_new_user("bar"); - let another_user = another_user.as_model(); - app.db(|conn| { - CrateBuilder::new("foo_not", another_user.id) - .version("1.0.0") - .expect_build(conn); - }); - - let json = token - .yank("foo_not", "1.0.0") - .bad_with_status(StatusCode::OK); - assert_eq!( - json.errors[0].detail, - "must already be an owner to yank or unyank" - ); -} - -#[test] -fn yank_max_version() { - let (_, anon, _, token) = TestApp::full().with_token(); - - // Upload a new crate - let crate_to_publish = PublishBuilder::new("fyk_max"); - token.enqueue_publish(crate_to_publish).good(); - - // double check the max version - let json = anon.show_crate("fyk_max"); - assert_eq!(json.krate.max_version, "1.0.0"); - - // add version 2.0.0 - let crate_to_publish = PublishBuilder::new("fyk_max").version("2.0.0"); - let json = token.enqueue_publish(crate_to_publish).good(); - assert_eq!(json.krate.max_version, "2.0.0"); - - // yank version 1.0.0 - token.yank("fyk_max", "1.0.0").good(); - - let json = anon.show_crate("fyk_max"); - assert_eq!(json.krate.max_version, "2.0.0"); - - // unyank version 1.0.0 - token.unyank("fyk_max", "1.0.0").good(); - - let json = anon.show_crate("fyk_max"); - assert_eq!(json.krate.max_version, "2.0.0"); - - // yank version 2.0.0 - token.yank("fyk_max", "2.0.0").good(); - - let json = anon.show_crate("fyk_max"); - assert_eq!(json.krate.max_version, "1.0.0"); - - // yank version 1.0.0 - token.yank("fyk_max", "1.0.0").good(); - - let json = anon.show_crate("fyk_max"); - assert_eq!(json.krate.max_version, "0.0.0"); - - // unyank version 2.0.0 - token.unyank("fyk_max", "2.0.0").good(); - - let json = anon.show_crate("fyk_max"); - assert_eq!(json.krate.max_version, "2.0.0"); - - // unyank version 1.0.0 - token.unyank("fyk_max", "1.0.0").good(); - - let json = anon.show_crate("fyk_max"); - assert_eq!(json.krate.max_version, "2.0.0"); -} - -#[test] -fn publish_after_yank_max_version() { - let (_, anon, _, token) = TestApp::full().with_token(); - - // Upload a new crate - let crate_to_publish = PublishBuilder::new("fyk_max"); - token.enqueue_publish(crate_to_publish).good(); - - // double check the max version - let json = anon.show_crate("fyk_max"); - assert_eq!(json.krate.max_version, "1.0.0"); - - // yank version 1.0.0 - token.yank("fyk_max", "1.0.0").good(); - - let json = anon.show_crate("fyk_max"); - assert_eq!(json.krate.max_version, "0.0.0"); - - // add version 2.0.0 - let crate_to_publish = PublishBuilder::new("fyk_max").version("2.0.0"); - let json = token.enqueue_publish(crate_to_publish).good(); - assert_eq!(json.krate.max_version, "2.0.0"); - - // unyank version 1.0.0 - token.unyank("fyk_max", "1.0.0").good(); - - let json = anon.show_crate("fyk_max"); - assert_eq!(json.krate.max_version, "2.0.0"); -} - -#[test] -fn publish_records_an_audit_action() { - use cargo_registry::models::VersionOwnerAction; - - let (app, anon, _, token) = TestApp::full().with_token(); - - app.db(|conn| assert!(VersionOwnerAction::all(&conn).unwrap().is_empty())); - - // Upload a new crate, putting it in the git index - let crate_to_publish = PublishBuilder::new("fyk"); - token.enqueue_publish(crate_to_publish).good(); - - // Make sure it has one publish audit action - let json = anon.show_version("fyk", "1.0.0"); - let actions = json.version.audit_actions; - - assert_eq!(actions.len(), 1); - let action = &actions[0]; - assert_eq!(action.action, "publish"); - assert_eq!(action.user.id, token.as_model().user_id); -} - -#[test] -fn yank_records_an_audit_action() { - let (_, anon, _, token) = TestApp::full().with_token(); - - // Upload a new crate, putting it in the git index - let crate_to_publish = PublishBuilder::new("fyk"); - token.enqueue_publish(crate_to_publish).good(); - - // Yank it - token.yank("fyk", "1.0.0").good(); - - // Make sure it has one publish and one yank audit action - let json = anon.show_version("fyk", "1.0.0"); - let actions = json.version.audit_actions; - - assert_eq!(actions.len(), 2); - let action = &actions[1]; - assert_eq!(action.action, "yank"); - assert_eq!(action.user.id, token.as_model().user_id); -} - -#[test] -fn unyank_records_an_audit_action() { - let (_, anon, _, token) = TestApp::full().with_token(); - - // Upload a new crate - let crate_to_publish = PublishBuilder::new("fyk"); - token.enqueue_publish(crate_to_publish).good(); - - // Yank version 1.0.0 - token.yank("fyk", "1.0.0").good(); - - // Unyank version 1.0.0 - token.unyank("fyk", "1.0.0").good(); - - // Make sure it has one publish, one yank, and one unyank audit action - let json = anon.show_version("fyk", "1.0.0"); - let actions = json.version.audit_actions; - - assert_eq!(actions.len(), 3); - let action = &actions[2]; - assert_eq!(action.action, "unyank"); - assert_eq!(action.user.id, token.as_model().user_id); -} - -#[test] -fn publish_after_removing_documentation() { - let (app, anon, user, token) = TestApp::full().with_token(); - let user = user.as_model(); - - // 1. Start with a crate with no documentation - app.db(|conn| { - CrateBuilder::new("docscrate", user.id) - .version("0.2.0") - .expect_build(conn); - }); - - // Verify that crates start without any documentation so the next assertion can *prove* - // that it was the one that added the documentation - let json = anon.show_crate("docscrate"); - assert_eq!(json.krate.documentation, None); - - // 2. Add documentation - let crate_to_publish = PublishBuilder::new("docscrate") - .version("0.2.1") - .documentation("http://foo.rs"); - let json = token.enqueue_publish(crate_to_publish).good(); - assert_eq!(json.krate.documentation, Some("http://foo.rs".to_owned())); - - // Ensure latest version also has the same documentation - let json = anon.show_crate("docscrate"); - assert_eq!(json.krate.documentation, Some("http://foo.rs".to_owned())); - - // 3. Remove the documentation - let crate_to_publish = PublishBuilder::new("docscrate").version("0.2.2"); - let json = token.enqueue_publish(crate_to_publish).good(); - assert_eq!(json.krate.documentation, None); - - // Ensure latest version no longer has documentation - let json = anon.show_crate("docscrate"); - assert_eq!(json.krate.documentation, None); -} - -#[test] -fn bad_keywords() { - let (_, _, _, token) = TestApp::init().with_token(); - let crate_to_publish = - PublishBuilder::new("foo_bad_key").keyword("super-long-keyword-name-oh-no"); - token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - - let crate_to_publish = PublishBuilder::new("foo_bad_key").keyword("?@?%"); - token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - - let crate_to_publish = PublishBuilder::new("foo_bad_key").keyword("áccênts"); - token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); -} - -#[test] -fn good_categories() { - let (app, _, _, token) = TestApp::full().with_token(); - - app.db(|conn| { - new_category("Category 1", "cat1", "Category 1 crates") - .create_or_update(conn) - .unwrap(); - }); - - let crate_to_publish = PublishBuilder::new("foo_good_cat").category("cat1"); - let json = token.enqueue_publish(crate_to_publish).good(); - - assert_eq!(json.krate.name, "foo_good_cat"); - assert_eq!(json.krate.max_version, "1.0.0"); - assert_eq!(json.warnings.invalid_categories.len(), 0); -} - -#[test] -fn ignored_categories() { - let (_, _, _, token) = TestApp::full().with_token(); - - let crate_to_publish = PublishBuilder::new("foo_ignored_cat").category("bar"); - let json = token.enqueue_publish(crate_to_publish).good(); - - assert_eq!(json.krate.name, "foo_ignored_cat"); - assert_eq!(json.krate.max_version, "1.0.0"); - assert_eq!(json.warnings.invalid_categories, vec!["bar"]); -} - -#[test] -fn good_badges() { - let (_, anon, _, token) = TestApp::full().with_token(); - - let mut badges = HashMap::new(); - let mut badge_attributes = HashMap::new(); - badge_attributes.insert( - String::from("repository"), - String::from("rust-lang/crates.io"), - ); - badges.insert(String::from("travis-ci"), badge_attributes); - - let crate_to_publish = PublishBuilder::new("foobadger").badges(badges); - - let json = token.enqueue_publish(crate_to_publish).good(); - assert_eq!(json.krate.name, "foobadger"); - assert_eq!(json.krate.max_version, "1.0.0"); - - let json = anon.show_crate("foobadger"); - let badges = json.krate.badges.unwrap(); - assert_eq!(badges.len(), 1); - assert_eq!(badges[0].badge_type, "travis-ci"); - assert_eq!( - badges[0].attributes["repository"], - Some(String::from("rust-lang/crates.io")) - ); -} - -#[test] -fn ignored_badges() { - let (_, anon, _, token) = TestApp::full().with_token(); - - let mut badges = HashMap::new(); - - // Known badge type, missing required repository attribute - let mut badge_attributes = HashMap::new(); - badge_attributes.insert(String::from("branch"), String::from("master")); - badges.insert(String::from("travis-ci"), badge_attributes); - - // Unknown badge type - let mut unknown_badge_attributes = HashMap::new(); - unknown_badge_attributes.insert(String::from("repository"), String::from("rust-lang/rust")); - badges.insert(String::from("not-a-badge"), unknown_badge_attributes); - - let crate_to_publish = PublishBuilder::new("foo_ignored_badge").badges(badges); - - let json = token.enqueue_publish(crate_to_publish).good(); - assert_eq!(json.krate.name, "foo_ignored_badge"); - assert_eq!(json.krate.max_version, "1.0.0"); - assert_eq!(json.warnings.invalid_badges.len(), 2); - assert!(json - .warnings - .invalid_badges - .contains(&"travis-ci".to_string(),)); - assert!(json - .warnings - .invalid_badges - .contains(&"not-a-badge".to_string(),)); - - let json = anon.show_crate("foo_ignored_badge"); - let badges = json.krate.badges.unwrap(); - assert_eq!(badges.len(), 0); -} - -#[test] -fn reverse_dependencies() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - let c1 = CrateBuilder::new("c1", user.id).expect_build(conn); - CrateBuilder::new("c2", user.id) - .version(VersionBuilder::new("1.0.0").dependency(&c1, None)) - .version( - VersionBuilder::new("1.1.0") - .dependency(&c1, None) - .dependency(&c1, Some("foo")), - ) - .expect_build(conn); - }); - - let deps = anon.reverse_dependencies("c1"); - assert_eq!(deps.dependencies.len(), 1); - assert_eq!(deps.meta.total, 1); - assert_eq!(deps.dependencies[0].crate_id, "c1"); - assert_eq!(deps.versions.len(), 1); - assert_eq!(deps.versions[0].krate, "c2"); - assert_eq!(deps.versions[0].num, "1.1.0"); - - // c1 has no dependent crates. - let deps = anon.reverse_dependencies("c2"); - assert_eq!(deps.dependencies.len(), 0); - assert_eq!(deps.meta.total, 0); -} - -#[test] -fn reverse_dependencies_when_old_version_doesnt_depend_but_new_does() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - let c1 = CrateBuilder::new("c1", user.id) - .version("1.1.0") - .expect_build(conn); - CrateBuilder::new("c2", user.id) - .version("1.0.0") - .version(VersionBuilder::new("2.0.0").dependency(&c1, None)) - .expect_build(conn); - }); - - let deps = anon.reverse_dependencies("c1"); - assert_eq!(deps.dependencies.len(), 1); - assert_eq!(deps.meta.total, 1); - assert_eq!(deps.dependencies[0].crate_id, "c1"); -} - -#[test] -fn reverse_dependencies_when_old_version_depended_but_new_doesnt() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - let c1 = CrateBuilder::new("c1", user.id) - .version("1.0.0") - .expect_build(conn); - CrateBuilder::new("c2", user.id) - .version(VersionBuilder::new("1.0.0").dependency(&c1, None)) - .version("2.0.0") - .expect_build(conn); - }); - - let deps = anon.reverse_dependencies("c1"); - assert_eq!(deps.dependencies.len(), 0); - assert_eq!(deps.meta.total, 0); -} - -#[test] -fn prerelease_versions_not_included_in_reverse_dependencies() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - let c1 = CrateBuilder::new("c1", user.id) - .version("1.0.0") - .expect_build(conn); - CrateBuilder::new("c2", user.id) - .version("1.1.0-pre") - .expect_build(conn); - CrateBuilder::new("c3", user.id) - .version(VersionBuilder::new("1.0.0").dependency(&c1, None)) - .version("1.1.0-pre") - .expect_build(conn); - }); - - let deps = anon.reverse_dependencies("c1"); - assert_eq!(deps.dependencies.len(), 1); - assert_eq!(deps.meta.total, 1); - assert_eq!(deps.dependencies[0].crate_id, "c1"); -} - -#[test] -fn yanked_versions_not_included_in_reverse_dependencies() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - let c1 = CrateBuilder::new("c1", user.id) - .version("1.0.0") - .expect_build(conn); - CrateBuilder::new("c2", user.id) - .version("1.0.0") - .version(VersionBuilder::new("2.0.0").dependency(&c1, None)) - .expect_build(conn); - }); - - let deps = anon.reverse_dependencies("c1"); - assert_eq!(deps.dependencies.len(), 1); - assert_eq!(deps.meta.total, 1); - assert_eq!(deps.dependencies[0].crate_id, "c1"); - - app.db(|conn| { - diesel::update(versions::table.filter(versions::num.eq("2.0.0"))) - .set(versions::yanked.eq(true)) - .execute(conn) - .unwrap(); - }); - - let deps = anon.reverse_dependencies("c1"); - assert_eq!(deps.dependencies.len(), 0); - assert_eq!(deps.meta.total, 0); -} - -#[test] -fn reverse_dependencies_includes_published_by_user_when_present() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - let c1 = CrateBuilder::new("c1", user.id) - .version("1.0.0") - .expect_build(conn); - CrateBuilder::new("c2", user.id) - .version(VersionBuilder::new("2.0.0").dependency(&c1, None)) - .expect_build(conn); - - // Make c2's version (and,incidentally, c1's, but that doesn't matter) 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(); - - // c3's version will have the published by info recorded - CrateBuilder::new("c3", user.id) - .version(VersionBuilder::new("3.0.0").dependency(&c1, None)) - .expect_build(conn); - }); - - let deps = anon.reverse_dependencies("c1"); - assert_eq!(deps.versions.len(), 2); - - let c2_version = deps.versions.iter().find(|v| v.krate == "c2").unwrap(); - assert_none!(&c2_version.published_by); - - let c3_version = deps.versions.iter().find(|v| v.krate == "c3").unwrap(); - assert_eq!( - c3_version.published_by.as_ref().unwrap().login, - user.gh_login - ); -} - -#[test] -fn reverse_dependencies_query_supports_u64_version_number_parts() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - let large_but_valid_version_number = format!("1.0.{}", std::u64::MAX); - - app.db(|conn| { - let c1 = CrateBuilder::new("c1", user.id).expect_build(conn); - // The crate that depends on c1... - CrateBuilder::new("c2", user.id) - // ...has a patch version at the limits of what the semver crate supports - .version(VersionBuilder::new(&large_but_valid_version_number).dependency(&c1, None)) - .expect_build(conn); - }); - - let deps = anon.reverse_dependencies("c1"); - assert_eq!(deps.dependencies.len(), 1); - assert_eq!(deps.meta.total, 1); - assert_eq!(deps.dependencies[0].crate_id, "c1"); - assert_eq!(deps.versions.len(), 1); - assert_eq!(deps.versions[0].krate, "c2"); - assert_eq!(deps.versions[0].num, large_but_valid_version_number); -} - -#[test] -fn author_license_and_description_required() { - let (_, _, _, token) = TestApp::init().with_token(); - - let crate_to_publish = PublishBuilder::new("foo_metadata") - .version("1.1.0") - .unset_license() - .unset_description() - .unset_authors(); - - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - assert!( - json.errors[0].detail.contains("author") - && json.errors[0].detail.contains("description") - && json.errors[0].detail.contains("license"), - "{:?}", - json.errors - ); - - let crate_to_publish = PublishBuilder::new("foo_metadata") - .version("1.1.0") - .unset_description() - .unset_authors() - .author(""); - - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - assert!( - json.errors[0].detail.contains("author") - && json.errors[0].detail.contains("description") - && !json.errors[0].detail.contains("license"), - "{:?}", - json.errors - ); - - let crate_to_publish = PublishBuilder::new("foo_metadata") - .version("1.1.0") - .unset_license() - .license_file("foo") - .unset_description(); - - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - assert!( - !json.errors[0].detail.contains("author") - && json.errors[0].detail.contains("description") - && !json.errors[0].detail.contains("license"), - "{:?}", - json.errors - ); -} - -/* Given two crates, one with downloads less than 90 days ago, the - other with all downloads greater than 90 days ago, check that - the order returned is by recent downloads, descending. Check - also that recent download counts are returned in recent_downloads, - and total downloads counts are returned in downloads, and that - these numbers do not overlap. -*/ -#[test] -fn test_recent_download_count() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - // More than 90 days ago - CrateBuilder::new("green_ball", user.id) - .description("For fetching") - .downloads(10) - .recent_downloads(0) - .expect_build(conn); - - CrateBuilder::new("sweet_potato_snack", user.id) - .description("For when better than usual") - .downloads(5) - .recent_downloads(2) - .expect_build(conn); - }); - - let json = anon.search("sort=recent-downloads"); - - assert_eq!(json.meta.total, 2); - - assert_eq!(json.crates[0].name, "sweet_potato_snack"); - assert_eq!(json.crates[1].name, "green_ball"); - - assert_eq!(json.crates[0].recent_downloads, Some(2)); - assert_eq!(json.crates[0].downloads, 5); - - assert_eq!(json.crates[1].recent_downloads, Some(0)); - assert_eq!(json.crates[1].downloads, 10); -} - -/* Given one crate with zero downloads, check that the crate - still shows up in index results, but that it displays 0 - for both recent downloads and downloads. -*/ -#[test] -fn test_zero_downloads() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - // More than 90 days ago - CrateBuilder::new("green_ball", user.id) - .description("For fetching") - .downloads(0) - .recent_downloads(0) - .expect_build(conn); - }); - - let json = anon.search("sort=recent-downloads"); - assert_eq!(json.meta.total, 1); - assert_eq!(json.crates[0].name, "green_ball"); - assert_eq!(json.crates[0].recent_downloads, Some(0)); - assert_eq!(json.crates[0].downloads, 0); -} - -/* Given two crates, one with more all-time downloads, the other with - more downloads in the past 90 days, check that the index page for - categories and keywords is sorted by recent downlaods by default. -*/ -#[test] -fn test_default_sort_recent() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - let (green_crate, potato_crate) = app.db(|conn| { - // More than 90 days ago - let green_crate = CrateBuilder::new("green_ball", user.id) - .description("For fetching") - .keyword("dog") - .downloads(10) - .recent_downloads(10) - .expect_build(conn); - - let potato_crate = CrateBuilder::new("sweet_potato_snack", user.id) - .description("For when better than usual") - .keyword("dog") - .downloads(20) - .recent_downloads(0) - .expect_build(conn); - - (green_crate, potato_crate) - }); - - // test that index for keywords is sorted by recent_downloads - // by default - let json = anon.search("keyword=dog"); - - assert_eq!(json.meta.total, 2); - - assert_eq!(json.crates[0].name, "green_ball"); - assert_eq!(json.crates[1].name, "sweet_potato_snack"); - - assert_eq!(json.crates[0].recent_downloads, Some(10)); - assert_eq!(json.crates[0].downloads, 10); - - assert_eq!(json.crates[1].recent_downloads, Some(0)); - assert_eq!(json.crates[1].downloads, 20); - - app.db(|conn| { - new_category("Animal", "animal", "animal crates") - .create_or_update(conn) - .unwrap(); - Category::update_crate(conn, &green_crate, &["animal"]).unwrap(); - Category::update_crate(conn, &potato_crate, &["animal"]).unwrap(); - }); - - // test that index for categories is sorted by recent_downloads - // by default - let json = anon.search("category=animal"); - - assert_eq!(json.meta.total, 2); - - assert_eq!(json.crates[0].name, "green_ball"); - assert_eq!(json.crates[1].name, "sweet_potato_snack"); - - assert_eq!(json.crates[0].recent_downloads, Some(10)); - assert_eq!(json.crates[0].downloads, 10); - - assert_eq!(json.crates[1].recent_downloads, Some(0)); - assert_eq!(json.crates[1].downloads, 20); -} - -#[test] -fn block_bad_documentation_url() { - let (app, anon, user) = TestApp::init().with_user(); - let user = user.as_model(); - - app.db(|conn| { - CrateBuilder::new("foo_bad_doc_url", user.id) - .documentation("http://rust-ci.org/foo/foo_bad_doc_url/doc/foo_bad_doc_url/") - .expect_build(conn) - }); - - let json = anon.show_crate("foo_bad_doc_url"); - assert_eq!(json.krate.documentation, None); -} - -// This is testing Cargo functionality! ! ! -// specifically functions modify_owners and add_owners -// which call the `PUT /crates/:crate_id/owners` route -#[test] -fn test_cargo_invite_owners() { - let (app, _, owner) = TestApp::init().with_user(); - - let new_user = app.db_new_user("cilantro"); - app.db(|conn| { - CrateBuilder::new("guacamole", owner.as_model().id).expect_build(conn); - }); - - #[derive(Serialize)] - struct OwnerReq { - owners: Option>, - } - #[derive(Deserialize, Debug)] - struct OwnerResp { - // server must include `ok: true` to support old cargo clients - ok: bool, - msg: String, - } - - let body = serde_json::to_string(&OwnerReq { - owners: Some(vec![new_user.as_model().gh_login.clone()]), - }); - let json: OwnerResp = owner - .put("/api/v1/crates/guacamole/owners", body.unwrap().as_bytes()) - .good(); - - // this ok:true field is what old versions of Cargo - // need - do not remove unless you're cool with - // dropping support for old versions - assert!(json.ok); - // msg field is what is sent and used in updated - // version of cargo - assert_eq!( - json.msg, - "user cilantro has been invited to be an owner of crate guacamole" - ) -} - -#[test] -fn new_krate_tarball_with_hard_links() { - let (_, _, _, token) = TestApp::init().with_token(); - - let mut tarball = Vec::new(); - { - let mut ar = tar::Builder::new(GzEncoder::new(&mut tarball, Compression::default())); - let mut header = tar::Header::new_gnu(); - assert_ok!(header.set_path("foo-1.1.0/bar")); - header.set_size(0); - header.set_cksum(); - header.set_entry_type(tar::EntryType::hard_link()); - assert_ok!(header.set_link_name("foo-1.1.0/another")); - assert_ok!(ar.append(&header, &[][..])); - assert_ok!(ar.finish()); - } - - let crate_to_publish = PublishBuilder::new("foo").version("1.1.0").tarball(tarball); - - let json = token - .enqueue_publish(crate_to_publish) - .bad_with_status(StatusCode::OK); - - assert!( - json.errors[0] - .detail - .contains("too large when decompressed"), - "{:?}", - json.errors - ); -} - -#[test] -fn publish_new_crate_rate_limited() { - let (app, anon, _, token) = TestApp::full() - .with_publish_rate_limit(Duration::from_millis(500), 1) - .with_token(); - - // Upload a new crate - let crate_to_publish = PublishBuilder::new("rate_limited1"); - token.enqueue_publish(crate_to_publish).good(); - - // Uploading a second crate is limited - let crate_to_publish = PublishBuilder::new("rate_limited2"); - token - .enqueue_publish(crate_to_publish) - .assert_status(StatusCode::TOO_MANY_REQUESTS); - app.run_pending_background_jobs(); - - anon.get::<()>("/api/v1/crates/rate_limited2") - .assert_status(StatusCode::NOT_FOUND); - - // Wait for the limit to be up - thread::sleep(Duration::from_millis(500)); - - let crate_to_publish = PublishBuilder::new("rate_limited2"); - token.enqueue_publish(crate_to_publish).good(); - - let json = anon.show_crate("rate_limited2"); - assert_eq!(json.krate.max_version, "1.0.0"); -} - -#[test] -fn publish_rate_limit_doesnt_affect_existing_crates() { - let (app, _, _, token) = TestApp::full() - .with_publish_rate_limit(Duration::from_millis(500), 1) - .with_token(); - - // Upload a new crate - let crate_to_publish = PublishBuilder::new("rate_limited1"); - token.enqueue_publish(crate_to_publish).good(); - - let new_version = PublishBuilder::new("rate_limited1").version("1.0.1"); - token.enqueue_publish(new_version).good(); - app.run_pending_background_jobs(); -} - -#[test] -fn pagination_links_included_if_applicable() { - let (app, anon, user) = TestApp::init().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 page1 = anon.search("per_page=1"); - let page2 = anon.search("page=2&per_page=1"); - let page3 = anon.search("page=3&per_page=1"); - let page4 = anon.search("page=4&per_page=1"); - - assert_eq!(Some("?per_page=1&page=2".to_string()), page1.meta.next_page); - assert_eq!(None, page1.meta.prev_page); - assert_eq!(Some("?page=3&per_page=1".to_string()), page2.meta.next_page); - assert_eq!(Some("?page=1&per_page=1".to_string()), page2.meta.prev_page); - assert_eq!(None, page4.meta.next_page); - assert_eq!(Some("?page=2&per_page=1".to_string()), page3.meta.prev_page); -} - -#[test] -fn pagination_parameters_only_accept_integers() { - let (app, anon, user) = TestApp::init().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 invalid_per_page_json = anon - .get_with_query::<()>("/api/v1/crates", "page=1&per_page=100%22%EF%BC%8Cexception") - .bad_with_status(StatusCode::BAD_REQUEST); - assert_eq!( - invalid_per_page_json.errors[0].detail, - "invalid digit found in string" - ); - - let invalid_page_json = anon - .get_with_query::<()>("/api/v1/crates", "page=100%22%EF%BC%8Cexception&per_page=1") - .bad_with_status(StatusCode::BAD_REQUEST); - assert_eq!( - invalid_page_json.errors[0].detail, - "invalid digit found in string" - ); -} diff --git a/src/tests/krate/dependencies.rs b/src/tests/krate/dependencies.rs new file mode 100644 index 00000000000..96d50cf0618 --- /dev/null +++ b/src/tests/krate/dependencies.rs @@ -0,0 +1,31 @@ +use crate::builders::{CrateBuilder, VersionBuilder}; +use crate::new_dependency; +use crate::util::{RequestHelper, TestApp}; +use cargo_registry::views::EncodableDependency; +use http::StatusCode; + +#[derive(Deserialize)] +struct Deps { + dependencies: Vec, +} + +#[test] +fn dependencies() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + let c1 = CrateBuilder::new("foo_deps", user.id).expect_build(conn); + let v = VersionBuilder::new("1.0.0").expect_build(c1.id, user.id, conn); + let c2 = CrateBuilder::new("bar_deps", user.id).expect_build(conn); + new_dependency(conn, &v, &c2); + }); + + let deps: Deps = anon + .get("/api/v1/crates/foo_deps/1.0.0/dependencies") + .good(); + assert_eq!(deps.dependencies[0].crate_id, "bar_deps"); + + anon.get::<()>("/api/v1/crates/foo_deps/1.0.2/dependencies") + .bad_with_status(StatusCode::OK); +} diff --git a/src/tests/krate/downloads.rs b/src/tests/krate/downloads.rs new file mode 100644 index 00000000000..771ee592a9c --- /dev/null +++ b/src/tests/krate/downloads.rs @@ -0,0 +1,92 @@ +use crate::builders::{CrateBuilder, VersionBuilder}; +use crate::util::{RequestHelper, TestApp}; +use cargo_registry::views::EncodableVersionDownload; +use chrono::{Duration, Utc}; +use http::StatusCode; + +#[derive(Deserialize)] +struct Downloads { + version_downloads: Vec, +} + +#[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 assert_dl_count = |name_and_version: &str, query: Option<&str>, count: i32| { + let url = format!("/api/v1/crates/{}/downloads", name_and_version); + 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); + }; + + let download = |name_and_version: &str| { + let url = format!("/api/v1/crates/{}/download", name_and_version); + anon.get::<()>(&url).assert_status(StatusCode::FOUND); + // TODO: test the with_json code path + }; + + download("foo_download/1.0.0"); + assert_dl_count("foo_download/1.0.0", None, 1); + assert_dl_count("foo_download", None, 1); + + download("FOO_DOWNLOAD/1.0.0"); + assert_dl_count("FOO_DOWNLOAD/1.0.0", None, 2); + assert_dl_count("FOO_DOWNLOAD", None, 2); + + let yesterday = (Utc::today() + Duration::days(-1)).format("%F"); + let query = format!("before_date={}", yesterday); + assert_dl_count("FOO_DOWNLOAD/1.0.0", Some(&query), 0); + // crate/downloads always returns the last 90 days and ignores date params + assert_dl_count("FOO_DOWNLOAD", Some(&query), 2); + + let tomorrow = (Utc::today() + Duration::days(1)).format("%F"); + let query = format!("before_date={}", tomorrow); + assert_dl_count("FOO_DOWNLOAD/1.0.0", Some(&query), 2); + assert_dl_count("FOO_DOWNLOAD", Some(&query), 2); +} + +#[test] +fn download_nonexistent_version_of_existing_crate_404s() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + CrateBuilder::new("foo_bad", user.id).expect_build(conn); + }); + + anon.get("/api/v1/crates/foo_bad/0.1.0/download") + .assert_not_found(); +} + +#[test] +fn download_noncanonical_crate_name() { + 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); + }); + + // Request download for "foo-download" with a dash instead of an underscore, + // and assert that the correct download link is returned. + anon.get::<()>("/api/v1/crates/foo-download/1.0.0/download") + .assert_redirect_ends_with("/crates/foo_download/foo_download-1.0.0.crate"); +} diff --git a/src/tests/krate/following.rs b/src/tests/krate/following.rs new file mode 100644 index 00000000000..b95502fdffc --- /dev/null +++ b/src/tests/krate/following.rs @@ -0,0 +1,59 @@ +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? + let (app, _, user) = TestApp::init().with_user(); + + app.db(|conn| { + CrateBuilder::new("foo_following", user.as_model().id).expect_build(conn); + }); + + let is_following = || -> bool { + #[derive(Deserialize)] + struct F { + following: bool, + } + + user.get::("/api/v1/crates/foo_following/following") + .good() + .following + }; + + let follow = || { + assert!( + user.put::("/api/v1/crates/foo_following/follow", b"") + .good() + .ok + ); + }; + + let unfollow = || { + assert!( + user.delete::("api/v1/crates/foo_following/follow") + .good() + .ok + ); + }; + + assert!(!is_following()); + follow(); + follow(); + assert!(is_following()); + assert_eq!(user.search("following=1").crates.len(), 1); + + unfollow(); + unfollow(); + assert!(!is_following()); + assert_eq!(user.search("following=1").crates.len(), 0); +} diff --git a/src/tests/krate/mod.rs b/src/tests/krate/mod.rs new file mode 100644 index 00000000000..29140ce05a9 --- /dev/null +++ b/src/tests/krate/mod.rs @@ -0,0 +1,11 @@ +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/owners.rs b/src/tests/krate/owners.rs new file mode 100644 index 00000000000..c94fdc5a42b --- /dev/null +++ b/src/tests/krate/owners.rs @@ -0,0 +1,44 @@ +use crate::builders::CrateBuilder; +use crate::util::{RequestHelper, TestApp}; + +// This is testing Cargo functionality! ! ! +// specifically functions modify_owners and add_owners +// which call the `PUT /crates/:crate_id/owners` route +#[test] +fn test_cargo_invite_owners() { + let (app, _, owner) = TestApp::init().with_user(); + + let new_user = app.db_new_user("cilantro"); + app.db(|conn| { + CrateBuilder::new("guacamole", owner.as_model().id).expect_build(conn); + }); + + #[derive(Serialize)] + struct OwnerReq { + owners: Option>, + } + #[derive(Deserialize, Debug)] + struct OwnerResp { + // server must include `ok: true` to support old cargo clients + ok: bool, + msg: String, + } + + let body = serde_json::to_string(&OwnerReq { + owners: Some(vec![new_user.as_model().gh_login.clone()]), + }); + let json: OwnerResp = owner + .put("/api/v1/crates/guacamole/owners", body.unwrap().as_bytes()) + .good(); + + // this ok:true field is what old versions of Cargo + // need - do not remove unless you're cool with + // dropping support for old versions + assert!(json.ok); + // msg field is what is sent and used in updated + // version of cargo + assert_eq!( + json.msg, + "user cilantro has been invited to be an owner of crate guacamole" + ) +} diff --git a/src/tests/krate/publish.rs b/src/tests/krate/publish.rs new file mode 100644 index 00000000000..09f3ba7bc20 --- /dev/null +++ b/src/tests/krate/publish.rs @@ -0,0 +1,944 @@ +use crate::builders::{CrateBuilder, DependencyBuilder, PublishBuilder}; +use crate::new_category; +use crate::util::{RequestHelper, TestApp}; +use cargo_registry::models::krate::MAX_NAME_LENGTH; +use cargo_registry::schema::{api_tokens, emails, versions_published_by}; +use cargo_registry::views::GoodCrate; +use diesel::{delete, update, ExpressionMethods, QueryDsl, RunQueryDsl}; +use failure::_core::time::Duration; +use flate2::write::GzEncoder; +use flate2::Compression; +use http::StatusCode; +use std::collections::HashMap; +use std::io::Read; +use std::{io, thread}; + +#[test] +fn uploading_new_version_touches_crate() { + use crate::builders::PublishBuilder; + use crate::util::{RequestHelper, TestApp}; + use crate::CrateResponse; + use cargo_registry::schema::crates; + use diesel::dsl::*; + use diesel::{ExpressionMethods, RunQueryDsl}; + + let (app, _, user) = TestApp::full().with_user(); + + let crate_to_publish = PublishBuilder::new("foo_versions_updated_at").version("1.0.0"); + user.enqueue_publish(crate_to_publish).good(); + + app.db(|conn| { + diesel::update(crates::table) + .set(crates::updated_at.eq(crates::updated_at - 1.hour())) + .execute(&*conn) + .unwrap(); + }); + + let json: CrateResponse = user.show_crate("foo_versions_updated_at"); + let updated_at_before = json.krate.updated_at; + + let crate_to_publish = PublishBuilder::new("foo_versions_updated_at").version("2.0.0"); + user.enqueue_publish(crate_to_publish).good(); + + let json: CrateResponse = user.show_crate("foo_versions_updated_at"); + let updated_at_after = json.krate.updated_at; + + assert_ne!(updated_at_before, updated_at_after); +} + +#[test] +fn new_wrong_token() { + let (app, anon, _, token) = TestApp::init().with_token(); + + // Try to publish without a token + let crate_to_publish = PublishBuilder::new("foo"); + let json = anon + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::FORBIDDEN); + assert!( + json.errors[0] + .detail + .contains("must be logged in to perform that action"), + "{:?}", + json.errors + ); + + // Try to publish with the wrong token (by changing the token in the database) + app.db(|conn| { + diesel::update(api_tokens::table) + .set(api_tokens::token.eq(b"bad" as &[u8])) + .execute(conn) + .unwrap(); + }); + + let crate_to_publish = PublishBuilder::new("foo"); + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::FORBIDDEN); + assert!( + json.errors[0] + .detail + .contains("must be logged in to perform that action"), + "{:?}", + json.errors + ); +} + +#[test] +fn invalid_names() { + let (_, _, _, token) = TestApp::init().with_token(); + + let bad_name = |name: &str, error_message: &str| { + let crate_to_publish = PublishBuilder::new(name).version("1.0.0"); + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + + assert!( + json.errors[0].detail.contains(error_message,), + "{:?}", + json.errors + ); + }; + + let error_message = "expected a valid crate name"; + bad_name("", error_message); + bad_name("foo bar", error_message); + bad_name(&"a".repeat(MAX_NAME_LENGTH + 1), error_message); + bad_name("snow☃", error_message); + bad_name("áccênts", error_message); + + let error_message = "cannot upload a crate with a reserved name"; + bad_name("std", error_message); + bad_name("STD", error_message); + bad_name("compiler-rt", error_message); + bad_name("compiler_rt", error_message); + bad_name("coMpiLer_Rt", error_message); +} + +#[test] +fn new_krate() { + let (_, _, user) = TestApp::full().with_user(); + + let crate_to_publish = PublishBuilder::new("foo_new").version("1.0.0"); + let json: GoodCrate = user.enqueue_publish(crate_to_publish).good(); + + assert_eq!(json.krate.name, "foo_new"); + assert_eq!(json.krate.max_version, "1.0.0"); +} + +#[test] +fn new_krate_with_token() { + let (_, _, _, token) = TestApp::full().with_token(); + + let crate_to_publish = PublishBuilder::new("foo_new").version("1.0.0"); + let json: GoodCrate = token.enqueue_publish(crate_to_publish).good(); + + assert_eq!(json.krate.name, "foo_new"); + assert_eq!(json.krate.max_version, "1.0.0"); +} + +#[test] +fn new_krate_weird_version() { + let (_, _, _, token) = TestApp::full().with_token(); + + let crate_to_publish = PublishBuilder::new("foo_weird").version("0.0.0-pre"); + let json: GoodCrate = token.enqueue_publish(crate_to_publish).good(); + + assert_eq!(json.krate.name, "foo_weird"); + assert_eq!(json.krate.max_version, "0.0.0-pre"); +} + +#[test] +fn new_with_renamed_dependency() { + let (app, _, user, token) = TestApp::full().with_token(); + + app.db(|conn| { + // Insert a crate directly into the database so that new-krate can depend on it + CrateBuilder::new("package-name", user.as_model().id).expect_build(conn); + }); + + let dependency = DependencyBuilder::new("package-name").rename("my-name"); + + let crate_to_publish = PublishBuilder::new("new-krate") + .version("1.0.0") + .dependency(dependency); + token.enqueue_publish(crate_to_publish).good(); + app.run_pending_background_jobs(); + + let crates = app.crates_from_index_head("ne/w-/new-krate"); + assert_eq!(crates.len(), 1); + assert_eq!(crates[0].name, "new-krate"); + assert_eq!(crates[0].vers, "1.0.0"); + assert_eq!(crates[0].deps.len(), 1); + assert_eq!(crates[0].deps[0].name, "my-name"); + assert_eq!(crates[0].deps[0].package.as_ref().unwrap(), "package-name"); +} + +#[test] +fn new_krate_with_dependency() { + let (app, _, user, token) = TestApp::full().with_token(); + + app.db(|conn| { + // Insert a crate directly into the database so that new_dep can depend on it + // The name choice of `foo-dep` is important! It has the property of + // name != canon_crate_name(name) and is a regression test for + // https://github.com/rust-lang/crates.io/issues/651 + CrateBuilder::new("foo-dep", user.as_model().id).expect_build(conn); + }); + + let dependency = DependencyBuilder::new("foo-dep"); + + let crate_to_publish = PublishBuilder::new("new_dep") + .version("1.0.0") + .dependency(dependency); + token.enqueue_publish(crate_to_publish).good(); +} + +#[test] +fn reject_new_krate_with_non_exact_dependency() { + let (app, _, user, token) = TestApp::init().with_token(); + + app.db(|conn| { + CrateBuilder::new("foo-dep", user.as_model().id).expect_build(conn); + }); + + // Use non-exact name for the dependency + let dependency = DependencyBuilder::new("foo_dep"); + + let crate_to_publish = PublishBuilder::new("new_dep") + .version("1.0.0") + .dependency(dependency); + token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); +} + +#[test] +fn new_crate_allow_empty_alternative_registry_dependency() { + let (app, _, user, token) = TestApp::full().with_token(); + + app.db(|conn| { + CrateBuilder::new("foo-dep", user.as_model().id).expect_build(conn); + }); + + let dependency = DependencyBuilder::new("foo-dep").registry(""); + let crate_to_publish = PublishBuilder::new("foo").dependency(dependency); + token.enqueue_publish(crate_to_publish).good(); +} + +#[test] +fn reject_new_crate_with_alternative_registry_dependency() { + let (_, _, _, token) = TestApp::init().with_token(); + + let dependency = + DependencyBuilder::new("dep").registry("https://server.example/path/to/registry"); + + let crate_to_publish = PublishBuilder::new("depends-on-alt-registry").dependency(dependency); + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + assert!( + json.errors[0] + .detail + .contains("Cross-registry dependencies are not permitted on crates.io."), + "{:?}", + json.errors + ); +} + +#[test] +fn new_krate_with_wildcard_dependency() { + let (app, _, user, token) = TestApp::init().with_token(); + + app.db(|conn| { + // Insert a crate directly into the database so that new_wild can depend on it + CrateBuilder::new("foo_wild", user.as_model().id).expect_build(conn); + }); + + let dependency = DependencyBuilder::new("foo_wild").version_req("*"); + + let crate_to_publish = PublishBuilder::new("new_wild") + .version("1.0.0") + .dependency(dependency); + + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + assert!( + json.errors[0].detail.contains("dependency constraints"), + "{:?}", + json.errors + ); +} + +#[test] +fn new_krate_twice() { + let (app, _, user, token) = TestApp::full().with_token(); + + app.db(|conn| { + // Insert a crate directly into the database and then we'll try to publish another version + CrateBuilder::new("foo_twice", user.as_model().id).expect_build(conn); + }); + + let crate_to_publish = PublishBuilder::new("foo_twice") + .version("2.0.0") + .description("2.0.0 description"); + let json = token.enqueue_publish(crate_to_publish).good(); + + assert_eq!(json.krate.name, "foo_twice"); + assert_eq!(json.krate.description.unwrap(), "2.0.0 description"); +} + +#[test] +fn new_krate_wrong_user() { + let (app, _, user) = TestApp::init().with_user(); + + app.db(|conn| { + // Create the foo_wrong crate with one user + CrateBuilder::new("foo_wrong", user.as_model().id).expect_build(conn); + }); + + // Then try to publish with a different user + let another_user = app.db_new_user("another").db_new_token("bar"); + let crate_to_publish = PublishBuilder::new("foo_wrong").version("2.0.0"); + + let json = another_user + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + assert!( + json.errors[0] + .detail + .contains("this crate exists but you don't seem to be an owner."), + "{:?}", + json.errors + ); +} + +#[test] +fn new_krate_too_big() { + let (_, _, user) = TestApp::init().with_user(); + + let files = [("foo_big-1.0.0/big", &[b'a'; 2000] as &[_])]; + let builder = PublishBuilder::new("foo_big").files(&files); + + let json = user + .enqueue_publish(builder) + .bad_with_status(StatusCode::OK); + assert!( + json.errors[0] + .detail + .contains("uploaded tarball is malformed or too large when decompressed"), + "{:?}", + json.errors + ); +} + +#[test] +fn new_krate_too_big_but_whitelisted() { + let (app, _, user, token) = TestApp::full().with_token(); + + app.db(|conn| { + CrateBuilder::new("foo_whitelist", user.as_model().id) + .max_upload_size(2_000_000) + .expect_build(conn); + }); + + let files = [("foo_whitelist-1.1.0/big", &[b'a'; 2000] as &[_])]; + let crate_to_publish = PublishBuilder::new("foo_whitelist") + .version("1.1.0") + .files(&files); + + token.enqueue_publish(crate_to_publish).good(); +} + +#[test] +fn new_krate_wrong_files() { + let (_, _, user) = TestApp::init().with_user(); + let data: &[u8] = &[1]; + let files = [("foo-1.0.0/a", data), ("bar-1.0.0/a", data)]; + let builder = PublishBuilder::new("foo").files(&files); + + let json = user + .enqueue_publish(builder) + .bad_with_status(StatusCode::OK); + assert!( + json.errors[0].detail.contains("invalid tarball uploaded"), + "{:?}", + json.errors + ); +} + +#[test] +fn new_krate_gzip_bomb() { + let (_, _, _, token) = TestApp::init().with_token(); + + let len = 512 * 1024; + let mut body = io::repeat(0).take(len); + + let crate_to_publish = PublishBuilder::new("foo") + .version("1.1.0") + .files_with_io(&mut [("foo-1.1.0/a", &mut body, len)]); + + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + + assert!( + json.errors[0] + .detail + .contains("too large when decompressed"), + "{:?}", + json.errors + ); +} + +#[test] +fn new_krate_duplicate_version() { + let (app, _, user, token) = TestApp::init().with_token(); + + app.db(|conn| { + // Insert a crate directly into the database and then we'll try to publish the same version + CrateBuilder::new("foo_dupe", user.as_model().id) + .version("1.0.0") + .expect_build(conn); + }); + + let crate_to_publish = PublishBuilder::new("foo_dupe").version("1.0.0"); + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + + assert!( + json.errors[0].detail.contains("already uploaded"), + "{:?}", + json.errors + ); +} + +#[test] +fn new_crate_similar_name() { + let (app, _, user, token) = TestApp::init().with_token(); + + app.db(|conn| { + CrateBuilder::new("Foo_similar", user.as_model().id) + .version("1.0.0") + .expect_build(conn); + }); + + let crate_to_publish = PublishBuilder::new("foo_similar").version("1.1.0"); + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + + assert!( + json.errors[0].detail.contains("previously named"), + "{:?}", + json.errors + ); +} + +#[test] +fn new_crate_similar_name_hyphen() { + let (app, _, user, token) = TestApp::init().with_token(); + + app.db(|conn| { + CrateBuilder::new("foo_bar_hyphen", user.as_model().id) + .version("1.0.0") + .expect_build(conn); + }); + + let crate_to_publish = PublishBuilder::new("foo-bar-hyphen").version("1.1.0"); + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + + assert!( + json.errors[0].detail.contains("previously named"), + "{:?}", + json.errors + ); +} + +#[test] +fn new_crate_similar_name_underscore() { + let (app, _, user, token) = TestApp::init().with_token(); + + app.db(|conn| { + CrateBuilder::new("foo-bar-underscore", user.as_model().id) + .version("1.0.0") + .expect_build(conn); + }); + + let crate_to_publish = PublishBuilder::new("foo_bar_underscore").version("1.1.0"); + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + + assert!( + json.errors[0].detail.contains("previously named"), + "{:?}", + json.errors + ); +} + +#[test] +fn new_krate_git_upload() { + let (app, _, _, token) = TestApp::full().with_token(); + + let crate_to_publish = PublishBuilder::new("fgt"); + token.enqueue_publish(crate_to_publish).good(); + app.run_pending_background_jobs(); + + let crates = app.crates_from_index_head("3/f/fgt"); + assert_eq!(crates.len(), 1); + assert_eq!(crates[0].name, "fgt"); + assert_eq!(crates[0].vers, "1.0.0"); + assert!(crates[0].deps.is_empty()); + assert_eq!( + crates[0].cksum, + "acb5604b126ac894c1eb11c4575bf2072fea61232a888e453770c79d7ed56419" + ); +} + +#[test] +fn new_krate_git_upload_appends() { + let (app, _, _, token) = TestApp::full().with_token(); + + let crate_to_publish = PublishBuilder::new("FPP").version("0.0.1"); + token.enqueue_publish(crate_to_publish).good(); + let crate_to_publish = PublishBuilder::new("FPP").version("1.0.0"); + token.enqueue_publish(crate_to_publish).good(); + app.run_pending_background_jobs(); + + let crates = app.crates_from_index_head("3/f/fpp"); + assert!(crates.len() == 2); + assert_eq!(crates[0].name, "FPP"); + assert_eq!(crates[0].vers, "0.0.1"); + assert!(crates[0].deps.is_empty()); + assert_eq!(crates[1].name, "FPP"); + assert_eq!(crates[1].vers, "1.0.0"); + assert!(crates[1].deps.is_empty()); +} + +#[test] +fn new_krate_git_upload_with_conflicts() { + let (app, _, _, token) = TestApp::full().with_token(); + + let index = app.upstream_repository(); + let target = index.head().unwrap().target().unwrap(); + let sig = index.signature().unwrap(); + let parent = index.find_commit(target).unwrap(); + let tree = index.find_tree(parent.tree_id()).unwrap(); + index + .commit(Some("HEAD"), &sig, &sig, "empty commit", &tree, &[&parent]) + .unwrap(); + + let crate_to_publish = PublishBuilder::new("foo_conflicts"); + token.enqueue_publish(crate_to_publish).good(); +} + +#[test] +fn new_krate_dependency_missing() { + let (_, _, _, token) = TestApp::init().with_token(); + + // Deliberately not inserting this crate in the database to test behavior when a dependency + // doesn't exist! + let dependency = DependencyBuilder::new("bar_missing"); + let crate_to_publish = PublishBuilder::new("foo_missing").dependency(dependency); + + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + assert!( + json.errors[0] + .detail + .contains("no known crate named `bar_missing`"), + "{:?}", + json.errors + ); +} + +#[test] +fn new_krate_with_readme() { + let (_, _, _, token) = TestApp::full().with_token(); + + let crate_to_publish = PublishBuilder::new("foo_readme").readme(""); + let json = token.enqueue_publish(crate_to_publish).good(); + + assert_eq!(json.krate.name, "foo_readme"); + assert_eq!(json.krate.max_version, "1.0.0"); +} + +#[test] +fn new_krate_without_any_email_fails() { + let (app, _, _, token) = TestApp::init().with_token(); + + app.db(|conn| { + delete(emails::table).execute(conn).unwrap(); + }); + + let crate_to_publish = PublishBuilder::new("foo_no_email"); + + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + + assert!( + json.errors[0] + .detail + .contains("A verified email address is required to publish crates to crates.io"), + "{:?}", + json.errors + ); +} + +#[test] +fn new_krate_with_unverified_email_fails() { + let (app, _, _, token) = TestApp::init().with_token(); + + app.db(|conn| { + update(emails::table) + .set((emails::verified.eq(false),)) + .execute(conn) + .unwrap(); + }); + + let crate_to_publish = PublishBuilder::new("foo_unverified_email"); + + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + + assert!( + json.errors[0] + .detail + .contains("A verified email address is required to publish crates to crates.io"), + "{:?}", + json.errors + ); +} + +#[test] +fn new_krate_records_verified_email() { + let (app, _, _, token) = TestApp::full().with_token(); + + let crate_to_publish = PublishBuilder::new("foo_verified_email"); + + token.enqueue_publish(crate_to_publish).good(); + + app.db(|conn| { + let email: String = versions_published_by::table + .select(versions_published_by::email) + .first(conn) + .unwrap(); + assert_eq!(email, "something@example.com"); + }); +} + +#[test] +fn publish_records_an_audit_action() { + use cargo_registry::models::VersionOwnerAction; + + let (app, anon, _, token) = TestApp::full().with_token(); + + app.db(|conn| assert!(VersionOwnerAction::all(&conn).unwrap().is_empty())); + + // Upload a new crate, putting it in the git index + let crate_to_publish = PublishBuilder::new("fyk"); + token.enqueue_publish(crate_to_publish).good(); + + // Make sure it has one publish audit action + let json = anon.show_version("fyk", "1.0.0"); + let actions = json.version.audit_actions; + + assert_eq!(actions.len(), 1); + let action = &actions[0]; + assert_eq!(action.action, "publish"); + assert_eq!(action.user.id, token.as_model().user_id); +} + +#[test] +fn publish_after_removing_documentation() { + let (app, anon, user, token) = TestApp::full().with_token(); + let user = user.as_model(); + + // 1. Start with a crate with no documentation + app.db(|conn| { + CrateBuilder::new("docscrate", user.id) + .version("0.2.0") + .expect_build(conn); + }); + + // Verify that crates start without any documentation so the next assertion can *prove* + // that it was the one that added the documentation + let json = anon.show_crate("docscrate"); + assert_eq!(json.krate.documentation, None); + + // 2. Add documentation + let crate_to_publish = PublishBuilder::new("docscrate") + .version("0.2.1") + .documentation("http://foo.rs"); + let json = token.enqueue_publish(crate_to_publish).good(); + assert_eq!(json.krate.documentation, Some("http://foo.rs".to_owned())); + + // Ensure latest version also has the same documentation + let json = anon.show_crate("docscrate"); + assert_eq!(json.krate.documentation, Some("http://foo.rs".to_owned())); + + // 3. Remove the documentation + let crate_to_publish = PublishBuilder::new("docscrate").version("0.2.2"); + let json = token.enqueue_publish(crate_to_publish).good(); + assert_eq!(json.krate.documentation, None); + + // Ensure latest version no longer has documentation + let json = anon.show_crate("docscrate"); + assert_eq!(json.krate.documentation, None); +} + +#[test] +fn bad_keywords() { + let (_, _, _, token) = TestApp::init().with_token(); + let crate_to_publish = + PublishBuilder::new("foo_bad_key").keyword("super-long-keyword-name-oh-no"); + token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + + let crate_to_publish = PublishBuilder::new("foo_bad_key").keyword("?@?%"); + token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + + let crate_to_publish = PublishBuilder::new("foo_bad_key").keyword("áccênts"); + token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); +} + +#[test] +fn good_categories() { + let (app, _, _, token) = TestApp::full().with_token(); + + app.db(|conn| { + new_category("Category 1", "cat1", "Category 1 crates") + .create_or_update(conn) + .unwrap(); + }); + + let crate_to_publish = PublishBuilder::new("foo_good_cat").category("cat1"); + let json = token.enqueue_publish(crate_to_publish).good(); + + assert_eq!(json.krate.name, "foo_good_cat"); + assert_eq!(json.krate.max_version, "1.0.0"); + assert_eq!(json.warnings.invalid_categories.len(), 0); +} + +#[test] +fn ignored_categories() { + let (_, _, _, token) = TestApp::full().with_token(); + + let crate_to_publish = PublishBuilder::new("foo_ignored_cat").category("bar"); + let json = token.enqueue_publish(crate_to_publish).good(); + + assert_eq!(json.krate.name, "foo_ignored_cat"); + assert_eq!(json.krate.max_version, "1.0.0"); + assert_eq!(json.warnings.invalid_categories, vec!["bar"]); +} + +#[test] +fn good_badges() { + let (_, anon, _, token) = TestApp::full().with_token(); + + let mut badges = HashMap::new(); + let mut badge_attributes = HashMap::new(); + badge_attributes.insert( + String::from("repository"), + String::from("rust-lang/crates.io"), + ); + badges.insert(String::from("travis-ci"), badge_attributes); + + let crate_to_publish = PublishBuilder::new("foobadger").badges(badges); + + let json = token.enqueue_publish(crate_to_publish).good(); + assert_eq!(json.krate.name, "foobadger"); + assert_eq!(json.krate.max_version, "1.0.0"); + + let json = anon.show_crate("foobadger"); + let badges = json.krate.badges.unwrap(); + assert_eq!(badges.len(), 1); + assert_eq!(badges[0].badge_type, "travis-ci"); + assert_eq!( + badges[0].attributes["repository"], + Some(String::from("rust-lang/crates.io")) + ); +} + +#[test] +fn ignored_badges() { + let (_, anon, _, token) = TestApp::full().with_token(); + + let mut badges = HashMap::new(); + + // Known badge type, missing required repository attribute + let mut badge_attributes = HashMap::new(); + badge_attributes.insert(String::from("branch"), String::from("master")); + badges.insert(String::from("travis-ci"), badge_attributes); + + // Unknown badge type + let mut unknown_badge_attributes = HashMap::new(); + unknown_badge_attributes.insert(String::from("repository"), String::from("rust-lang/rust")); + badges.insert(String::from("not-a-badge"), unknown_badge_attributes); + + let crate_to_publish = PublishBuilder::new("foo_ignored_badge").badges(badges); + + let json = token.enqueue_publish(crate_to_publish).good(); + assert_eq!(json.krate.name, "foo_ignored_badge"); + assert_eq!(json.krate.max_version, "1.0.0"); + assert_eq!(json.warnings.invalid_badges.len(), 2); + assert!(json + .warnings + .invalid_badges + .contains(&"travis-ci".to_string(),)); + assert!(json + .warnings + .invalid_badges + .contains(&"not-a-badge".to_string(),)); + + let json = anon.show_crate("foo_ignored_badge"); + let badges = json.krate.badges.unwrap(); + assert_eq!(badges.len(), 0); +} + +#[test] +fn author_license_and_description_required() { + let (_, _, _, token) = TestApp::init().with_token(); + + let crate_to_publish = PublishBuilder::new("foo_metadata") + .version("1.1.0") + .unset_license() + .unset_description() + .unset_authors(); + + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + assert!( + json.errors[0].detail.contains("author") + && json.errors[0].detail.contains("description") + && json.errors[0].detail.contains("license"), + "{:?}", + json.errors + ); + + let crate_to_publish = PublishBuilder::new("foo_metadata") + .version("1.1.0") + .unset_description() + .unset_authors() + .author(""); + + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + assert!( + json.errors[0].detail.contains("author") + && json.errors[0].detail.contains("description") + && !json.errors[0].detail.contains("license"), + "{:?}", + json.errors + ); + + let crate_to_publish = PublishBuilder::new("foo_metadata") + .version("1.1.0") + .unset_license() + .license_file("foo") + .unset_description(); + + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + assert!( + !json.errors[0].detail.contains("author") + && json.errors[0].detail.contains("description") + && !json.errors[0].detail.contains("license"), + "{:?}", + json.errors + ); +} + +#[test] +fn new_krate_tarball_with_hard_links() { + let (_, _, _, token) = TestApp::init().with_token(); + + let mut tarball = Vec::new(); + { + let mut ar = tar::Builder::new(GzEncoder::new(&mut tarball, Compression::default())); + let mut header = tar::Header::new_gnu(); + assert_ok!(header.set_path("foo-1.1.0/bar")); + header.set_size(0); + header.set_cksum(); + header.set_entry_type(tar::EntryType::hard_link()); + assert_ok!(header.set_link_name("foo-1.1.0/another")); + assert_ok!(ar.append(&header, &[][..])); + assert_ok!(ar.finish()); + } + + let crate_to_publish = PublishBuilder::new("foo").version("1.1.0").tarball(tarball); + + let json = token + .enqueue_publish(crate_to_publish) + .bad_with_status(StatusCode::OK); + + assert!( + json.errors[0] + .detail + .contains("too large when decompressed"), + "{:?}", + json.errors + ); +} + +#[test] +fn publish_new_crate_rate_limited() { + let (app, anon, _, token) = TestApp::full() + .with_publish_rate_limit(Duration::from_millis(500), 1) + .with_token(); + + // Upload a new crate + let crate_to_publish = PublishBuilder::new("rate_limited1"); + token.enqueue_publish(crate_to_publish).good(); + + // Uploading a second crate is limited + let crate_to_publish = PublishBuilder::new("rate_limited2"); + token + .enqueue_publish(crate_to_publish) + .assert_status(StatusCode::TOO_MANY_REQUESTS); + app.run_pending_background_jobs(); + + anon.get::<()>("/api/v1/crates/rate_limited2") + .assert_status(StatusCode::NOT_FOUND); + + // Wait for the limit to be up + thread::sleep(Duration::from_millis(500)); + + let crate_to_publish = PublishBuilder::new("rate_limited2"); + token.enqueue_publish(crate_to_publish).good(); + + let json = anon.show_crate("rate_limited2"); + assert_eq!(json.krate.max_version, "1.0.0"); +} + +#[test] +fn publish_rate_limit_doesnt_affect_existing_crates() { + let (app, _, _, token) = TestApp::full() + .with_publish_rate_limit(Duration::from_millis(500), 1) + .with_token(); + + // Upload a new crate + let crate_to_publish = PublishBuilder::new("rate_limited1"); + token.enqueue_publish(crate_to_publish).good(); + + let new_version = PublishBuilder::new("rate_limited1").version("1.0.1"); + token.enqueue_publish(new_version).good(); + app.run_pending_background_jobs(); +} diff --git a/src/tests/krate/reverse_dependencies.rs b/src/tests/krate/reverse_dependencies.rs new file mode 100644 index 00000000000..9a28f2ef72a --- /dev/null +++ b/src/tests/krate/reverse_dependencies.rs @@ -0,0 +1,217 @@ +use crate::builders::{CrateBuilder, VersionBuilder}; +use crate::util::{RequestHelper, TestApp}; +use crate::CrateMeta; +use cargo_registry::views::{EncodableDependency, EncodableVersion}; + +#[derive(Deserialize)] +struct RevDeps { + dependencies: Vec, + versions: Vec, + meta: CrateMeta, +} + +impl crate::util::MockAnonymousUser { + fn reverse_dependencies(&self, krate_name: &str) -> RevDeps { + let url = format!("/api/v1/crates/{}/reverse_dependencies", krate_name); + self.get(&url).good() + } +} + +#[test] +fn reverse_dependencies() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + let c1 = CrateBuilder::new("c1", user.id).expect_build(conn); + CrateBuilder::new("c2", user.id) + .version(VersionBuilder::new("1.0.0").dependency(&c1, None)) + .version( + VersionBuilder::new("1.1.0") + .dependency(&c1, None) + .dependency(&c1, Some("foo")), + ) + .expect_build(conn); + }); + + let deps = anon.reverse_dependencies("c1"); + assert_eq!(deps.dependencies.len(), 1); + assert_eq!(deps.meta.total, 1); + assert_eq!(deps.dependencies[0].crate_id, "c1"); + assert_eq!(deps.versions.len(), 1); + assert_eq!(deps.versions[0].krate, "c2"); + assert_eq!(deps.versions[0].num, "1.1.0"); + + // c1 has no dependent crates. + let deps = anon.reverse_dependencies("c2"); + assert_eq!(deps.dependencies.len(), 0); + assert_eq!(deps.meta.total, 0); +} + +#[test] +fn reverse_dependencies_when_old_version_doesnt_depend_but_new_does() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + let c1 = CrateBuilder::new("c1", user.id) + .version("1.1.0") + .expect_build(conn); + CrateBuilder::new("c2", user.id) + .version("1.0.0") + .version(VersionBuilder::new("2.0.0").dependency(&c1, None)) + .expect_build(conn); + }); + + let deps = anon.reverse_dependencies("c1"); + assert_eq!(deps.dependencies.len(), 1); + assert_eq!(deps.meta.total, 1); + assert_eq!(deps.dependencies[0].crate_id, "c1"); +} + +#[test] +fn reverse_dependencies_when_old_version_depended_but_new_doesnt() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + let c1 = CrateBuilder::new("c1", user.id) + .version("1.0.0") + .expect_build(conn); + CrateBuilder::new("c2", user.id) + .version(VersionBuilder::new("1.0.0").dependency(&c1, None)) + .version("2.0.0") + .expect_build(conn); + }); + + let deps = anon.reverse_dependencies("c1"); + assert_eq!(deps.dependencies.len(), 0); + assert_eq!(deps.meta.total, 0); +} + +#[test] +fn prerelease_versions_not_included_in_reverse_dependencies() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + let c1 = CrateBuilder::new("c1", user.id) + .version("1.0.0") + .expect_build(conn); + CrateBuilder::new("c2", user.id) + .version("1.1.0-pre") + .expect_build(conn); + CrateBuilder::new("c3", user.id) + .version(VersionBuilder::new("1.0.0").dependency(&c1, None)) + .version("1.1.0-pre") + .expect_build(conn); + }); + + let deps = anon.reverse_dependencies("c1"); + assert_eq!(deps.dependencies.len(), 1); + assert_eq!(deps.meta.total, 1); + assert_eq!(deps.dependencies[0].crate_id, "c1"); +} + +#[test] +fn yanked_versions_not_included_in_reverse_dependencies() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + let c1 = CrateBuilder::new("c1", user.id) + .version("1.0.0") + .expect_build(conn); + CrateBuilder::new("c2", user.id) + .version("1.0.0") + .version(VersionBuilder::new("2.0.0").dependency(&c1, None)) + .expect_build(conn); + }); + + let deps = anon.reverse_dependencies("c1"); + assert_eq!(deps.dependencies.len(), 1); + assert_eq!(deps.meta.total, 1); + assert_eq!(deps.dependencies[0].crate_id, "c1"); + + app.db(|conn| { + use cargo_registry::schema::versions; + use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; + + diesel::update(versions::table.filter(versions::num.eq("2.0.0"))) + .set(versions::yanked.eq(true)) + .execute(conn) + .unwrap(); + }); + + let deps = anon.reverse_dependencies("c1"); + assert_eq!(deps.dependencies.len(), 0); + assert_eq!(deps.meta.total, 0); +} + +#[test] +fn reverse_dependencies_includes_published_by_user_when_present() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + use cargo_registry::schema::versions; + use diesel::{update, ExpressionMethods, RunQueryDsl}; + + let c1 = CrateBuilder::new("c1", user.id) + .version("1.0.0") + .expect_build(conn); + CrateBuilder::new("c2", user.id) + .version(VersionBuilder::new("2.0.0").dependency(&c1, None)) + .expect_build(conn); + + // Make c2's version (and,incidentally, c1's, but that doesn't matter) 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(); + + // c3's version will have the published by info recorded + CrateBuilder::new("c3", user.id) + .version(VersionBuilder::new("3.0.0").dependency(&c1, None)) + .expect_build(conn); + }); + + let deps = anon.reverse_dependencies("c1"); + assert_eq!(deps.versions.len(), 2); + + let c2_version = deps.versions.iter().find(|v| v.krate == "c2").unwrap(); + assert_none!(&c2_version.published_by); + + let c3_version = deps.versions.iter().find(|v| v.krate == "c3").unwrap(); + assert_eq!( + c3_version.published_by.as_ref().unwrap().login, + user.gh_login + ); +} + +#[test] +fn reverse_dependencies_query_supports_u64_version_number_parts() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + let large_but_valid_version_number = format!("1.0.{}", std::u64::MAX); + + app.db(|conn| { + let c1 = CrateBuilder::new("c1", user.id).expect_build(conn); + // The crate that depends on c1... + CrateBuilder::new("c2", user.id) + // ...has a patch version at the limits of what the semver crate supports + .version(VersionBuilder::new(&large_but_valid_version_number).dependency(&c1, None)) + .expect_build(conn); + }); + + let deps = anon.reverse_dependencies("c1"); + assert_eq!(deps.dependencies.len(), 1); + assert_eq!(deps.meta.total, 1); + assert_eq!(deps.dependencies[0].crate_id, "c1"); + assert_eq!(deps.versions.len(), 1); + assert_eq!(deps.versions[0].krate, "c2"); + assert_eq!(deps.versions[0].num, large_but_valid_version_number); +} diff --git a/src/tests/krate/search.rs b/src/tests/krate/search.rs new file mode 100644 index 00000000000..ca13d77fdb3 --- /dev/null +++ b/src/tests/krate/search.rs @@ -0,0 +1,694 @@ +use crate::builders::{CrateBuilder, VersionBuilder}; +use crate::util::{RequestHelper, TestApp}; +use crate::{new_category, new_user}; +use cargo_registry::models::Category; +use cargo_registry::schema::crates; +use diesel::{dsl::*, prelude::*, update}; +use http::StatusCode; + +#[test] +fn index() { + let (app, anon) = TestApp::init().empty(); + let json = anon.search(""); + assert_eq!(json.crates.len(), 0); + assert_eq!(json.meta.total, 0); + + let krate = app.db(|conn| { + let u = new_user("foo").create_or_update(None, conn).unwrap(); + CrateBuilder::new("fooindex", u.id).expect_build(conn) + }); + + let json = anon.search(""); + assert_eq!(json.crates.len(), 1); + assert_eq!(json.meta.total, 1); + assert_eq!(json.crates[0].name, krate.name); + assert_eq!(json.crates[0].id, krate.name); +} + +#[test] +#[allow(clippy::cognitive_complexity)] +fn index_queries() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + let (krate, krate2) = app.db(|conn| { + let krate = CrateBuilder::new("foo_index_queries", user.id) + .readme("readme") + .description("description") + .keyword("kw1") + .expect_build(conn); + + let krate2 = CrateBuilder::new("BAR_INDEX_QUERIES", user.id) + .keyword("KW1") + .expect_build(conn); + + CrateBuilder::new("foo", user.id) + .keyword("kw3") + .expect_build(conn); + + CrateBuilder::new("two-keywords", user.id) + .keyword("kw1") + .keyword("kw3") + .expect_build(conn); + (krate, krate2) + }); + + assert_eq!(anon.search("q=baz").meta.total, 0); + + // All of these fields should be indexed/searched by the queries + assert_eq!(anon.search("q=foo").meta.total, 2); + assert_eq!(anon.search("q=kw1").meta.total, 3); + assert_eq!(anon.search("q=readme").meta.total, 1); + assert_eq!(anon.search("q=description").meta.total, 1); + + assert_eq!(anon.search_by_user_id(user.id).crates.len(), 4); + assert_eq!(anon.search_by_user_id(0).crates.len(), 0); + + assert_eq!(anon.search("letter=F").crates.len(), 2); + assert_eq!(anon.search("letter=B").crates.len(), 1); + assert_eq!(anon.search("letter=b").crates.len(), 1); + assert_eq!(anon.search("letter=c").crates.len(), 0); + + assert_eq!(anon.search("keyword=kw1").crates.len(), 3); + assert_eq!(anon.search("keyword=KW1").crates.len(), 3); + assert_eq!(anon.search("keyword=kw2").crates.len(), 0); + assert_eq!(anon.search("all_keywords=kw1 kw3").crates.len(), 1); + + assert_eq!(anon.search("q=foo&keyword=kw1").crates.len(), 1); + assert_eq!(anon.search("q=foo2&keyword=kw1").crates.len(), 0); + + app.db(|conn| { + new_category("Category 1", "cat1", "Category 1 crates") + .create_or_update(conn) + .unwrap(); + new_category("Category 1::Ba'r", "cat1::bar", "Ba'r crates") + .create_or_update(conn) + .unwrap(); + Category::update_crate(conn, &krate, &["cat1"]).unwrap(); + Category::update_crate(conn, &krate2, &["cat1::bar"]).unwrap(); + }); + + let cl = anon.search("category=cat1"); + assert_eq!(cl.crates.len(), 2); + assert_eq!(cl.meta.total, 2); + + let cl = anon.search("category=cat1::bar"); + assert_eq!(cl.crates.len(), 1); + assert_eq!(cl.meta.total, 1); + + let cl = anon.search("keyword=cat2"); + assert_eq!(cl.crates.len(), 0); + assert_eq!(cl.meta.total, 0); + + let cl = anon.search("q=readme&category=cat1"); + assert_eq!(cl.crates.len(), 1); + assert_eq!(cl.meta.total, 1); + + let cl = anon.search("keyword=kw1&category=cat1"); + assert_eq!(cl.crates.len(), 2); + assert_eq!(cl.meta.total, 2); + + let cl = anon.search("keyword=kw3&category=cat1"); + assert_eq!(cl.crates.len(), 0); + assert_eq!(cl.meta.total, 0); +} + +#[test] +fn search_includes_crates_where_name_is_stopword() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + app.db(|conn| { + CrateBuilder::new("which", user.id).expect_build(conn); + CrateBuilder::new("should_be_excluded", user.id) + .readme("crate which does things") + .expect_build(conn); + }); + let json = anon.search("q=which"); + assert_eq!(json.crates.len(), 1); + assert_eq!(json.meta.total, 1); +} + +#[test] +fn exact_match_first_on_queries() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + CrateBuilder::new("foo_exact", user.id) + .description("bar_exact baz_exact") + .expect_build(conn); + + CrateBuilder::new("bar-exact", user.id) + .description("foo_exact baz_exact foo-exact baz_exact") + .expect_build(conn); + + CrateBuilder::new("baz_exact", user.id) + .description("foo-exact bar_exact foo-exact bar_exact foo_exact bar_exact") + .expect_build(conn); + + CrateBuilder::new("other_exact", user.id) + .description("other_exact") + .expect_build(conn); + }); + + let json = anon.search("q=foo-exact"); + assert_eq!(json.meta.total, 3); + assert_eq!(json.crates[0].name, "foo_exact"); + assert_eq!(json.crates[1].name, "baz_exact"); + assert_eq!(json.crates[2].name, "bar-exact"); + + let json = anon.search("q=bar_exact"); + assert_eq!(json.meta.total, 3); + assert_eq!(json.crates[0].name, "bar-exact"); + assert_eq!(json.crates[1].name, "baz_exact"); + assert_eq!(json.crates[2].name, "foo_exact"); + + let json = anon.search("q=baz_exact"); + assert_eq!(json.meta.total, 3); + assert_eq!(json.crates[0].name, "baz_exact"); + assert_eq!(json.crates[1].name, "bar-exact"); + assert_eq!(json.crates[2].name, "foo_exact"); +} + +#[test] +#[allow(clippy::cognitive_complexity)] +fn index_sorting() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + let krate1 = CrateBuilder::new("foo_sort", user.id) + .description("bar_sort baz_sort const") + .downloads(50) + .recent_downloads(50) + .expect_build(conn); + + let krate2 = CrateBuilder::new("bar_sort", user.id) + .description("foo_sort baz_sort foo_sort baz_sort const") + .downloads(3333) + .recent_downloads(0) + .expect_build(conn); + + let krate3 = CrateBuilder::new("baz_sort", user.id) + .description("foo_sort bar_sort foo_sort bar_sort foo_sort bar_sort const") + .downloads(100_000) + .recent_downloads(10) + .expect_build(conn); + + let krate4 = CrateBuilder::new("other_sort", user.id) + .description("other_sort const") + .downloads(999_999) + .expect_build(conn); + + // Set the created at column for each crate + update(&krate1) + .set(crates::created_at.eq(now - 4.weeks())) + .execute(conn) + .unwrap(); + update(&krate2) + .set(crates::created_at.eq(now - 1.weeks())) + .execute(conn) + .unwrap(); + update(&krate3) + .set(crates::created_at.eq(now - 2.weeks())) + .execute(conn) + .unwrap(); + update(&krate4) + .set(crates::created_at.eq(now - 3.weeks())) + .execute(conn) + .unwrap(); + + // Set the updated at column for each crate + update(&krate1) + .set(crates::updated_at.eq(now - 3.weeks())) + .execute(conn) + .unwrap(); + update(&krate2) + .set(crates::updated_at.eq(now - 5.days())) + .execute(conn) + .unwrap(); + update(&krate3) + .set(crates::updated_at.eq(now - 10.seconds())) + .execute(conn) + .unwrap(); + update(&krate4) + .set(crates::updated_at.eq(now)) + .execute(conn) + .unwrap(); + }); + + // Sort by downloads + let json = anon.search("sort=downloads"); + assert_eq!(json.meta.total, 4); + assert_eq!(json.crates[0].name, "other_sort"); + assert_eq!(json.crates[1].name, "baz_sort"); + assert_eq!(json.crates[2].name, "bar_sort"); + assert_eq!(json.crates[3].name, "foo_sort"); + + // Sort by recent-downloads + let json = anon.search("sort=recent-downloads"); + assert_eq!(json.meta.total, 4); + assert_eq!(json.crates[0].name, "foo_sort"); + assert_eq!(json.crates[1].name, "baz_sort"); + assert_eq!(json.crates[2].name, "bar_sort"); + assert_eq!(json.crates[3].name, "other_sort"); + + // Sort by recent-updates + let json = anon.search("sort=recent-updates"); + assert_eq!(json.meta.total, 4); + assert_eq!(json.crates[0].name, "other_sort"); + assert_eq!(json.crates[1].name, "baz_sort"); + assert_eq!(json.crates[2].name, "bar_sort"); + assert_eq!(json.crates[3].name, "foo_sort"); + + // Sort by new + let json = anon.search("sort=new"); + assert_eq!(json.meta.total, 4); + assert_eq!(json.crates[0].name, "bar_sort"); + assert_eq!(json.crates[1].name, "baz_sort"); + assert_eq!(json.crates[2].name, "other_sort"); + assert_eq!(json.crates[3].name, "foo_sort"); + + // Test for bug with showing null results first when sorting + // by descending downloads + let json = anon.search("sort=recent-downloads"); + assert_eq!(json.meta.total, 4); + assert_eq!(json.crates[0].name, "foo_sort"); + assert_eq!(json.crates[1].name, "baz_sort"); + assert_eq!(json.crates[2].name, "bar_sort"); + assert_eq!(json.crates[3].name, "other_sort"); +} + +#[test] +#[allow(clippy::cognitive_complexity)] +fn exact_match_on_queries_with_sort() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + let krate1 = CrateBuilder::new("foo_sort", user.id) + .description("bar_sort baz_sort const") + .downloads(50) + .recent_downloads(50) + .expect_build(conn); + + let krate2 = CrateBuilder::new("bar_sort", user.id) + .description("foo_sort baz_sort foo_sort baz_sort const") + .downloads(3333) + .recent_downloads(0) + .expect_build(conn); + + let krate3 = CrateBuilder::new("baz_sort", user.id) + .description("foo_sort bar_sort foo_sort bar_sort foo_sort bar_sort const") + .downloads(100_000) + .recent_downloads(10) + .expect_build(conn); + + let krate4 = CrateBuilder::new("other_sort", user.id) + .description("other_sort const") + .downloads(999_999) + .expect_build(conn); + + // Set the created at column for each crate + update(&krate1) + .set(crates::created_at.eq(now - 4.weeks())) + .execute(conn) + .unwrap(); + update(&krate2) + .set(crates::created_at.eq(now - 1.weeks())) + .execute(conn) + .unwrap(); + update(&krate3) + .set(crates::created_at.eq(now - 2.weeks())) + .execute(conn) + .unwrap(); + update(&krate4) + .set(crates::created_at.eq(now - 3.weeks())) + .execute(conn) + .unwrap(); + + // Set the updated at column for each crate + update(&krate1) + .set(crates::updated_at.eq(now - 3.weeks())) + .execute(&*conn) + .unwrap(); + update(&krate2) + .set(crates::updated_at.eq(now - 5.days())) + .execute(&*conn) + .unwrap(); + update(&krate3) + .set(crates::updated_at.eq(now - 10.seconds())) + .execute(&*conn) + .unwrap(); + update(&krate4) + .set(crates::updated_at.eq(now)) + .execute(&*conn) + .unwrap(); + }); + + // Sort by downloads + let json = anon.search("q=foo_sort&sort=downloads"); + assert_eq!(json.meta.total, 3); + assert_eq!(json.crates[0].name, "foo_sort"); + assert_eq!(json.crates[1].name, "baz_sort"); + assert_eq!(json.crates[2].name, "bar_sort"); + + let json = anon.search("q=bar_sort&sort=downloads"); + assert_eq!(json.meta.total, 3); + assert_eq!(json.crates[0].name, "bar_sort"); + assert_eq!(json.crates[1].name, "baz_sort"); + assert_eq!(json.crates[2].name, "foo_sort"); + + let json = anon.search("q=baz_sort&sort=downloads"); + assert_eq!(json.meta.total, 3); + assert_eq!(json.crates[0].name, "baz_sort"); + assert_eq!(json.crates[1].name, "bar_sort"); + assert_eq!(json.crates[2].name, "foo_sort"); + + let json = anon.search("q=const&sort=downloads"); + assert_eq!(json.meta.total, 4); + assert_eq!(json.crates[0].name, "other_sort"); + assert_eq!(json.crates[1].name, "baz_sort"); + assert_eq!(json.crates[2].name, "bar_sort"); + assert_eq!(json.crates[3].name, "foo_sort"); + + // Sort by recent-downloads + let json = anon.search("q=bar_sort&sort=recent-downloads"); + assert_eq!(json.meta.total, 3); + assert_eq!(json.crates[0].name, "bar_sort"); + assert_eq!(json.crates[1].name, "foo_sort"); + assert_eq!(json.crates[2].name, "baz_sort"); + + // Sort by recent-updates + let json = anon.search("q=bar_sort&sort=recent-updates"); + assert_eq!(json.meta.total, 3); + assert_eq!(json.crates[0].name, "baz_sort"); + assert_eq!(json.crates[1].name, "bar_sort"); + assert_eq!(json.crates[2].name, "foo_sort"); + + // Sort by new + let json = anon.search("q=bar_sort&sort=new"); + assert_eq!(json.meta.total, 3); + assert_eq!(json.crates[0].name, "bar_sort"); + assert_eq!(json.crates[1].name, "baz_sort"); + assert_eq!(json.crates[2].name, "foo_sort"); + + // Test for bug with showing null results first when sorting + // by descending downloads + let json = anon.search("sort=recent-downloads"); + assert_eq!(json.meta.total, 4); + assert_eq!(json.crates[0].name, "foo_sort"); + assert_eq!(json.crates[1].name, "baz_sort"); + assert_eq!(json.crates[2].name, "bar_sort"); + assert_eq!(json.crates[3].name, "other_sort"); +} + +#[test] +fn loose_search_order() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + let ordered = app.db(|conn| { + // exact match should be first + let one = CrateBuilder::new("temp", user.id) + .readme("readme") + .description("description") + .keyword("kw1") + .expect_build(conn); + // temp_udp should match second because of _ + let two = CrateBuilder::new("temp_utp", user.id) + .readme("readme") + .description("description") + .keyword("kw1") + .expect_build(conn); + // evalrs should match 3rd because of readme + let three = CrateBuilder::new("evalrs", user.id) + .readme("evalrs_temp evalrs_temp evalrs_temp") + .description("description") + .keyword("kw1") + .expect_build(conn); + // tempfile should appear 4th + let four = CrateBuilder::new("tempfile", user.id) + .readme("readme") + .description("description") + .keyword("kw1") + .expect_build(conn); + vec![one, two, three, four] + }); + let search_temp = anon.search("q=temp"); + assert_eq!(search_temp.meta.total, 4); + assert_eq!(search_temp.crates.len(), 4); + for (lhs, rhs) in search_temp.crates.iter().zip(ordered) { + assert_eq!(lhs.name, rhs.name); + } + + let search_temp = anon.search("q=te"); + assert_eq!(search_temp.meta.total, 3); + assert_eq!(search_temp.crates.len(), 3); +} + +#[test] +fn index_include_yanked() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + CrateBuilder::new("unyanked", user.id) + .version(VersionBuilder::new("1.0.0")) + .version(VersionBuilder::new("2.0.0")) + .expect_build(conn); + + CrateBuilder::new("newest_yanked", user.id) + .version(VersionBuilder::new("1.0.0")) + .version(VersionBuilder::new("2.0.0").yanked(true)) + .expect_build(conn); + + CrateBuilder::new("oldest_yanked", user.id) + .version(VersionBuilder::new("1.0.0").yanked(true)) + .version(VersionBuilder::new("2.0.0")) + .expect_build(conn); + + CrateBuilder::new("all_yanked", user.id) + .version(VersionBuilder::new("1.0.0").yanked(true)) + .version(VersionBuilder::new("2.0.0").yanked(true)) + .expect_build(conn); + }); + + // Include fully yanked (all versions were yanked) crates + let json = anon.search("include_yanked=yes&sort=alphabetical"); + assert_eq!(json.meta.total, 4); + assert_eq!(json.crates[0].name, "all_yanked"); + assert_eq!(json.crates[1].name, "newest_yanked"); + assert_eq!(json.crates[2].name, "oldest_yanked"); + assert_eq!(json.crates[3].name, "unyanked"); + + // Do not include fully yanked (all versions were yanked) crates + let json = anon.search("include_yanked=no&sort=alphabetical"); + assert_eq!(json.meta.total, 3); + assert_eq!(json.crates[0].name, "newest_yanked"); + assert_eq!(json.crates[1].name, "oldest_yanked"); + assert_eq!(json.crates[2].name, "unyanked"); +} + +#[test] +fn yanked_versions_are_not_considered_for_max_version() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + CrateBuilder::new("foo_yanked_version", user.id) + .description("foo") + .version("1.0.0") + .version(VersionBuilder::new("1.1.0").yanked(true)) + .expect_build(conn); + }); + + let json = anon.search("q=foo"); + assert_eq!(json.meta.total, 1); + assert_eq!(json.crates[0].max_version, "1.0.0"); +} + +/* Given two crates, one with downloads less than 90 days ago, the + other with all downloads greater than 90 days ago, check that + the order returned is by recent downloads, descending. Check + also that recent download counts are returned in recent_downloads, + and total downloads counts are returned in downloads, and that + these numbers do not overlap. +*/ +#[test] +fn test_recent_download_count() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + // More than 90 days ago + CrateBuilder::new("green_ball", user.id) + .description("For fetching") + .downloads(10) + .recent_downloads(0) + .expect_build(conn); + + CrateBuilder::new("sweet_potato_snack", user.id) + .description("For when better than usual") + .downloads(5) + .recent_downloads(2) + .expect_build(conn); + }); + + let json = anon.search("sort=recent-downloads"); + + assert_eq!(json.meta.total, 2); + + assert_eq!(json.crates[0].name, "sweet_potato_snack"); + assert_eq!(json.crates[1].name, "green_ball"); + + assert_eq!(json.crates[0].recent_downloads, Some(2)); + assert_eq!(json.crates[0].downloads, 5); + + assert_eq!(json.crates[1].recent_downloads, Some(0)); + assert_eq!(json.crates[1].downloads, 10); +} + +/* Given one crate with zero downloads, check that the crate + still shows up in index results, but that it displays 0 + for both recent downloads and downloads. +*/ +#[test] +fn test_zero_downloads() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + // More than 90 days ago + CrateBuilder::new("green_ball", user.id) + .description("For fetching") + .downloads(0) + .recent_downloads(0) + .expect_build(conn); + }); + + let json = anon.search("sort=recent-downloads"); + assert_eq!(json.meta.total, 1); + assert_eq!(json.crates[0].name, "green_ball"); + assert_eq!(json.crates[0].recent_downloads, Some(0)); + assert_eq!(json.crates[0].downloads, 0); +} + +/* Given two crates, one with more all-time downloads, the other with + more downloads in the past 90 days, check that the index page for + categories and keywords is sorted by recent downlaods by default. +*/ +#[test] +fn test_default_sort_recent() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + let (green_crate, potato_crate) = app.db(|conn| { + // More than 90 days ago + let green_crate = CrateBuilder::new("green_ball", user.id) + .description("For fetching") + .keyword("dog") + .downloads(10) + .recent_downloads(10) + .expect_build(conn); + + let potato_crate = CrateBuilder::new("sweet_potato_snack", user.id) + .description("For when better than usual") + .keyword("dog") + .downloads(20) + .recent_downloads(0) + .expect_build(conn); + + (green_crate, potato_crate) + }); + + // test that index for keywords is sorted by recent_downloads + // by default + let json = anon.search("keyword=dog"); + + assert_eq!(json.meta.total, 2); + + assert_eq!(json.crates[0].name, "green_ball"); + assert_eq!(json.crates[1].name, "sweet_potato_snack"); + + assert_eq!(json.crates[0].recent_downloads, Some(10)); + assert_eq!(json.crates[0].downloads, 10); + + assert_eq!(json.crates[1].recent_downloads, Some(0)); + assert_eq!(json.crates[1].downloads, 20); + + app.db(|conn| { + new_category("Animal", "animal", "animal crates") + .create_or_update(conn) + .unwrap(); + Category::update_crate(conn, &green_crate, &["animal"]).unwrap(); + Category::update_crate(conn, &potato_crate, &["animal"]).unwrap(); + }); + + // test that index for categories is sorted by recent_downloads + // by default + let json = anon.search("category=animal"); + + assert_eq!(json.meta.total, 2); + + assert_eq!(json.crates[0].name, "green_ball"); + assert_eq!(json.crates[1].name, "sweet_potato_snack"); + + assert_eq!(json.crates[0].recent_downloads, Some(10)); + assert_eq!(json.crates[0].downloads, 10); + + assert_eq!(json.crates[1].recent_downloads, Some(0)); + assert_eq!(json.crates[1].downloads, 20); +} + +#[test] +fn pagination_links_included_if_applicable() { + let (app, anon, user) = TestApp::init().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 page1 = anon.search("per_page=1"); + let page2 = anon.search("page=2&per_page=1"); + let page3 = anon.search("page=3&per_page=1"); + let page4 = anon.search("page=4&per_page=1"); + + assert_eq!(Some("?per_page=1&page=2".to_string()), page1.meta.next_page); + assert_eq!(None, page1.meta.prev_page); + assert_eq!(Some("?page=3&per_page=1".to_string()), page2.meta.next_page); + assert_eq!(Some("?page=1&per_page=1".to_string()), page2.meta.prev_page); + assert_eq!(None, page4.meta.next_page); + assert_eq!(Some("?page=2&per_page=1".to_string()), page3.meta.prev_page); +} + +#[test] +fn pagination_parameters_only_accept_integers() { + let (app, anon, user) = TestApp::init().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 invalid_per_page_json = anon + .get_with_query::<()>("/api/v1/crates", "page=1&per_page=100%22%EF%BC%8Cexception") + .bad_with_status(StatusCode::BAD_REQUEST); + assert_eq!( + invalid_per_page_json.errors[0].detail, + "invalid digit found in string" + ); + + let invalid_page_json = anon + .get_with_query::<()>("/api/v1/crates", "page=100%22%EF%BC%8Cexception&per_page=1") + .bad_with_status(StatusCode::BAD_REQUEST); + assert_eq!( + invalid_page_json.errors[0].detail, + "invalid digit found in string" + ); +} diff --git a/src/tests/krate/show.rs b/src/tests/krate/show.rs new file mode 100644 index 00000000000..6177fc62185 --- /dev/null +++ b/src/tests/krate/show.rs @@ -0,0 +1,84 @@ +use crate::builders::{CrateBuilder, VersionBuilder}; +use crate::util::{RequestHelper, TestApp}; +use diesel::prelude::*; + +#[test] +fn show() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + let krate = app.db(|conn| { + use cargo_registry::schema::versions; + use diesel::{update, ExpressionMethods}; + + let krate = CrateBuilder::new("foo_show", user.id) + .description("description") + .documentation("https://example.com") + .homepage("http://example.com") + .version(VersionBuilder::new("1.0.0")) + .version(VersionBuilder::new("0.5.0")) + .version(VersionBuilder::new("0.5.1")) + .keyword("kw1") + .downloads(20) + .recent_downloads(10) + .expect_build(conn); + + // Make version 1.0.0 mimic a version published before we started recording who published + // versions + let none: Option = None; + update(versions::table) + .filter(versions::num.eq("1.0.0")) + .set(versions::published_by.eq(none)) + .execute(conn) + .unwrap(); + + krate + }); + + let json = anon.show_crate("foo_show"); + assert_eq!(json.krate.name, krate.name); + assert_eq!(json.krate.id, krate.name); + assert_eq!(json.krate.description, krate.description); + assert_eq!(json.krate.homepage, krate.homepage); + assert_eq!(json.krate.documentation, krate.documentation); + assert_eq!(json.krate.keywords, Some(vec!["kw1".into()])); + assert_eq!(json.krate.recent_downloads, Some(10)); + let versions = json.krate.versions.as_ref().unwrap(); + assert_eq!(versions.len(), 3); + assert_eq!(json.versions.len(), 3); + + assert_eq!(json.versions[0].id, versions[0]); + assert_eq!(json.versions[0].krate, json.krate.id); + assert_eq!(json.versions[0].num, "1.0.0"); + assert_none!(&json.versions[0].published_by); + let suffix = "/api/v1/crates/foo_show/1.0.0/download"; + assert!( + json.versions[0].dl_path.ends_with(suffix), + "bad suffix {}", + json.versions[0].dl_path + ); + assert_eq!(1, json.keywords.len()); + assert_eq!("kw1", json.keywords[0].id); + + assert_eq!(json.versions[1].num, "0.5.1"); + assert_eq!(json.versions[2].num, "0.5.0"); + assert_eq!( + json.versions[1].published_by.as_ref().unwrap().login, + user.gh_login + ); +} + +#[test] +fn block_bad_documentation_url() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + CrateBuilder::new("foo_bad_doc_url", user.id) + .documentation("http://rust-ci.org/foo/foo_bad_doc_url/doc/foo_bad_doc_url/") + .expect_build(conn) + }); + + let json = anon.show_crate("foo_bad_doc_url"); + assert_eq!(json.krate.documentation, None); +} diff --git a/src/tests/krate/summary.rs b/src/tests/krate/summary.rs new file mode 100644 index 00000000000..8eb8c0078e1 --- /dev/null +++ b/src/tests/krate/summary.rs @@ -0,0 +1,109 @@ +use crate::builders::{CrateBuilder, VersionBuilder}; +use crate::new_category; +use crate::util::{RequestHelper, TestApp}; +use cargo_registry::schema::metadata; +use cargo_registry::views::{EncodableCategory, EncodableCrate, EncodableKeyword}; +use chrono::Utc; +use diesel::{update, ExpressionMethods, RunQueryDsl}; + +#[derive(Deserialize)] +struct SummaryResponse { + num_downloads: i64, + num_crates: i64, + new_crates: Vec, + most_downloaded: Vec, + most_recently_downloaded: Vec, + just_updated: Vec, + popular_keywords: Vec, + popular_categories: Vec, +} + +#[test] +fn summary_doesnt_die() { + let (_, anon) = TestApp::init().empty(); + anon.get::("/api/v1/summary").good(); +} + +#[test] +fn summary_new_crates() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + app.db(|conn| { + let now_ = Utc::now().naive_utc(); + let now_plus_two = now_ + chrono::Duration::seconds(2); + + new_category("Category 1", "cat1", "Category 1 crates") + .create_or_update(conn) + .unwrap(); + + CrateBuilder::new("some_downloads", user.id) + .version(VersionBuilder::new("0.1.0")) + .description("description") + .keyword("popular") + .category("cat1") + .downloads(20) + .recent_downloads(10) + .expect_build(conn); + + CrateBuilder::new("most_recent_downloads", user.id) + .version(VersionBuilder::new("0.2.0")) + .keyword("popular") + .category("cat1") + .downloads(5000) + .recent_downloads(50) + .expect_build(conn); + + CrateBuilder::new("just_updated", user.id) + .version(VersionBuilder::new("0.1.0")) + .version(VersionBuilder::new("0.1.2")) + // update 'just_updated' krate. Others won't appear because updated_at == created_at. + .updated_at(now_) + .expect_build(conn); + + CrateBuilder::new("just_updated_patch", user.id) + .version(VersionBuilder::new("0.1.0")) + .version(VersionBuilder::new("0.2.0")) + // Add a patch version be newer than the other versions, including the higher one. + .version(VersionBuilder::new("0.1.1").created_at(now_plus_two)) + .updated_at(now_plus_two) + .expect_build(conn); + + CrateBuilder::new("with_downloads", user.id) + .version(VersionBuilder::new("0.3.0")) + .keyword("popular") + .downloads(1000) + .expect_build(conn); + + // set total_downloads global value for `num_downloads` prop + update(metadata::table) + .set(metadata::total_downloads.eq(6000)) + .execute(&*conn) + .unwrap(); + }); + + let json: SummaryResponse = anon.get("/api/v1/summary").good(); + + assert_eq!(json.num_crates, 5); + assert_eq!(json.num_downloads, 6000); + assert_eq!(json.most_downloaded[0].name, "most_recent_downloads"); + assert_eq!(json.most_downloaded[0].downloads, 5000); + assert_eq!(json.most_downloaded[0].recent_downloads, Some(50)); + assert_eq!( + json.most_recently_downloaded[0].name, + "most_recent_downloads" + ); + assert_eq!(json.most_recently_downloaded[0].recent_downloads, Some(50)); + assert_eq!(json.popular_keywords[0].keyword, "popular"); + assert_eq!(json.popular_categories[0].category, "Category 1"); + assert_eq!(json.just_updated.len(), 2); + + assert_eq!(json.just_updated[0].name, "just_updated_patch"); + assert_eq!(json.just_updated[0].max_version, "0.2.0"); + assert_eq!(json.just_updated[0].newest_version, "0.1.1"); + + assert_eq!(json.just_updated[1].name, "just_updated"); + assert_eq!(json.just_updated[1].max_version, "0.1.2"); + assert_eq!(json.just_updated[1].newest_version, "0.1.2"); + + assert_eq!(json.new_crates.len(), 5); +} diff --git a/src/tests/krate/versions.rs b/src/tests/krate/versions.rs new file mode 100644 index 00000000000..13ecc69300b --- /dev/null +++ b/src/tests/krate/versions.rs @@ -0,0 +1,43 @@ +use crate::builders::CrateBuilder; +use crate::util::{RequestHelper, TestApp}; +use cargo_registry::schema::versions; +use cargo_registry::views::EncodableVersion; +use diesel::{prelude::*, update}; + +#[derive(Deserialize)] +struct VersionsList { + versions: Vec, +} + +#[test] +fn versions() { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + app.db(|conn| { + CrateBuilder::new("foo_versions", user.id) + .version("0.5.1") + .version("1.0.0") + .version("0.5.0") + .expect_build(conn); + // Make version 1.0.0 mimic a version published before we started recording who published + // versions + let none: Option = None; + update(versions::table) + .filter(versions::num.eq("1.0.0")) + .set(versions::published_by.eq(none)) + .execute(conn) + .unwrap(); + }); + + let json: VersionsList = anon.get("/api/v1/crates/foo_versions/versions").good(); + + assert_eq!(json.versions.len(), 3); + assert_eq!(json.versions[0].num, "1.0.0"); + assert_eq!(json.versions[1].num, "0.5.1"); + assert_eq!(json.versions[2].num, "0.5.0"); + assert_none!(&json.versions[0].published_by); + assert_eq!( + json.versions[1].published_by.as_ref().unwrap().login, + user.gh_login + ); +} diff --git a/src/tests/krate/yanking.rs b/src/tests/krate/yanking.rs new file mode 100644 index 00000000000..ce1e7d1dead --- /dev/null +++ b/src/tests/krate/yanking.rs @@ -0,0 +1,248 @@ +use crate::builders::{CrateBuilder, PublishBuilder}; +use crate::util::{RequestHelper, TestApp}; +use crate::OkBool; +use http::StatusCode; + +impl crate::util::MockTokenUser { + /// Yank the specified version of the specified crate and run all pending background jobs + fn yank(&self, krate_name: &str, version: &str) -> crate::util::Response { + let url = format!("/api/v1/crates/{}/{}/yank", krate_name, version); + let response = self.delete(&url); + self.app().run_pending_background_jobs(); + response + } + + /// Unyank the specified version of the specified crate and run all pending background jobs + fn unyank(&self, krate_name: &str, version: &str) -> crate::util::Response { + let url = format!("/api/v1/crates/{}/{}/unyank", krate_name, version); + let response = self.put(&url, &[]); + self.app().run_pending_background_jobs(); + response + } +} + +impl crate::util::MockCookieUser { + /// Yank the specified version of the specified crate and run all pending background jobs + fn yank(&self, krate_name: &str, version: &str) -> crate::util::Response { + let url = format!("/api/v1/crates/{}/{}/yank", krate_name, version); + let response = self.delete(&url); + self.app().run_pending_background_jobs(); + response + } + + /// Unyank the specified version of the specified crate and run all pending background jobs + fn unyank(&self, krate_name: &str, version: &str) -> crate::util::Response { + let url = format!("/api/v1/crates/{}/{}/unyank", krate_name, version); + let response = self.put(&url, &[]); + self.app().run_pending_background_jobs(); + response + } +} + +#[test] +fn yank_works_as_intended() { + let (app, anon, cookie, token) = TestApp::full().with_token(); + + // Upload a new crate, putting it in the git index + let crate_to_publish = PublishBuilder::new("fyk"); + token.enqueue_publish(crate_to_publish).good(); + app.run_pending_background_jobs(); + + let crates = app.crates_from_index_head("3/f/fyk"); + assert_eq!(crates.len(), 1); + assert_some_eq!(crates[0].yanked, false); + + // make sure it's not yanked + let json = anon.show_version("fyk", "1.0.0"); + assert!(!json.version.yanked); + + // yank it + token.yank("fyk", "1.0.0").good(); + + let crates = app.crates_from_index_head("3/f/fyk"); + assert_eq!(crates.len(), 1); + assert_some_eq!(crates[0].yanked, true); + + let json = anon.show_version("fyk", "1.0.0"); + assert!(json.version.yanked); + + // un-yank it + token.unyank("fyk", "1.0.0").good(); + + let crates = app.crates_from_index_head("3/f/fyk"); + assert_eq!(crates.len(), 1); + assert_some_eq!(crates[0].yanked, false); + + let json = anon.show_version("fyk", "1.0.0"); + assert!(!json.version.yanked); + + // yank it + cookie.yank("fyk", "1.0.0").good(); + + let crates = app.crates_from_index_head("3/f/fyk"); + assert_eq!(crates.len(), 1); + assert_some_eq!(crates[0].yanked, true); + + let json = anon.show_version("fyk", "1.0.0"); + assert!(json.version.yanked); + + // un-yank it + cookie.unyank("fyk", "1.0.0").good(); + + let crates = app.crates_from_index_head("3/f/fyk"); + assert_eq!(crates.len(), 1); + assert_some_eq!(crates[0].yanked, false); + + let json = anon.show_version("fyk", "1.0.0"); + assert!(!json.version.yanked); +} + +#[test] +fn yank_by_a_non_owner_fails() { + let (app, _, _, token) = TestApp::full().with_token(); + + let another_user = app.db_new_user("bar"); + let another_user = another_user.as_model(); + app.db(|conn| { + CrateBuilder::new("foo_not", another_user.id) + .version("1.0.0") + .expect_build(conn); + }); + + let json = token + .yank("foo_not", "1.0.0") + .bad_with_status(StatusCode::OK); + assert_eq!( + json.errors[0].detail, + "must already be an owner to yank or unyank" + ); +} + +#[test] +fn yank_max_version() { + let (_, anon, _, token) = TestApp::full().with_token(); + + // Upload a new crate + let crate_to_publish = PublishBuilder::new("fyk_max"); + token.enqueue_publish(crate_to_publish).good(); + + // double check the max version + let json = anon.show_crate("fyk_max"); + assert_eq!(json.krate.max_version, "1.0.0"); + + // add version 2.0.0 + let crate_to_publish = PublishBuilder::new("fyk_max").version("2.0.0"); + let json = token.enqueue_publish(crate_to_publish).good(); + assert_eq!(json.krate.max_version, "2.0.0"); + + // yank version 1.0.0 + token.yank("fyk_max", "1.0.0").good(); + + let json = anon.show_crate("fyk_max"); + assert_eq!(json.krate.max_version, "2.0.0"); + + // unyank version 1.0.0 + token.unyank("fyk_max", "1.0.0").good(); + + let json = anon.show_crate("fyk_max"); + assert_eq!(json.krate.max_version, "2.0.0"); + + // yank version 2.0.0 + token.yank("fyk_max", "2.0.0").good(); + + let json = anon.show_crate("fyk_max"); + assert_eq!(json.krate.max_version, "1.0.0"); + + // yank version 1.0.0 + token.yank("fyk_max", "1.0.0").good(); + + let json = anon.show_crate("fyk_max"); + assert_eq!(json.krate.max_version, "0.0.0"); + + // unyank version 2.0.0 + token.unyank("fyk_max", "2.0.0").good(); + + let json = anon.show_crate("fyk_max"); + assert_eq!(json.krate.max_version, "2.0.0"); + + // unyank version 1.0.0 + token.unyank("fyk_max", "1.0.0").good(); + + let json = anon.show_crate("fyk_max"); + assert_eq!(json.krate.max_version, "2.0.0"); +} + +#[test] +fn publish_after_yank_max_version() { + let (_, anon, _, token) = TestApp::full().with_token(); + + // Upload a new crate + let crate_to_publish = PublishBuilder::new("fyk_max"); + token.enqueue_publish(crate_to_publish).good(); + + // double check the max version + let json = anon.show_crate("fyk_max"); + assert_eq!(json.krate.max_version, "1.0.0"); + + // yank version 1.0.0 + token.yank("fyk_max", "1.0.0").good(); + + let json = anon.show_crate("fyk_max"); + assert_eq!(json.krate.max_version, "0.0.0"); + + // add version 2.0.0 + let crate_to_publish = PublishBuilder::new("fyk_max").version("2.0.0"); + let json = token.enqueue_publish(crate_to_publish).good(); + assert_eq!(json.krate.max_version, "2.0.0"); + + // unyank version 1.0.0 + token.unyank("fyk_max", "1.0.0").good(); + + let json = anon.show_crate("fyk_max"); + assert_eq!(json.krate.max_version, "2.0.0"); +} + +#[test] +fn yank_records_an_audit_action() { + let (_, anon, _, token) = TestApp::full().with_token(); + + // Upload a new crate, putting it in the git index + let crate_to_publish = PublishBuilder::new("fyk"); + token.enqueue_publish(crate_to_publish).good(); + + // Yank it + token.yank("fyk", "1.0.0").good(); + + // Make sure it has one publish and one yank audit action + let json = anon.show_version("fyk", "1.0.0"); + let actions = json.version.audit_actions; + + assert_eq!(actions.len(), 2); + let action = &actions[1]; + assert_eq!(action.action, "yank"); + assert_eq!(action.user.id, token.as_model().user_id); +} + +#[test] +fn unyank_records_an_audit_action() { + let (_, anon, _, token) = TestApp::full().with_token(); + + // Upload a new crate + let crate_to_publish = PublishBuilder::new("fyk"); + token.enqueue_publish(crate_to_publish).good(); + + // Yank version 1.0.0 + token.yank("fyk", "1.0.0").good(); + + // Unyank version 1.0.0 + token.unyank("fyk", "1.0.0").good(); + + // Make sure it has one publish, one yank, and one unyank audit action + let json = anon.show_version("fyk", "1.0.0"); + let actions = json.version.audit_actions; + + assert_eq!(actions.len(), 3); + let action = &actions[2]; + assert_eq!(action.action, "unyank"); + assert_eq!(action.user.id, token.as_model().user_id); +}