diff --git a/Cargo.lock b/Cargo.lock index b253a8cf7cd..72504dbc1e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -489,6 +489,7 @@ dependencies = [ "openssl 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)", "phf 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)", "postgres-protocol 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/Cargo.toml b/Cargo.toml index 0ce7a391666..cf656ef8cef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ git2 = "0.6" flate2 = "0.2" semver = "0.5" url = "1.2.1" -postgres = { version = "0.13", features = ["with-time", "with-openssl"] } +postgres = { version = "0.13", features = ["with-time", "with-openssl", "with-rustc-serialize"] } r2d2 = "0.7.0" r2d2_postgres = "0.11" openssl = "0.9" diff --git a/app/components/badge-appveyor.js b/app/components/badge-appveyor.js new file mode 100644 index 00000000000..fddf752fcd3 --- /dev/null +++ b/app/components/badge-appveyor.js @@ -0,0 +1,16 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + tagName: 'span', + classNames: ['badge'], + repository: Ember.computed.alias('badge.attributes.repository'), + branch: Ember.computed('badge.attributes.branch', function() { + return this.get('badge.attributes.branch') || 'master'; + }), + service: Ember.computed('badge.attributes.service', function() { + return this.get('badge.attributes.service') || 'github'; + }), + text: Ember.computed('badge', function() { + return `Appveyor build status for the ${ this.get('branch') } branch`; + }) +}); diff --git a/app/components/badge-travis-ci.js b/app/components/badge-travis-ci.js new file mode 100644 index 00000000000..774d1a29afe --- /dev/null +++ b/app/components/badge-travis-ci.js @@ -0,0 +1,13 @@ +import Ember from 'ember'; + +export default Ember.Component.extend({ + tagName: 'span', + classNames: ['badge'], + repository: Ember.computed.alias('badge.attributes.repository'), + branch: Ember.computed('badge.attributes.branch', function() { + return this.get('badge.attributes.branch') || 'master'; + }), + text: Ember.computed('branch', function() { + return `Travis CI build status for the ${ this.get('branch') } branch`; + }) +}); diff --git a/app/controllers/crate/version.js b/app/controllers/crate/version.js index 99216589ed0..4dd6e0e81a7 100644 --- a/app/controllers/crate/version.js +++ b/app/controllers/crate/version.js @@ -20,6 +20,7 @@ export default Ember.Controller.extend({ requestedVersion: null, keywords: computed.alias('crate.keywords'), categories: computed.alias('crate.categories'), + badges: computed.alias('crate.badges'), sortedVersions: computed.readOnly('crate.versions'), diff --git a/app/mirage/fixtures/search.js b/app/mirage/fixtures/search.js index 38889e3a683..1a9b0826927 100644 --- a/app/mirage/fixtures/search.js +++ b/app/mirage/fixtures/search.js @@ -19,6 +19,21 @@ export default { "name": "rust_mixin", "repository": "https://github.com/huonw/external_mixin", "updated_at": "2015-02-27T11:52:13Z", + "badges": [ + { + "attributes": { + "repository": "huonw/external_mixin" + }, + "badge_type": "appveyor" + }, + { + "attributes": { + "branch": "master", + "repository": "huonw/external_mixin" + }, + "badge_type": "travis-ci" + } + ], "versions": null }, { "created_at": "2015-02-27T11:51:58Z", diff --git a/app/models/crate.js b/app/models/crate.js index 9065b323f7e..e5c07f40af8 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -1,4 +1,5 @@ import DS from 'ember-data'; +import Ember from 'ember'; export default DS.Model.extend({ name: DS.attr('string'), @@ -17,6 +18,16 @@ export default DS.Model.extend({ license: DS.attr('string'), versions: DS.hasMany('versions', { async: true }), + badges: DS.attr(), + enhanced_badges: Ember.computed.map('badges', badge => ({ + // jshint ignore:start + // needed until https://github.com/jshint/jshint/issues/2991 is fixed + ...badge, + // jshint ignore:end + component_name: `badge-${badge.badge_type}` + })), + badge_sort: ['badge_type'], + annotated_badges: Ember.computed.sort('enhanced_badges', 'badge_sort'), owners: DS.hasMany('users', { async: true }), version_downloads: DS.hasMany('version-download', { async: true }), keywords: DS.hasMany('keywords', { async: true }), diff --git a/app/styles/crate.scss b/app/styles/crate.scss index 6bc03d4eb51..05e2b453280 100644 --- a/app/styles/crate.scss +++ b/app/styles/crate.scss @@ -119,9 +119,6 @@ } .vers { margin-left: 10px; - img { - margin-bottom: -4px; - } } .stats { diff --git a/app/templates/components/badge-appveyor.hbs b/app/templates/components/badge-appveyor.hbs new file mode 100644 index 00000000000..58495f33960 --- /dev/null +++ b/app/templates/components/badge-appveyor.hbs @@ -0,0 +1,6 @@ + + {{ text }} + diff --git a/app/templates/components/badge-travis-ci.hbs b/app/templates/components/badge-travis-ci.hbs new file mode 100644 index 00000000000..05985aa3421 --- /dev/null +++ b/app/templates/components/badge-travis-ci.hbs @@ -0,0 +1,6 @@ + + {{ text }} + diff --git a/app/templates/components/crate-row.hbs b/app/templates/components/crate-row.hbs index 1c8bfb07529..80c27e3e65e 100644 --- a/app/templates/components/crate-row.hbs +++ b/app/templates/components/crate-row.hbs @@ -7,6 +7,9 @@ alt="{{ crate.max_version }}" title="{{ crate.name }}’s current version badge" /> + {{#each crate.annotated_badges as |badge|}} + {{component badge.component_name badge=badge}} + {{/each}}
diff --git a/app/templates/crate/version.hbs b/app/templates/crate/version.hbs index 419f52d5a80..30dccfaf25f 100644 --- a/app/templates/crate/version.hbs +++ b/app/templates/crate/version.hbs @@ -71,6 +71,12 @@ alt="{{ crate.name }}’s current version badge" title="{{ crate.name }}’s current version badge" />

+ + {{#each crate.annotated_badges as |badge|}} +

+ {{component badge.component_name badge=badge}} +

+ {{/each}}
diff --git a/src/badge.rs b/src/badge.rs new file mode 100644 index 00000000000..7a94847d9ed --- /dev/null +++ b/src/badge.rs @@ -0,0 +1,194 @@ +use util::CargoResult; +use krate::Crate; +use Model; + +use std::collections::HashMap; +use pg::GenericConnection; +use pg::rows::Row; +use rustc_serialize::json::Json; + +#[derive(Debug, PartialEq, Clone)] +pub enum Badge { + TravisCi { + repository: String, branch: Option, + }, + Appveyor { + repository: String, branch: Option, service: Option, + }, +} + +#[derive(RustcEncodable, RustcDecodable, PartialEq, Debug)] +pub struct EncodableBadge { + pub badge_type: String, + pub attributes: HashMap, +} + +impl Model for Badge { + fn from_row(row: &Row) -> Badge { + let attributes: Json = row.get("attributes"); + if let Json::Object(attributes) = attributes { + let badge_type: String = row.get("badge_type"); + match badge_type.as_str() { + "travis-ci" => { + Badge::TravisCi { + branch: attributes.get("branch") + .and_then(Json::as_string) + .map(str::to_string), + repository: attributes.get("repository") + .and_then(Json::as_string) + .map(str::to_string) + .expect("Invalid TravisCi badge \ + without repository in the \ + database"), + } + }, + "appveyor" => { + Badge::Appveyor { + service: attributes.get("service") + .and_then(Json::as_string) + .map(str::to_string), + branch: attributes.get("branch") + .and_then(Json::as_string) + .map(str::to_string), + repository: attributes.get("repository") + .and_then(Json::as_string) + .map(str::to_string) + .expect("Invalid Appveyor badge \ + without repository in the \ + database"), + } + }, + _ => { + panic!("Unknown badge type {} in the database", badge_type); + }, + } + } else { + panic!( + "badge attributes {:?} in the database was not a JSON object", + attributes + ); + } + } + fn table_name(_: Option) -> &'static str { "badges" } +} + +impl Badge { + pub fn encodable(self) -> EncodableBadge { + EncodableBadge { + badge_type: self.badge_type().to_string(), + attributes: self.attributes(), + } + } + + pub fn badge_type(&self) -> &'static str { + match *self { + Badge::TravisCi {..} => "travis-ci", + Badge::Appveyor {..} => "appveyor", + } + } + + pub fn json_attributes(self) -> Json { + Json::Object(self.attributes().into_iter().map(|(k, v)| { + (k, Json::String(v)) + }).collect()) + } + + fn attributes(self) -> HashMap { + let mut attributes = HashMap::new(); + + match self { + Badge::TravisCi { branch, repository } => { + attributes.insert(String::from("repository"), repository); + if let Some(branch) = branch { + attributes.insert( + String::from("branch"), + branch + ); + } + }, + Badge::Appveyor { service, branch, repository } => { + attributes.insert(String::from("repository"), repository); + if let Some(branch) = branch { + attributes.insert( + String::from("branch"), + branch + ); + } + if let Some(service) = service { + attributes.insert( + String::from("service"), + service + ); + } + } + } + + attributes + } + + fn from_attributes(badge_type: &str, + attributes: &HashMap) + -> Result { + match badge_type { + "travis-ci" => { + match attributes.get("repository") { + Some(repository) => { + Ok(Badge::TravisCi { + repository: repository.to_string(), + branch: attributes.get("branch") + .map(String::to_string), + }) + }, + None => Err(badge_type.to_string()), + } + }, + "appveyor" => { + match attributes.get("repository") { + Some(repository) => { + Ok(Badge::Appveyor { + repository: repository.to_string(), + branch: attributes.get("branch") + .map(String::to_string), + service: attributes.get("service") + .map(String::to_string), + + }) + }, + None => Err(badge_type.to_string()), + } + }, + _ => Err(badge_type.to_string()), + } + } + + pub fn update_crate(conn: &GenericConnection, + krate: &Crate, + badges: HashMap>) + -> CargoResult> { + + let mut invalid_badges = vec![]; + + let badges: Vec<_> = badges.iter().filter_map(|(k, v)| { + Badge::from_attributes(k, v).map_err(|invalid_badge| { + invalid_badges.push(invalid_badge) + }).ok() + }).collect(); + + conn.execute("\ + DELETE FROM badges \ + WHERE crate_id = $1;", + &[&krate.id] + )?; + + for badge in badges { + conn.execute("\ + INSERT INTO badges (crate_id, badge_type, attributes) \ + VALUES ($1, $2, $3) \ + ON CONFLICT (crate_id, badge_type) DO UPDATE \ + SET attributes = EXCLUDED.attributes;", + &[&krate.id, &badge.badge_type(), &badge.json_attributes()] + )?; + } + Ok(invalid_badges) + } +} diff --git a/src/bin/migrate.rs b/src/bin/migrate.rs index 9d1cb86db67..ce635d3db5a 100644 --- a/src/bin/migrate.rs +++ b/src/bin/migrate.rs @@ -819,6 +819,18 @@ fn migrations() -> Vec { ON crates_categories;")); Ok(()) }), + Migration::add_table(20170102131034, "badges", " \ + crate_id INTEGER NOT NULL, \ + badge_type VARCHAR NOT NULL, \ + attributes JSONB NOT NULL"), + Migration::new(20170102145236, |tx| { + try!(tx.execute("CREATE UNIQUE INDEX badges_crate_type \ + ON badges (crate_id, badge_type)", &[])); + Ok(()) + }, |tx| { + try!(tx.execute("DROP INDEX badges_crate_type", &[])); + Ok(()) + }), ]; // NOTE: Generate a new id via `date +"%Y%m%d%H%M%S"` diff --git a/src/krate.rs b/src/krate.rs index 0a3247b04d9..1fc5a486a79 100644 --- a/src/krate.rs +++ b/src/krate.rs @@ -20,7 +20,7 @@ use semver; use time::{Timespec, Duration}; use url::Url; -use {Model, User, Keyword, Version, Category}; +use {Model, User, Keyword, Version, Category, Badge}; use app::{App, RequestApp}; use db::RequestTransaction; use dependency::{Dependency, EncodableDependency}; @@ -28,6 +28,7 @@ use download::{VersionDownload, EncodableVersionDownload}; use git; use keyword::EncodableKeyword; use category::EncodableCategory; +use badge::EncodableBadge; use upload; use user::RequestUser; use owner::{EncodableOwner, Owner, Rights, OwnerKind, Team, rights}; @@ -61,6 +62,7 @@ pub struct EncodableCrate { pub versions: Option>, pub keywords: Option>, pub categories: Option>, + pub badges: Option>, pub created_at: String, pub downloads: i32, pub max_version: String, @@ -229,14 +231,16 @@ impl Crate { parts.next().is_none() } - pub fn minimal_encodable(self) -> EncodableCrate { - self.encodable(None, None, None) + pub fn minimal_encodable(self, + badges: Option>) -> EncodableCrate { + self.encodable(None, None, None, badges) } pub fn encodable(self, versions: Option>, keywords: Option<&[Keyword]>, - categories: Option<&[Category]>) + categories: Option<&[Category]>, + badges: Option>) -> EncodableCrate { let Crate { name, created_at, updated_at, downloads, max_version, description, @@ -249,6 +253,9 @@ impl Crate { }; let keyword_ids = keywords.map(|kws| kws.iter().map(|kw| kw.keyword.clone()).collect()); let category_ids = categories.map(|cats| cats.iter().map(|cat| cat.slug.clone()).collect()); + let badges = badges.map(|bs| { + bs.iter().map(|b| b.clone().encodable()).collect() + }); EncodableCrate { id: name.clone(), name: name.clone(), @@ -258,6 +265,7 @@ impl Crate { versions: versions, keywords: keyword_ids, categories: category_ids, + badges: badges, max_version: max_version.to_string(), documentation: documentation, homepage: homepage, @@ -405,6 +413,13 @@ impl Crate { Ok(rows.iter().map(|r| Model::from_row(&r)).collect()) } + pub fn badges(&self, conn: &GenericConnection) -> CargoResult> { + let stmt = try!(conn.prepare("SELECT badges.* from badges \ + WHERE badges.crate_id = $1")); + let rows = try!(stmt.query(&[&self.id])); + Ok(rows.iter().map(|r| Model::from_row(&r)).collect()) + } + /// Returns (dependency, dependent crate name) pub fn reverse_dependencies(&self, conn: &GenericConnection, @@ -587,7 +602,8 @@ pub fn index(req: &mut Request) -> CargoResult { let mut crates = Vec::new(); for row in try!(stmt.query(&args)).iter() { let krate: Crate = Model::from_row(&row); - crates.push(krate.minimal_encodable()); + let badges = krate.badges(conn); + crates.push(krate.minimal_encodable(badges.ok())); } // Query for the total count of crates @@ -622,7 +638,7 @@ pub fn summary(req: &mut Request) -> CargoResult { let rows = try!(stmt.query(&[])); Ok(rows.iter().map(|r| { let krate: Crate = Model::from_row(&r); - krate.minimal_encodable() + krate.minimal_encodable(None) }).collect::>()) }; let new_crates = try!(tx.prepare("SELECT * FROM crates \ @@ -660,6 +676,7 @@ pub fn show(req: &mut Request) -> CargoResult { let ids = versions.iter().map(|v| v.id).collect(); let kws = try!(krate.keywords(conn)); let cats = try!(krate.categories(conn)); + let badges = try!(krate.badges(conn)); #[derive(RustcEncodable)] struct R { @@ -669,7 +686,9 @@ pub fn show(req: &mut Request) -> CargoResult { categories: Vec, } Ok(req.json(&R { - krate: krate.clone().encodable(Some(ids), Some(&kws), Some(&cats)), + krate: krate.clone().encodable( + Some(ids), Some(&kws), Some(&cats), Some(badges) + ), versions: versions.into_iter().map(|v| { v.encodable(&krate.name) }).collect(), @@ -746,6 +765,16 @@ pub fn new(req: &mut Request) -> CargoResult { Category::update_crate(try!(req.tx()), &krate, &categories) ); + // Update all badges for this crate, collecting any invalid badges in + // order to be able to warn about them + let ignored_invalid_badges = try!( + Badge::update_crate( + try!(req.tx()), + &krate, + new_crate.badges.unwrap_or_else(HashMap::new) + ) + ); + // Upload the crate to S3 let mut handle = req.app().handle(); let path = krate.s3_path(&vers.to_string()); @@ -803,14 +832,21 @@ pub fn new(req: &mut Request) -> CargoResult { bomb.path = None; #[derive(RustcEncodable)] - struct Warnings { invalid_categories: Vec } + struct Warnings { + invalid_categories: Vec, + invalid_badges: Vec, + } let warnings = Warnings { invalid_categories: ignored_invalid_categories, + invalid_badges: ignored_invalid_badges, }; #[derive(RustcEncodable)] struct R { krate: EncodableCrate, warnings: Warnings } - Ok(req.json(&R { krate: krate.minimal_encodable(), warnings: warnings })) + Ok(req.json(&R { + krate: krate.minimal_encodable(None), + warnings: warnings + })) } fn parse_new_headers(req: &mut Request) -> CargoResult<(upload::NewCrate, User)> { diff --git a/src/lib.rs b/src/lib.rs index 550cb7d88a3..f3efe3fedb2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,7 @@ extern crate conduit_router; extern crate conduit_static; pub use app::App; +pub use self::badge::Badge; pub use self::category::Category; pub use config::Config; pub use self::dependency::Dependency; @@ -53,6 +54,7 @@ use conduit_middleware::MiddlewareBuilder; use util::{C, R, R404}; pub mod app; +pub mod badge; pub mod categories; pub mod category; pub mod config; diff --git a/src/tests/all.rs b/src/tests/all.rs index 4f288adc969..a008de37d67 100755 --- a/src/tests/all.rs +++ b/src/tests/all.rs @@ -63,6 +63,7 @@ struct Error { detail: String } #[derive(RustcDecodable)] struct Bad { errors: Vec } +mod badge; mod category; mod git; mod keyword; @@ -272,30 +273,49 @@ fn new_req(app: Arc, krate: &str, version: &str) -> MockRequest { fn new_req_full(app: Arc, krate: Crate, version: &str, deps: Vec) -> MockRequest { let mut req = ::req(app, Method::Put, "/api/v1/crates/new"); - req.with_body(&new_req_body(krate, version, deps, Vec::new(), Vec::new())); + req.with_body(&new_req_body( + krate, version, deps, Vec::new(), Vec::new(), HashMap::new() + )); return req; } fn new_req_with_keywords(app: Arc, krate: Crate, version: &str, kws: Vec) -> MockRequest { let mut req = ::req(app, Method::Put, "/api/v1/crates/new"); - req.with_body(&new_req_body(krate, version, Vec::new(), kws, Vec::new())); + req.with_body(&new_req_body( + krate, version, Vec::new(), kws, Vec::new(), HashMap::new() + )); return req; } fn new_req_with_categories(app: Arc, krate: Crate, version: &str, cats: Vec) -> MockRequest { let mut req = ::req(app, Method::Put, "/api/v1/crates/new"); - req.with_body(&new_req_body(krate, version, Vec::new(), Vec::new(), cats)); + req.with_body(&new_req_body( + krate, version, Vec::new(), Vec::new(), cats, HashMap::new() + )); + return req; +} + +fn new_req_with_badges(app: Arc, krate: Crate, version: &str, + badges: HashMap>) + -> MockRequest { + let mut req = ::req(app, Method::Put, "/api/v1/crates/new"); + req.with_body(&new_req_body( + krate, version, Vec::new(), Vec::new(), Vec::new(), badges + )); return req; } fn new_req_body_version_2(krate: Crate) -> Vec { - new_req_body(krate, "2.0.0", Vec::new(), Vec::new(), Vec::new()) + new_req_body( + krate, "2.0.0", Vec::new(), Vec::new(), Vec::new(), HashMap::new() + ) } fn new_req_body(krate: Crate, version: &str, deps: Vec, - kws: Vec, cats: Vec) -> Vec { + kws: Vec, cats: Vec, + badges: HashMap>) -> Vec { let kws = kws.into_iter().map(u::Keyword).collect(); let cats = cats.into_iter().map(u::Category).collect(); new_crate_to_body(&u::NewCrate { @@ -313,6 +333,7 @@ fn new_req_body(krate: Crate, version: &str, deps: Vec, license: Some("MIT".to_string()), license_file: None, repository: krate.repository, + badges: Some(badges), }, &[]) } diff --git a/src/tests/badge.rs b/src/tests/badge.rs new file mode 100644 index 00000000000..43c5618b989 --- /dev/null +++ b/src/tests/badge.rs @@ -0,0 +1,154 @@ +use conduit::{Request, Method}; +use postgres::GenericConnection; + +use cargo_registry::db::RequestTransaction; +use cargo_registry::badge::Badge; + +use std::collections::HashMap; + +fn tx(req: &Request) -> &GenericConnection { req.tx().unwrap() } + +#[test] +fn update_crate() { + let (_b, app, _middle) = ::app(); + let mut req = ::req(app, Method::Get, "/api/v1/crates/badged_crate"); + + ::mock_user(&mut req, ::user("foo")); + let (krate, _) = ::mock_crate(&mut req, ::krate("badged_crate")); + + let appveyor = Badge::Appveyor { + service: Some(String::from("github")), + branch: None, + repository: String::from("rust-lang/cargo"), + }; + let mut badge_attributes_appveyor = HashMap::new(); + badge_attributes_appveyor.insert( + String::from("service"), + String::from("github") + ); + badge_attributes_appveyor.insert( + String::from("repository"), + String::from("rust-lang/cargo") + ); + + let travis_ci = Badge::TravisCi { + branch: Some(String::from("beta")), + repository: String::from("rust-lang/rust"), + }; + let mut badge_attributes_travis_ci = HashMap::new(); + badge_attributes_travis_ci.insert( + String::from("branch"), + String::from("beta") + ); + badge_attributes_travis_ci.insert( + String::from("repository"), + String::from("rust-lang/rust") + ); + + let mut badges = HashMap::new(); + + // Updating with no badges has no effect + Badge::update_crate(tx(&req), &krate, badges.clone()).unwrap(); + assert_eq!(krate.badges(tx(&req)).unwrap(), vec![]); + + // Happy path adding one badge + badges.insert( + String::from("appveyor"), + badge_attributes_appveyor.clone() + ); + Badge::update_crate(tx(&req), &krate, badges.clone()).unwrap(); + assert_eq!(krate.badges(tx(&req)).unwrap(), vec![appveyor.clone()]); + + // Replacing one badge with another + badges.clear(); + badges.insert( + String::from("travis-ci"), + badge_attributes_travis_ci.clone() + ); + Badge::update_crate(tx(&req), &krate, badges.clone()).unwrap(); + assert_eq!(krate.badges(tx(&req)).unwrap(), vec![travis_ci.clone()]); + + // Updating badge attributes + let travis_ci2 = Badge::TravisCi { + branch: None, + repository: String::from("rust-lang/rust"), + }; + let mut badge_attributes_travis_ci2 = HashMap::new(); + badge_attributes_travis_ci2.insert( + String::from("repository"), + String::from("rust-lang/rust") + ); + badges.insert( + String::from("travis-ci"), + badge_attributes_travis_ci2.clone() + ); + Badge::update_crate(tx(&req), &krate, badges.clone()).unwrap(); + assert_eq!(krate.badges(tx(&req)).unwrap(), vec![travis_ci2.clone()]); + + // Removing one badge + badges.clear(); + Badge::update_crate(tx(&req), &krate, badges.clone()).unwrap(); + assert_eq!(krate.badges(tx(&req)).unwrap(), vec![]); + + // Adding 2 badges + badges.insert( + String::from("appveyor"), + badge_attributes_appveyor.clone() + ); + badges.insert( + String::from("travis-ci"), + badge_attributes_travis_ci.clone() + ); + Badge::update_crate( + tx(&req), &krate, badges.clone() + ).unwrap(); + + let current_badges = krate.badges(tx(&req)).unwrap(); + assert_eq!(current_badges.len(), 2); + assert!(current_badges.contains(&appveyor)); + assert!(current_badges.contains(&travis_ci)); + + // Removing all badges + badges.clear(); + Badge::update_crate(tx(&req), &krate, badges.clone()).unwrap(); + assert_eq!(krate.badges(tx(&req)).unwrap(), vec![]); + + // Attempting to add one valid badge (appveyor) and two invalid badges + // (travis-ci without a required attribute and an unknown badge type) + + // Extra invalid keys are fine, we'll just ignore those + badge_attributes_appveyor.insert( + String::from("extra"), + String::from("info") + ); + badges.insert( + String::from("appveyor"), + badge_attributes_appveyor.clone() + ); + + // Repository is a required key + badge_attributes_travis_ci.remove("repository"); + badges.insert( + String::from("travis-ci"), + badge_attributes_travis_ci.clone() + ); + + // This is not a badge that crates.io knows about + let mut invalid_attributes = HashMap::new(); + invalid_attributes.insert( + String::from("not-a-badge-attribute"), + String::from("not-a-badge-value") + ); + badges.insert( + String::from("not-a-badge"), + invalid_attributes.clone() + ); + + let invalid_badges = Badge::update_crate( + tx(&req), &krate, badges.clone() + ).unwrap(); + assert_eq!(invalid_badges.len(), 2); + assert!(invalid_badges.contains(&String::from("travis-ci"))); + assert!(invalid_badges.contains(&String::from("not-a-badge"))); + assert_eq!(krate.badges(tx(&req)).unwrap(), vec![appveyor.clone()]); +} diff --git a/src/tests/http-data/krate_good_badges b/src/tests/http-data/krate_good_badges new file mode 100644 index 00000000000..d0871b53db2 --- /dev/null +++ b/src/tests/http-data/krate_good_badges @@ -0,0 +1,21 @@ +===REQUEST 343 +PUT http://alexcrichton-test.s3.amazonaws.com/crates/foobadger/foobadger-1.0.0.crate HTTP/1.1 +Accept: */* +Proxy-Connection: Keep-Alive +Authorization: AWS AKIAJF3GEK7N44BACDZA:GDxGb6r3SIqo9wXuzHrgMNWekwk= +Content-Length: 0 +Host: alexcrichton-test.s3.amazonaws.com +Content-Type: application/x-tar +Date: Sun, 28 Jun 2015 14:07:17 -0700 + + +===RESPONSE 258 +HTTP/1.1 200 +x-amz-request-id: CB0E925D8E3AB3E8 +x-amz-id-2: SiaMwszM1p2TzXlLauvZ6kRKcUCg7HoyBW29vts42w9ArrLwkJWl8vuvPuGFkpM6XGH+YXN852g= +date: Sun, 28 Jun 2015 21:07:51 GMT +etag: "d41d8cd98f00b204e9800998ecf8427e" +content-length: 0 +server: AmazonS3 + + diff --git a/src/tests/http-data/krate_ignored_badges b/src/tests/http-data/krate_ignored_badges new file mode 100644 index 00000000000..425ca782969 --- /dev/null +++ b/src/tests/http-data/krate_ignored_badges @@ -0,0 +1,21 @@ +===REQUEST 359 +PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo_ignored_badge/foo_ignored_badge-1.0.0.crate HTTP/1.1 +Accept: */* +Proxy-Connection: Keep-Alive +Authorization: AWS AKIAJF3GEK7N44BACDZA:GDxGb6r3SIqo9wXuzHrgMNWekwk= +Content-Length: 0 +Host: alexcrichton-test.s3.amazonaws.com +Content-Type: application/x-tar +Date: Sun, 28 Jun 2015 14:07:17 -0700 + + +===RESPONSE 258 +HTTP/1.1 200 +x-amz-request-id: CB0E925D8E3AB3E8 +x-amz-id-2: SiaMwszM1p2TzXlLauvZ6kRKcUCg7HoyBW29vts42w9ArrLwkJWl8vuvPuGFkpM6XGH+YXN852g= +date: Sun, 28 Jun 2015 21:07:51 GMT +etag: "d41d8cd98f00b204e9800998ecf8427e" +content-length: 0 +server: AmazonS3 + + diff --git a/src/tests/krate.rs b/src/tests/krate.rs index 92b1971a854..752d4e7cb7f 100644 --- a/src/tests/krate.rs +++ b/src/tests/krate.rs @@ -3,6 +3,7 @@ use std::io::prelude::*; use std::fs::{self, File}; use conduit::{Handler, Request, Method}; + use git2; use postgres::GenericConnection; use rustc_serialize::json; @@ -26,7 +27,7 @@ struct CrateMeta { total: i32 } #[derive(RustcDecodable)] struct GitCrate { name: String, vers: String, deps: Vec, cksum: String } #[derive(RustcDecodable)] -struct Warnings { invalid_categories: Vec } +struct Warnings { invalid_categories: Vec, invalid_badges: Vec } #[derive(RustcDecodable)] struct GoodCrate { krate: EncodableCrate, warnings: Warnings } #[derive(RustcDecodable)] @@ -54,6 +55,7 @@ fn new_crate(name: &str) -> u::NewCrate { license: Some("MIT".to_string()), license_file: None, repository: None, + badges: None, } } @@ -915,6 +917,86 @@ fn ignored_categories() { assert_eq!(json.warnings.invalid_categories, vec!["bar".to_string()]); } +#[test] +fn good_badges() { + let krate = ::krate("foobadger"); + 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 (_b, app, middle) = ::app(); + let mut req = ::new_req_with_badges(app, krate.clone(), "1.0.0", badges); + + ::mock_user(&mut req, ::user("foo")); + let mut response = ok_resp!(middle.call(&mut req)); + + let json: GoodCrate = ::json(&mut response); + assert_eq!(json.krate.name, "foobadger"); + assert_eq!(json.krate.max_version, "1.0.0"); + + let mut response = ok_resp!( + middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/foobadger"))); + + let json: CrateResponse = ::json(&mut response); + + let badges = json.krate.badges.unwrap(); + assert_eq!(badges.len(), 1); + assert_eq!(badges[0].badge_type, "travis-ci"); + assert_eq!( + badges[0].attributes.get("repository").unwrap(), + &String::from("rust-lang/crates.io") + ); +} + +#[test] +fn ignored_badges() { + let krate = ::krate("foo_ignored_badge"); + 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 (_b, app, middle) = ::app(); + let mut req = ::new_req_with_badges(app, krate.clone(), "1.0.0", badges); + + ::mock_user(&mut req, ::user("foo")); + let mut response = ok_resp!(middle.call(&mut req)); + + let json: GoodCrate = ::json(&mut response); + 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 mut response = ok_resp!( + middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/foo_ignored_badge"))); + + let json: CrateResponse = ::json(&mut response); + + let badges = json.krate.badges.unwrap(); + assert_eq!(badges.len(), 0); +} + #[test] fn reverse_dependencies() { let (_b, app, middle) = ::app(); @@ -982,3 +1064,4 @@ fn author_license_and_description_required() { !json.errors[0].detail.contains("license"), "{:?}", json.errors); } + diff --git a/src/upload.rs b/src/upload.rs index 72a8ee32ba7..3a775645f6e 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -24,6 +24,7 @@ pub struct NewCrate { pub license: Option, pub license_file: Option, pub repository: Option, + pub badges: Option>>, } #[derive(PartialEq, Eq, Hash)] diff --git a/src/user/mod.rs b/src/user/mod.rs index 1b3ea1853d4..8e9bdf1e928 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -336,7 +336,9 @@ pub fn updates(req: &mut Request) -> CargoResult { } // Encode everything! - let crates = crates.into_iter().map(|c| c.minimal_encodable()).collect(); + let crates = crates.into_iter().map(|c| { + c.minimal_encodable(None) + }).collect(); let versions = versions.into_iter().map(|v| { let id = v.crate_id; v.encodable(&map[&id]) diff --git a/tests/acceptance/search-test.js b/tests/acceptance/search-test.js index 082b4279194..7bfa0b9dcc9 100644 --- a/tests/acceptance/search-test.js +++ b/tests/acceptance/search-test.js @@ -23,6 +23,10 @@ test('searching for "rust"', function(assert) { hasText(assert, '#crates .row:first .desc .info', 'rust_mixin'); findWithAssert('#crates .row:first .desc .info .vers img[alt="0.0.1"]'); + + findWithAssert('#crates .row:first .desc .info .badge:first a img[src="https://ci.appveyor.com/api/projects/status/github/huonw/external_mixin?svg=true&branch=master"]'); + findWithAssert('#crates .row:first .desc .info .badge:eq(1) a img[src="https://travis-ci.org/huonw/external_mixin.svg?branch=master"]'); + hasText(assert, '#crates .row:first .desc .summary', 'Yo dawg, use Rust to generate Rust, right in your Rust. (See `external_mixin` to use scripting languages.)'); hasText(assert, '#crates .row:first .downloads', '477'); });