diff --git a/src/tests/builders.rs b/src/tests/builders.rs deleted file mode 100644 index 5a688b764bd..00000000000 --- a/src/tests/builders.rs +++ /dev/null @@ -1,585 +0,0 @@ -//! Structs using the builder pattern that make it easier to create records in tests. - -use cargo_registry::{ - models::{Crate, Keyword, NewCrate, NewVersion, Version}, - schema::{crates, dependencies, version_downloads, versions}, - util::errors::AppResult, - views::krate_publish as u, -}; -use std::{collections::HashMap, io::Read}; - -use diesel::prelude::*; -use flate2::{write::GzEncoder, Compression}; - -/// A builder to create version records for the purpose of inserting directly into the database. -pub struct VersionBuilder<'a> { - num: semver::Version, - license: Option<&'a str>, - license_file: Option<&'a str>, - features: HashMap>, - dependencies: Vec<(i32, Option<&'static str>)>, - yanked: bool, - size: i32, -} - -impl<'a> VersionBuilder<'a> { - /// Creates a VersionBuilder from a string slice `num` representing the version's number. - /// - /// # Panics - /// - /// Panics if `num` cannot be parsed as a valid `semver::Version`. - #[track_caller] - pub fn new(num: &str) -> Self { - let num = semver::Version::parse(num).unwrap_or_else(|e| { - panic!("The version {} is not valid: {}", num, e); - }); - - VersionBuilder { - num, - license: None, - license_file: None, - features: HashMap::new(), - dependencies: Vec::new(), - yanked: false, - size: 0, - } - } - - /// Sets the version's `license` value. - pub fn license(mut self, license: Option<&'a str>) -> Self { - self.license = license; - self - } - - /// Adds a dependency to this version. - pub fn dependency(mut self, dependency: &Crate, target: Option<&'static str>) -> Self { - self.dependencies.push((dependency.id, target)); - self - } - - /// Sets the version's `yanked` value. - pub fn yanked(self, yanked: bool) -> Self { - Self { yanked, ..self } - } - - /// Sets the version's size. - pub fn size(mut self, size: i32) -> Self { - self.size = size; - self - } - - fn build( - self, - crate_id: i32, - published_by: i32, - connection: &PgConnection, - ) -> AppResult { - use diesel::{insert_into, update}; - - let license = match self.license { - Some(license) => Some(license.to_owned()), - None => None, - }; - - let mut vers = NewVersion::new( - crate_id, - &self.num, - &self.features, - license, - self.license_file, - self.size, - published_by, - )? - .save(connection, &[], "someone@example.com")?; - - if self.yanked { - vers = update(&vers) - .set(versions::yanked.eq(true)) - .get_result(connection)?; - } - - let new_deps = self - .dependencies - .into_iter() - .map(|(crate_id, target)| { - ( - dependencies::version_id.eq(vers.id), - dependencies::req.eq(">= 0"), - dependencies::crate_id.eq(crate_id), - dependencies::target.eq(target), - dependencies::optional.eq(false), - dependencies::default_features.eq(false), - dependencies::features.eq(Vec::::new()), - ) - }) - .collect::>(); - insert_into(dependencies::table) - .values(&new_deps) - .execute(connection)?; - - Ok(vers) - } - - /// Consumes the builder and creates the version record in the database. - /// - /// # Panics - /// - /// Panics (and fails the test) if any part of inserting the version record fails. - #[track_caller] - pub fn expect_build( - self, - crate_id: i32, - published_by: i32, - connection: &PgConnection, - ) -> Version { - self.build(crate_id, published_by, connection) - .unwrap_or_else(|e| { - panic!("Unable to create version: {:?}", e); - }) - } -} - -impl<'a> From<&'a str> for VersionBuilder<'a> { - fn from(num: &'a str) -> Self { - VersionBuilder::new(num) - } -} - -/// A builder to create crate records for the purpose of inserting directly into the database. -/// If you want to test logic that happens as part of a publish request, use `PublishBuilder` -/// instead. -pub struct CrateBuilder<'a> { - owner_id: i32, - krate: NewCrate<'a>, - downloads: Option, - recent_downloads: Option, - versions: Vec>, - keywords: Vec<&'a str>, -} - -impl<'a> CrateBuilder<'a> { - /// Create a new instance with the given crate name and owner. If the owner with the given ID - /// doesn't exist in the database, `expect_build` will fail. - pub fn new(name: &str, owner_id: i32) -> CrateBuilder<'_> { - CrateBuilder { - owner_id, - krate: NewCrate { - name, - ..NewCrate::default() - }, - downloads: None, - recent_downloads: None, - versions: Vec::new(), - keywords: Vec::new(), - } - } - - /// Sets the crate's `description` value. - pub fn description(mut self, description: &'a str) -> Self { - self.krate.description = Some(description); - self - } - - /// Sets the crate's `documentation` URL. - pub fn documentation(mut self, documentation: &'a str) -> Self { - self.krate.documentation = Some(documentation); - self - } - - /// Sets the crate's `homepage` URL. - pub fn homepage(mut self, homepage: &'a str) -> Self { - self.krate.homepage = Some(homepage); - self - } - - /// Sets the crate's `readme` content. - pub fn readme(mut self, readme: &'a str) -> Self { - self.krate.readme = Some(readme); - self - } - - /// Sets the crate's `max_upload_size` override value. - pub fn max_upload_size(mut self, max_upload_size: i32) -> Self { - self.krate.max_upload_size = Some(max_upload_size); - self - } - - /// Sets the crate's number of downloads that happened more than 90 days ago. The total - /// number of downloads for this crate will be this plus the number of recent downloads. - pub fn downloads(mut self, downloads: i32) -> Self { - self.downloads = Some(downloads); - self - } - - /// Sets the crate's number of downloads in the last 90 days. The total number of downloads - /// for this crate will be this plus the number of downloads set with the `downloads` method. - pub fn recent_downloads(mut self, recent_downloads: i32) -> Self { - self.recent_downloads = Some(recent_downloads); - self - } - - /// Adds a version record to be associated with the crate record when the crate record is - /// built. - pub fn version>>(mut self, version: T) -> Self { - self.versions.push(version.into()); - self - } - - /// Adds a keyword to the crate. - pub fn keyword(mut self, keyword: &'a str) -> Self { - self.keywords.push(keyword); - self - } - - fn build(mut self, connection: &PgConnection) -> AppResult { - use diesel::{insert_into, select, update}; - - let mut krate = self - .krate - .create_or_update(connection, self.owner_id, None)?; - - // Since we are using `NewCrate`, we can't set all the - // crate properties in a single DB call. - - if let Some(downloads) = self.downloads { - krate = update(&krate) - .set(crates::downloads.eq(downloads)) - .returning(cargo_registry::models::krate::ALL_COLUMNS) - .get_result(connection)?; - } - - if self.versions.is_empty() { - self.versions.push(VersionBuilder::new("0.99.0")); - } - - let mut last_version_id = 0; - for version_builder in self.versions { - last_version_id = version_builder - .build(krate.id, self.owner_id, connection)? - .id; - } - - if let Some(downloads) = self.recent_downloads { - insert_into(version_downloads::table) - .values(( - version_downloads::version_id.eq(last_version_id), - version_downloads::downloads.eq(downloads), - )) - .execute(connection)?; - - no_arg_sql_function!(refresh_recent_crate_downloads, ()); - select(refresh_recent_crate_downloads).execute(connection)?; - } - - if !self.keywords.is_empty() { - Keyword::update_crate(connection, &krate, &self.keywords)?; - } - - Ok(krate) - } - - /// Consumes the builder and creates the crate record in the database. - /// - /// # Panics - /// - /// Panics (and fails the test) if any part of inserting the crate record fails. - #[track_caller] - pub fn expect_build(self, connection: &PgConnection) -> Crate { - let name = self.krate.name; - self.build(connection).unwrap_or_else(|e| { - panic!("Unable to create crate {}: {:?}", name, e); - }) - } -} - -lazy_static! { - // The bytes of an empty tarball is not an empty vector of bytes because of tarball headers. - // Unless files are added to a PublishBuilder, the `.crate` tarball that gets uploaded - // will be empty, so precompute the empty tarball bytes to use as a default. - static ref EMPTY_TARBALL_BYTES: Vec = { - let mut empty_tarball = vec![]; - { - let mut ar = - tar::Builder::new(GzEncoder::new(&mut empty_tarball, Compression::default())); - t!(ar.finish()); - } - empty_tarball - }; -} - -/// A builder for constructing a crate for the purposes of testing publishing. If you only need -/// a crate to exist and don't need to test behavior caused by the publish request, inserting -/// a crate into the database directly by using CrateBuilder will be faster. -pub struct PublishBuilder { - pub krate_name: String, - version: semver::Version, - tarball: Vec, - deps: Vec, - desc: Option, - readme: Option, - doc_url: Option, - keywords: Vec, - categories: Vec, - badges: HashMap>, - license: Option, - license_file: Option, - authors: Vec, -} - -impl PublishBuilder { - /// Create a request to publish a crate with the given name, version 1.0.0, and no files - /// in its tarball. - pub fn new(krate_name: &str) -> Self { - PublishBuilder { - krate_name: krate_name.into(), - version: semver::Version::parse("1.0.0").unwrap(), - tarball: EMPTY_TARBALL_BYTES.to_vec(), - deps: vec![], - desc: Some("description".to_string()), - readme: None, - doc_url: None, - keywords: vec![], - categories: vec![], - badges: HashMap::new(), - license: Some("MIT".to_string()), - license_file: None, - authors: vec!["foo".to_string()], - } - } - - /// Set the version of the crate being published to something other than the default of 1.0.0. - pub fn version(mut self, version: &str) -> Self { - self.version = semver::Version::parse(version).unwrap(); - self - } - - /// Set the files in the crate's tarball. - pub fn files(self, files: &[(&str, &[u8])]) -> Self { - let mut slices = files.iter().map(|p| p.1).collect::>(); - let mut files = files - .iter() - .zip(&mut slices) - .map(|(&(name, _), data)| { - let len = data.len() as u64; - (name, data as &mut dyn Read, len) - }) - .collect::>(); - - self.files_with_io(&mut files) - } - - /// Set the tarball from a Read trait object - pub fn files_with_io(mut self, files: &mut [(&str, &mut dyn Read, u64)]) -> Self { - let mut tarball = Vec::new(); - { - let mut ar = tar::Builder::new(GzEncoder::new(&mut tarball, Compression::default())); - for &mut (name, ref mut data, size) in files { - let mut header = tar::Header::new_gnu(); - t!(header.set_path(name)); - header.set_size(size); - header.set_cksum(); - t!(ar.append(&header, data)); - } - t!(ar.finish()); - } - - self.tarball = tarball; - self - } - - /// Set the tarball directly to the given Vec of bytes - pub fn tarball(mut self, tarball: Vec) -> Self { - self.tarball = tarball; - self - } - - /// Add a dependency to this crate. Make sure the dependency already exists in the - /// database or publish will fail. - pub fn dependency(mut self, dep: DependencyBuilder) -> Self { - self.deps.push(dep.build()); - self - } - - /// Set the description of this crate - pub fn description(mut self, description: &str) -> Self { - self.desc = Some(description.to_string()); - self - } - - /// Unset the description of this crate. Publish will fail unless description is reset. - pub fn unset_description(mut self) -> Self { - self.desc = None; - self - } - - /// Set the readme of this crate - pub fn readme(mut self, readme: &str) -> Self { - self.readme = Some(readme.to_string()); - self - } - - /// Set the documentation URL of this crate - pub fn documentation(mut self, documentation: &str) -> Self { - self.doc_url = Some(documentation.to_string()); - self - } - - /// Add a keyword to this crate. - pub fn keyword(mut self, keyword: &str) -> Self { - self.keywords.push(keyword.into()); - self - } - - /// Add a category to this crate. Make sure the category already exists in the - /// database or it will be ignored. - pub fn category(mut self, slug: &str) -> Self { - self.categories.push(slug.into()); - self - } - - /// Add badges to this crate. - pub fn badges(mut self, badges: HashMap>) -> Self { - self.badges = badges; - self - } - - /// Remove the license from this crate. Publish will fail unless license or license file is set. - pub fn unset_license(mut self) -> Self { - self.license = None; - self - } - - /// Set the license file for this crate - pub fn license_file(mut self, license_file: &str) -> Self { - self.license_file = Some(license_file.into()); - self - } - - /// Add an author to this crate - pub fn author(mut self, author: &str) -> Self { - self.authors.push(author.into()); - self - } - - /// Remove the authors from this crate. Publish will fail unless authors are reset. - pub fn unset_authors(mut self) -> Self { - self.authors = vec![]; - self - } - - /// Consume this builder to make the Put request body - pub fn body(self) -> Vec { - let new_crate = u::EncodableCrateUpload { - name: u::EncodableCrateName(self.krate_name.clone()), - vers: u::EncodableCrateVersion(self.version), - features: HashMap::new(), - deps: self.deps, - authors: self.authors, - description: self.desc, - homepage: None, - documentation: self.doc_url, - readme: self.readme, - readme_file: None, - keywords: u::EncodableKeywordList( - self.keywords.into_iter().map(u::EncodableKeyword).collect(), - ), - categories: u::EncodableCategoryList( - self.categories - .into_iter() - .map(u::EncodableCategory) - .collect(), - ), - license: self.license, - license_file: self.license_file, - repository: None, - badges: Some(self.badges), - links: None, - }; - - let json = serde_json::to_string(&new_crate).unwrap(); - let mut body = Vec::new(); - body.extend( - [ - json.len() as u8, - (json.len() >> 8) as u8, - (json.len() >> 16) as u8, - (json.len() >> 24) as u8, - ] - .iter() - .cloned(), - ); - body.extend(json.as_bytes().iter().cloned()); - - let tarball = &self.tarball; - body.extend(&[ - tarball.len() as u8, - (tarball.len() >> 8) as u8, - (tarball.len() >> 16) as u8, - (tarball.len() >> 24) as u8, - ]); - body.extend(tarball); - body - } -} - -/// A builder for constructing a dependency of another crate. -pub struct DependencyBuilder { - name: String, - registry: Option, - explicit_name_in_toml: Option, - version_req: u::EncodableCrateVersionReq, -} - -impl DependencyBuilder { - /// Create a dependency on the crate with the given name. - pub fn new(name: &str) -> Self { - DependencyBuilder { - name: name.to_string(), - registry: None, - explicit_name_in_toml: None, - version_req: u::EncodableCrateVersionReq(semver::VersionReq::parse(">= 0").unwrap()), - } - } - - /// Rename this dependency. - pub fn rename(mut self, new_name: &str) -> Self { - self.explicit_name_in_toml = Some(u::EncodableCrateName(new_name.to_string())); - self - } - - /// Set an alternative registry for this dependency. - pub fn registry(mut self, registry: &str) -> Self { - self.registry = Some(registry.to_string()); - self - } - - /// Set the version requirement for this dependency. - /// - /// # Panics - /// - /// Panics if the `version_req` string specified isn't a valid `semver::VersionReq`. - #[track_caller] - pub fn version_req(mut self, version_req: &str) -> Self { - self.version_req = u::EncodableCrateVersionReq( - semver::VersionReq::parse(version_req) - .expect("version req isn't a valid semver::VersionReq"), - ); - self - } - - /// Consume this builder to create a `u::CrateDependency`. If the dependent crate doesn't - /// already exist, publishing a crate with this dependency will fail. - fn build(self) -> u::EncodableCrateDependency { - u::EncodableCrateDependency { - name: u::EncodableCrateName(self.name), - optional: false, - default_features: true, - features: Vec::new(), - version_req: self.version_req, - target: None, - kind: None, - explicit_name_in_toml: self.explicit_name_in_toml, - registry: self.registry, - } - } -} diff --git a/src/tests/builders/dependency.rs b/src/tests/builders/dependency.rs new file mode 100644 index 00000000000..8757e499845 --- /dev/null +++ b/src/tests/builders/dependency.rs @@ -0,0 +1,63 @@ +use cargo_registry::views::krate_publish as u; + +/// A builder for constructing a dependency of another crate. +pub struct DependencyBuilder { + explicit_name_in_toml: Option, + name: String, + registry: Option, + version_req: u::EncodableCrateVersionReq, +} + +impl DependencyBuilder { + /// Create a dependency on the crate with the given name. + pub fn new(name: &str) -> Self { + DependencyBuilder { + explicit_name_in_toml: None, + name: name.to_string(), + registry: None, + version_req: u::EncodableCrateVersionReq(semver::VersionReq::parse(">= 0").unwrap()), + } + } + + /// Rename this dependency. + pub fn rename(mut self, new_name: &str) -> Self { + self.explicit_name_in_toml = Some(u::EncodableCrateName(new_name.to_string())); + self + } + + /// Set an alternative registry for this dependency. + pub fn registry(mut self, registry: &str) -> Self { + self.registry = Some(registry.to_string()); + self + } + + /// Set the version requirement for this dependency. + /// + /// # Panics + /// + /// Panics if the `version_req` string specified isn't a valid `semver::VersionReq`. + #[track_caller] + pub fn version_req(mut self, version_req: &str) -> Self { + self.version_req = u::EncodableCrateVersionReq( + semver::VersionReq::parse(version_req) + .expect("version req isn't a valid semver::VersionReq"), + ); + self + } + + /// Consume this builder to create a `u::CrateDependency`. If the dependent crate doesn't + /// already exist, publishing a crate with this dependency will fail. + pub fn build(self) -> u::EncodableCrateDependency { + u::EncodableCrateDependency { + name: u::EncodableCrateName(self.name), + optional: false, + default_features: true, + features: Vec::new(), + version_req: self.version_req, + target: None, + kind: None, + explicit_name_in_toml: self.explicit_name_in_toml, + registry: self.registry, + } + } +} diff --git a/src/tests/builders/krate.rs b/src/tests/builders/krate.rs new file mode 100644 index 00000000000..5669567587c --- /dev/null +++ b/src/tests/builders/krate.rs @@ -0,0 +1,184 @@ +use cargo_registry::{ + models::{Category, Crate, Keyword, NewCrate}, + schema::{crates, version_downloads}, + util::errors::AppResult, +}; + +use chrono::NaiveDateTime; +use diesel::prelude::*; + +use super::VersionBuilder; + +/// A builder to create crate records for the purpose of inserting directly into the database. +/// If you want to test logic that happens as part of a publish request, use `PublishBuilder` +/// instead. +pub struct CrateBuilder<'a> { + categories: Vec<&'a str>, + downloads: Option, + keywords: Vec<&'a str>, + krate: NewCrate<'a>, + owner_id: i32, + recent_downloads: Option, + updated_at: Option, + versions: Vec>, +} + +impl<'a> CrateBuilder<'a> { + /// Create a new instance with the given crate name and owner. If the owner with the given ID + /// doesn't exist in the database, `expect_build` will fail. + pub fn new(name: &str, owner_id: i32) -> CrateBuilder<'_> { + CrateBuilder { + categories: Vec::new(), + downloads: None, + keywords: Vec::new(), + krate: NewCrate { + name, + ..NewCrate::default() + }, + owner_id, + recent_downloads: None, + updated_at: None, + versions: Vec::new(), + } + } + + /// Sets the crate's `description` value. + pub fn description(mut self, description: &'a str) -> Self { + self.krate.description = Some(description); + self + } + + /// Sets the crate's `documentation` URL. + pub fn documentation(mut self, documentation: &'a str) -> Self { + self.krate.documentation = Some(documentation); + self + } + + /// Sets the crate's `homepage` URL. + pub fn homepage(mut self, homepage: &'a str) -> Self { + self.krate.homepage = Some(homepage); + self + } + + /// Sets the crate's `readme` content. + pub fn readme(mut self, readme: &'a str) -> Self { + self.krate.readme = Some(readme); + self + } + + /// Sets the crate's `max_upload_size` override value. + pub fn max_upload_size(mut self, max_upload_size: i32) -> Self { + self.krate.max_upload_size = Some(max_upload_size); + self + } + + /// Sets the crate's number of downloads that happened more than 90 days ago. The total + /// number of downloads for this crate will be this plus the number of recent downloads. + pub fn downloads(mut self, downloads: i32) -> Self { + self.downloads = Some(downloads); + self + } + + /// Sets the crate's number of downloads in the last 90 days. The total number of downloads + /// for this crate will be this plus the number of downloads set with the `downloads` method. + pub fn recent_downloads(mut self, recent_downloads: i32) -> Self { + self.recent_downloads = Some(recent_downloads); + self + } + + /// Adds a version record to be associated with the crate record when the crate record is + /// built. + pub fn version>>(mut self, version: T) -> Self { + self.versions.push(version.into()); + self + } + + /// Adds a category to the crate. + pub fn category(mut self, category: &'a str) -> Self { + self.categories.push(category); + self + } + + /// Adds a keyword to the crate. + pub fn keyword(mut self, keyword: &'a str) -> Self { + self.keywords.push(keyword); + self + } + + /// Sets the crate's `updated_at` value. + pub fn updated_at(mut self, updated_at: NaiveDateTime) -> Self { + self.updated_at = Some(updated_at); + self + } + + pub fn build(mut self, connection: &PgConnection) -> AppResult { + use diesel::{insert_into, select, update}; + + let mut krate = self + .krate + .create_or_update(connection, self.owner_id, None)?; + + // Since we are using `NewCrate`, we can't set all the + // crate properties in a single DB call. + + if let Some(downloads) = self.downloads { + krate = update(&krate) + .set(crates::downloads.eq(downloads)) + .returning(cargo_registry::models::krate::ALL_COLUMNS) + .get_result(connection)?; + } + + if self.versions.is_empty() { + self.versions.push(VersionBuilder::new("0.99.0")); + } + + let mut last_version_id = 0; + for version_builder in self.versions { + last_version_id = version_builder + .build(krate.id, self.owner_id, connection)? + .id; + } + + if let Some(downloads) = self.recent_downloads { + insert_into(version_downloads::table) + .values(( + version_downloads::version_id.eq(last_version_id), + version_downloads::downloads.eq(downloads), + )) + .execute(connection)?; + + no_arg_sql_function!(refresh_recent_crate_downloads, ()); + select(refresh_recent_crate_downloads).execute(connection)?; + } + + if !self.categories.is_empty() { + Category::update_crate(connection, &krate, &self.categories)?; + } + + if !self.keywords.is_empty() { + Keyword::update_crate(connection, &krate, &self.keywords)?; + } + + if let Some(updated_at) = self.updated_at { + krate = update(&krate) + .set(crates::updated_at.eq(updated_at)) + .returning(cargo_registry::models::krate::ALL_COLUMNS) + .get_result(connection)?; + } + + Ok(krate) + } + + /// Consumes the builder and creates the crate record in the database. + /// + /// # Panics + /// + /// Panics (and fails the test) if any part of inserting the crate record fails. + #[track_caller] + pub fn expect_build(self, connection: &PgConnection) -> Crate { + let name = self.krate.name; + self.build(connection).unwrap_or_else(|e| { + panic!("Unable to create crate {}: {:?}", name, e); + }) + } +} diff --git a/src/tests/builders/mod.rs b/src/tests/builders/mod.rs new file mode 100644 index 00000000000..dd5312be3d1 --- /dev/null +++ b/src/tests/builders/mod.rs @@ -0,0 +1,11 @@ +//! Structs using the builder pattern that make it easier to create records in tests. + +mod dependency; +mod krate; +mod publish; +mod version; + +pub use dependency::DependencyBuilder; +pub use krate::CrateBuilder; +pub use publish::PublishBuilder; +pub use version::VersionBuilder; diff --git a/src/tests/builders/publish.rs b/src/tests/builders/publish.rs new file mode 100644 index 00000000000..c9665ed3fd6 --- /dev/null +++ b/src/tests/builders/publish.rs @@ -0,0 +1,236 @@ +use cargo_registry::views::krate_publish as u; +use std::{collections::HashMap, io::Read}; + +use flate2::{write::GzEncoder, Compression}; + +use super::DependencyBuilder; + +lazy_static! { + // The bytes of an empty tarball is not an empty vector of bytes because of tarball headers. + // Unless files are added to a PublishBuilder, the `.crate` tarball that gets uploaded + // will be empty, so precompute the empty tarball bytes to use as a default. + static ref EMPTY_TARBALL_BYTES: Vec = { + let mut empty_tarball = vec![]; + { + let mut ar = + tar::Builder::new(GzEncoder::new(&mut empty_tarball, Compression::default())); + t!(ar.finish()); + } + empty_tarball + }; +} + +/// A builder for constructing a crate for the purposes of testing publishing. If you only need +/// a crate to exist and don't need to test behavior caused by the publish request, inserting +/// a crate into the database directly by using CrateBuilder will be faster. +pub struct PublishBuilder { + authors: Vec, + badges: HashMap>, + categories: Vec, + deps: Vec, + desc: Option, + doc_url: Option, + keywords: Vec, + pub krate_name: String, + license: Option, + license_file: Option, + readme: Option, + tarball: Vec, + version: semver::Version, +} + +impl PublishBuilder { + /// Create a request to publish a crate with the given name, version 1.0.0, and no files + /// in its tarball. + pub fn new(krate_name: &str) -> Self { + PublishBuilder { + authors: vec!["foo".to_string()], + badges: HashMap::new(), + categories: vec![], + deps: vec![], + desc: Some("description".to_string()), + doc_url: None, + keywords: vec![], + krate_name: krate_name.into(), + license: Some("MIT".to_string()), + license_file: None, + readme: None, + tarball: EMPTY_TARBALL_BYTES.to_vec(), + version: semver::Version::parse("1.0.0").unwrap(), + } + } + + /// Set the version of the crate being published to something other than the default of 1.0.0. + pub fn version(mut self, version: &str) -> Self { + self.version = semver::Version::parse(version).unwrap(); + self + } + + /// Set the files in the crate's tarball. + pub fn files(self, files: &[(&str, &[u8])]) -> Self { + let mut slices = files.iter().map(|p| p.1).collect::>(); + let mut files = files + .iter() + .zip(&mut slices) + .map(|(&(name, _), data)| { + let len = data.len() as u64; + (name, data as &mut dyn Read, len) + }) + .collect::>(); + + self.files_with_io(&mut files) + } + + /// Set the tarball from a Read trait object + pub fn files_with_io(mut self, files: &mut [(&str, &mut dyn Read, u64)]) -> Self { + let mut tarball = Vec::new(); + { + let mut ar = tar::Builder::new(GzEncoder::new(&mut tarball, Compression::default())); + for &mut (name, ref mut data, size) in files { + let mut header = tar::Header::new_gnu(); + t!(header.set_path(name)); + header.set_size(size); + header.set_cksum(); + t!(ar.append(&header, data)); + } + t!(ar.finish()); + } + + self.tarball = tarball; + self + } + + /// Set the tarball directly to the given Vec of bytes + pub fn tarball(mut self, tarball: Vec) -> Self { + self.tarball = tarball; + self + } + + /// Add a dependency to this crate. Make sure the dependency already exists in the + /// database or publish will fail. + pub fn dependency(mut self, dep: DependencyBuilder) -> Self { + self.deps.push(dep.build()); + self + } + + /// Set the description of this crate + pub fn description(mut self, description: &str) -> Self { + self.desc = Some(description.to_string()); + self + } + + /// Unset the description of this crate. Publish will fail unless description is reset. + pub fn unset_description(mut self) -> Self { + self.desc = None; + self + } + + /// Set the readme of this crate + pub fn readme(mut self, readme: &str) -> Self { + self.readme = Some(readme.to_string()); + self + } + + /// Set the documentation URL of this crate + pub fn documentation(mut self, documentation: &str) -> Self { + self.doc_url = Some(documentation.to_string()); + self + } + + /// Add a keyword to this crate. + pub fn keyword(mut self, keyword: &str) -> Self { + self.keywords.push(keyword.into()); + self + } + + /// Add a category to this crate. Make sure the category already exists in the + /// database or it will be ignored. + pub fn category(mut self, slug: &str) -> Self { + self.categories.push(slug.into()); + self + } + + /// Add badges to this crate. + pub fn badges(mut self, badges: HashMap>) -> Self { + self.badges = badges; + self + } + + /// Remove the license from this crate. Publish will fail unless license or license file is set. + pub fn unset_license(mut self) -> Self { + self.license = None; + self + } + + /// Set the license file for this crate + pub fn license_file(mut self, license_file: &str) -> Self { + self.license_file = Some(license_file.into()); + self + } + + /// Add an author to this crate + pub fn author(mut self, author: &str) -> Self { + self.authors.push(author.into()); + self + } + + /// Remove the authors from this crate. Publish will fail unless authors are reset. + pub fn unset_authors(mut self) -> Self { + self.authors = vec![]; + self + } + + /// Consume this builder to make the Put request body + pub fn body(self) -> Vec { + let new_crate = u::EncodableCrateUpload { + name: u::EncodableCrateName(self.krate_name.clone()), + vers: u::EncodableCrateVersion(self.version), + features: HashMap::new(), + deps: self.deps, + authors: self.authors, + description: self.desc, + homepage: None, + documentation: self.doc_url, + readme: self.readme, + readme_file: None, + keywords: u::EncodableKeywordList( + self.keywords.into_iter().map(u::EncodableKeyword).collect(), + ), + categories: u::EncodableCategoryList( + self.categories + .into_iter() + .map(u::EncodableCategory) + .collect(), + ), + license: self.license, + license_file: self.license_file, + repository: None, + badges: Some(self.badges), + links: None, + }; + + let json = serde_json::to_string(&new_crate).unwrap(); + let mut body = Vec::new(); + body.extend( + [ + json.len() as u8, + (json.len() >> 8) as u8, + (json.len() >> 16) as u8, + (json.len() >> 24) as u8, + ] + .iter() + .cloned(), + ); + body.extend(json.as_bytes().iter().cloned()); + + let tarball = &self.tarball; + body.extend(&[ + tarball.len() as u8, + (tarball.len() >> 8) as u8, + (tarball.len() >> 16) as u8, + (tarball.len() >> 24) as u8, + ]); + body.extend(tarball); + body + } +} diff --git a/src/tests/builders/version.rs b/src/tests/builders/version.rs new file mode 100644 index 00000000000..6a97dfbfad4 --- /dev/null +++ b/src/tests/builders/version.rs @@ -0,0 +1,157 @@ +use cargo_registry::{ + models::{Crate, NewVersion, Version}, + schema::{dependencies, versions}, + util::errors::AppResult, +}; +use std::collections::HashMap; + +use chrono::NaiveDateTime; +use diesel::prelude::*; + +/// A builder to create version records for the purpose of inserting directly into the database. +pub struct VersionBuilder<'a> { + created_at: Option, + dependencies: Vec<(i32, Option<&'static str>)>, + features: HashMap>, + license: Option<&'a str>, + license_file: Option<&'a str>, + num: semver::Version, + size: i32, + yanked: bool, +} + +impl<'a> VersionBuilder<'a> { + /// Creates a VersionBuilder from a string slice `num` representing the version's number. + /// + /// # Panics + /// + /// Panics if `num` cannot be parsed as a valid `semver::Version`. + #[track_caller] + pub fn new(num: &str) -> Self { + let num = semver::Version::parse(num).unwrap_or_else(|e| { + panic!("The version {} is not valid: {}", num, e); + }); + + VersionBuilder { + created_at: None, + dependencies: Vec::new(), + features: HashMap::new(), + license: None, + license_file: None, + num, + size: 0, + yanked: false, + } + } + + /// Sets the version's `created_at` value. + pub fn created_at(mut self, created_at: NaiveDateTime) -> Self { + self.created_at = Some(created_at); + self + } + + /// Sets the version's `license` value. + pub fn license(mut self, license: Option<&'a str>) -> Self { + self.license = license; + self + } + + /// Adds a dependency to this version. + pub fn dependency(mut self, dependency: &Crate, target: Option<&'static str>) -> Self { + self.dependencies.push((dependency.id, target)); + self + } + + /// Sets the version's `yanked` value. + pub fn yanked(self, yanked: bool) -> Self { + Self { yanked, ..self } + } + + /// Sets the version's size. + pub fn size(mut self, size: i32) -> Self { + self.size = size; + self + } + + pub fn build( + self, + crate_id: i32, + published_by: i32, + connection: &PgConnection, + ) -> AppResult { + use diesel::{insert_into, update}; + + let license = match self.license { + Some(license) => Some(license.to_owned()), + None => None, + }; + + let mut vers = NewVersion::new( + crate_id, + &self.num, + &self.features, + license, + self.license_file, + self.size, + published_by, + )? + .save(connection, &[], "someone@example.com")?; + + if self.yanked { + vers = update(&vers) + .set(versions::yanked.eq(true)) + .get_result(connection)?; + } + + if let Some(created_at) = self.created_at { + vers = update(&vers) + .set(versions::created_at.eq(created_at)) + .get_result(connection)?; + } + + let new_deps = self + .dependencies + .into_iter() + .map(|(crate_id, target)| { + ( + dependencies::version_id.eq(vers.id), + dependencies::req.eq(">= 0"), + dependencies::crate_id.eq(crate_id), + dependencies::target.eq(target), + dependencies::optional.eq(false), + dependencies::default_features.eq(false), + dependencies::features.eq(Vec::::new()), + ) + }) + .collect::>(); + insert_into(dependencies::table) + .values(&new_deps) + .execute(connection)?; + + Ok(vers) + } + + /// Consumes the builder and creates the version record in the database. + /// + /// # Panics + /// + /// Panics (and fails the test) if any part of inserting the version record fails. + #[track_caller] + pub fn expect_build( + self, + crate_id: i32, + published_by: i32, + connection: &PgConnection, + ) -> Version { + self.build(crate_id, published_by, connection) + .unwrap_or_else(|e| { + panic!("Unable to create version: {:?}", e); + }) + } +} + +impl<'a> From<&'a str> for VersionBuilder<'a> { + fn from(num: &'a str) -> Self { + VersionBuilder::new(num) + } +} diff --git a/src/tests/krate.rs b/src/tests/krate.rs index 516b901dd5f..4e2151ade9b 100644 --- a/src/tests/krate.rs +++ b/src/tests/krate.rs @@ -1338,29 +1338,43 @@ fn summary_new_crates() { let (app, anon, user) = TestApp::init().with_user(); let user = user.as_model(); app.db(|conn| { - let krate = CrateBuilder::new("some_downloads", user.id) + 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); - let krate2 = CrateBuilder::new("most_recent_downloads", user.id) + 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); - let krate3 = CrateBuilder::new("just_updated", user.id) + 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); - let krate4 = CrateBuilder::new("just_updated_patch", user.id) + 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) @@ -1369,38 +1383,11 @@ fn summary_new_crates() { .downloads(1000) .expect_build(conn); - new_category("Category 1", "cat1", "Category 1 crates") - .create_or_update(conn) - .unwrap(); - Category::update_crate(conn, &krate, &["cat1"]).unwrap(); - Category::update_crate(conn, &krate2, &["cat1"]).unwrap(); - // set total_downloads global value for `num_downloads` prop update(metadata::table) .set(metadata::total_downloads.eq(6000)) .execute(&*conn) .unwrap(); - - // update 'just_updated' krate. Others won't appear because updated_at == created_at. - let updated = Utc::now().naive_utc(); - update(&krate3) - .set(crates::updated_at.eq(updated)) - .execute(&*conn) - .unwrap(); - - let plus_two = Utc::now().naive_utc() + chrono::Duration::seconds(2); - let newer = VersionBuilder::new("0.1.1").expect_build(krate4.id, user.id, conn); - - // Update the patch version to be newer than the other versions, including the higher one. - update(&newer) - .set(versions::created_at.eq(plus_two)) - .execute(&*conn) - .unwrap(); - - update(&krate4) - .set(crates::updated_at.eq(plus_two)) - .execute(&*conn) - .unwrap(); }); let json: SummaryResponse = anon.get("/api/v1/summary").good();