diff --git a/app/components/user-link.js b/app/components/user-link.js index a5ac8b17584..dbef9d4b70d 100644 --- a/app/components/user-link.js +++ b/app/components/user-link.js @@ -12,6 +12,6 @@ export default Ember.Component.extend({ 'href': function() { // TODO replace this with a link to a native crates.io profile // page when they exist. - return 'https://github.com/' + this.get('user.login'); + return this.get('user.url'); }.property('user'), }); diff --git a/app/models/user.js b/app/models/user.js index c13b1948efd..d1a736e1a76 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -6,4 +6,5 @@ export default DS.Model.extend({ login: DS.attr('string'), api_token: DS.attr('string'), avatar: DS.attr('string'), + url: DS.attr('string'), }); diff --git a/src/app.rs b/src/app.rs index 84f61163b65..9c89c5475e3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,6 +8,7 @@ use git2; use oauth2; use r2d2; use s3; +use curl::http; use {db, Config}; @@ -28,13 +29,15 @@ pub struct AppMiddleware { impl App { pub fn new(config: &Config) -> App { - let github = oauth2::Config::new( + let mut github = oauth2::Config::new( &config.gh_client_id, &config.gh_client_secret, "https://github.com/login/oauth/authorize", "https://github.com/login/oauth/access_token", ); + github.scopes.push(String::from("read:org")); + let db_config = r2d2::Config::builder() .pool_size(if config.env == ::Env::Production {10} else {1}) .helper_threads(if config.env == ::Env::Production {3} else {1}) @@ -56,6 +59,14 @@ impl App { config: config.clone(), }; } + + pub fn handle(&self) -> http::Handle { + let handle = http::handle(); + match self.s3_proxy { + Some(ref proxy) => handle.proxy(&proxy[..]), + None => handle, + } + } } impl AppMiddleware { diff --git a/src/bin/migrate.rs b/src/bin/migrate.rs index 6b523d2338f..f75d534a757 100644 --- a/src/bin/migrate.rs +++ b/src/bin/migrate.rs @@ -448,6 +448,24 @@ fn migrations() -> Vec { try!(tx.execute("DROP INDEX index_keywords_lower_keyword", &[])); Ok(()) }), + Migration::add_column(20150715170350, "crate_owners", "owner_kind", + "INTEGER NOT NULL DEFAULT 0"), + Migration::run(20150804170127, + "ALTER TABLE crate_owners ALTER owner_kind DROP DEFAULT", + "ALTER TABLE crate_owners ALTER owner_kind SET DEFAULT 0", + ), + Migration::add_table(20150804170128, "teams", " + id SERIAL PRIMARY KEY, + login VARCHAR NOT NULL UNIQUE, + github_id INTEGER NOT NULL UNIQUE, + name VARCHAR, + avatar VARCHAR + "), + Migration::run(20150804170129, + "ALTER TABLE crate_owners RENAME user_id TO owner_id", + "ALTER TABLE crate_owners RENAME owner_id TO user_id", + ), + undo_foreign_key(20150804170130, "crate_owners", "user_id", "users (id)"), ]; // NOTE: Generate a new id via `date +"%Y%m%d%H%M%S"` @@ -469,6 +487,16 @@ fn migrations() -> Vec { Migration::run(id, &add, &rm) } + fn undo_foreign_key(id: i64, table: &str, column: &str, + references: &str) -> Migration { + let add = format!("ALTER TABLE {table} ADD CONSTRAINT fk_{table}_{col} + FOREIGN KEY ({col}) REFERENCES {reference}", + table = table, col = column, reference = references); + let rm = format!("ALTER TABLE {table} DROP CONSTRAINT fk_{table}_{col}", + table = table, col = column); + Migration::run(id, &rm, &add) + } + fn index(id: i64, table: &str, column: &str) -> Migration { let add = format!("CREATE INDEX index_{table}_{column} ON {table} ({column})", @@ -479,6 +507,7 @@ fn migrations() -> Vec { } } +// DO NOT UPDATE OR USE FOR NEW MIGRATIONS fn fix_duplicate_crate_owners(tx: &postgres::Transaction) -> postgres::Result<()> { let v: Vec<(i32, i32)> = { let stmt = try!(tx.prepare("SELECT user_id, crate_id diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 00000000000..79c079b19bf --- /dev/null +++ b/src/http.rs @@ -0,0 +1,67 @@ +use curl; +use oauth2::*; +use app::App; +use util::{CargoResult, internal, ChainError, human}; +use rustc_serialize::{json, Decodable}; +use std::str; + + +/// Does all the nonsense for sending a GET to Github. Doesn't handle parsing +/// because custom error-code handling may be desirable. Use +/// parse_github_response to handle the "common" processing of responses. +pub fn github(app: &App, url: &str, auth: &Token) + -> Result { + info!("GITHUB HTTP: {}", url); + + let url = if app.config.env == ::Env::Test { + format!("http://api.github.com{}", url) + } else { + format!("https://api.github.com{}", url) + }; + + app.handle() + .get(url) + .header("Accept", "application/vnd.github.v3+json") + .header("User-Agent", "hello!") + .auth_with(auth) + .exec() +} + +/// Checks for normal responses +pub fn parse_github_response(resp: curl::http::Response) + -> CargoResult { + match resp.get_code() { + 200 => {} // Ok! + 403 => { + return Err(human("It looks like you don't have permission \ + to query a necessary property from Github + to complete this request. \ + You may need to re-authenticate on \ + crates.io to grant permission to read \ + github org memberships. Just go to \ + https://crates.io/login")); + } + _ => { + return Err(internal(format!("didn't get a 200 result from + github: {}", resp))); + } + } + + let json = try!(str::from_utf8(resp.get_body()).ok().chain_error(||{ + internal("github didn't send a utf8-response") + })); + + json::decode(json).chain_error(|| { + internal("github didn't send a valid json response") + }) +} + +/// Gets a token with the given string as the access token, but all +/// other info null'd out. Generally, just to be fed to the `github` fn. +pub fn token(token: String) -> Token { + Token { + access_token: token, + scopes: Vec::new(), + token_type: String::new(), + } +} diff --git a/src/krate.rs b/src/krate.rs index bdeaba17bcf..a5ad92440f9 100644 --- a/src/krate.rs +++ b/src/krate.rs @@ -27,7 +27,8 @@ use download::{VersionDownload, EncodableVersionDownload}; use git; use keyword::EncodableKeyword; use upload; -use user::{RequestUser, EncodableUser}; +use user::RequestUser; +use owner::{EncodableOwner, Owner, Rights, OwnerKind, Team, rights}; use util::errors::{NotFound, CargoError}; use util::{LimitErrorReader, HashingReader}; use util::{RequestUtils, CargoResult, internal, ChainError, human}; @@ -174,9 +175,10 @@ impl Crate { }))); try!(conn.execute("INSERT INTO crate_owners - (crate_id, user_id, created_at, updated_at, deleted) - VALUES ($1, $2, $3, $3, FALSE)", - &[&ret.id, &user_id, &now])); + (crate_id, owner_id, created_by, created_at, + updated_at, deleted, owner_kind) + VALUES ($1, $2, $2, $3, $3, FALSE, $4)", + &[&ret.id, &user_id, &now, &(OwnerKind::User as i32)])); return Ok(ret); fn validate_url(url: Option<&str>, field: &str) -> CargoResult<()> { @@ -288,38 +290,63 @@ impl Crate { Ok(ret) } - pub fn owners(&self, conn: &Connection) -> CargoResult> { + pub fn owners(&self, conn: &Connection) -> CargoResult> { let stmt = try!(conn.prepare("SELECT * FROM users INNER JOIN crate_owners - ON crate_owners.user_id = users.id + ON crate_owners.owner_id = users.id WHERE crate_owners.crate_id = $1 - AND crate_owners.deleted = FALSE")); - let rows = try!(stmt.query(&[&self.id])); - Ok(rows.iter().map(|r| Model::from_row(&r)).collect()) + AND crate_owners.deleted = FALSE + AND crate_owners.owner_kind = $2")); + let user_rows = try!(stmt.query(&[&self.id, &(OwnerKind::User as i32)])); + + let stmt = try!(conn.prepare("SELECT * FROM teams + INNER JOIN crate_owners + ON crate_owners.owner_id = teams.id + WHERE crate_owners.crate_id = $1 + AND crate_owners.deleted = FALSE + AND crate_owners.owner_kind = $2")); + let team_rows = try!(stmt.query(&[&self.id, &(OwnerKind::Team as i32)])); + + let mut owners = vec![]; + owners.extend(user_rows.iter().map(|r| Owner::User(Model::from_row(&r)))); + owners.extend(team_rows.iter().map(|r| Owner::Team(Model::from_row(&r)))); + Ok(owners) } - pub fn owner_add(&self, conn: &Connection, who: i32, - name: &str) -> CargoResult<()> { - let user = try!(User::find_by_login(conn, name).map_err(|_| { - human(format!("could not find user with login `{}`", name)) - })); + pub fn owner_add(&self, app: &App, conn: &Connection, req_user: &User, + login: &str) -> CargoResult<()> { + + let owner = match Owner::find_by_login(conn, login) { + Ok(owner@Owner::User(_)) => { owner } + Ok(Owner::Team(team)) => if try!(team.contains_user(app, req_user)) { + Owner::Team(team) + } else { + return Err(human(format!("only members of {} can add it as an owner", login))); + }, + Err(err) => if login.contains(":") { + Owner::Team(try!(Team::create(app, conn, login, req_user))) + } else { + return Err(err); + }, + }; + try!(conn.execute("INSERT INTO crate_owners - (crate_id, user_id, created_at, updated_at, - created_by, deleted) - VALUES ($1, $2, $3, $3, $4, FALSE)", - &[&self.id, &user.id, &::now(), &who])); + (crate_id, owner_id, created_at, updated_at, + created_by, owner_kind, deleted) + VALUES ($1, $2, $3, $3, $4, $5, FALSE)", + &[&self.id, &owner.id(), &::now(), &req_user.id, &owner.kind()])); Ok(()) } - pub fn owner_remove(&self, conn: &Connection, _who: i32, - name: &str) -> CargoResult<()> { - let user = try!(User::find_by_login(conn, name).map_err(|_| { - human(format!("could not find user with login `{}`", name)) + pub fn owner_remove(&self, conn: &Connection, _req_user: &User, + login: &str) -> CargoResult<()> { + let owner = try!(Owner::find_by_login(conn, login).map_err(|_| { + human(format!("could not find owner with login `{}`", login)) })); try!(conn.execute("UPDATE crate_owners SET deleted = TRUE, updated_at = $1 - WHERE crate_id = $2 AND user_id = $3", - &[&::now(), &self.id, &user.id])); + WHERE crate_id = $2 AND owner_id = $3 AND owner_kind = $4", + &[&::now(), &self.id, &owner.id(), &owner.kind()])); Ok(()) } @@ -476,13 +503,16 @@ pub fn index(req: &mut Request) -> CargoResult { (format!("SELECT crates.* FROM crates INNER JOIN crate_owners ON crate_owners.crate_id = crates.id - WHERE crate_owners.user_id = $1 {} \ + WHERE crate_owners.owner_id = $1 + AND crate_owners.owner_kind = {} {} LIMIT $2 OFFSET $3", - sort_sql), - "SELECT COUNT(crates.*) FROM crates + OwnerKind::User as i32, sort_sql), + format!("SELECT COUNT(crates.*) FROM crates INNER JOIN crate_owners ON crate_owners.crate_id = crates.id - WHERE crate_owners.user_id = $1".to_string()) + WHERE crate_owners.owner_id = $1 \ + AND crate_owners.owner_kind = {}", + OwnerKind::User as i32)) }) }).or_else(|| { query.get("following").map(|_| { @@ -629,13 +659,13 @@ pub fn new(req: &mut Request) -> CargoResult { &new_crate.repository, &new_crate.license, &new_crate.license_file)); - if krate.user_id != user.id { - let owners = try!(krate.owners(try!(req.tx()))); - if !owners.iter().any(|o| o.id == user.id) { - return Err(human("crate name has already been claimed by \ - another user")) - } + + let owners = try!(krate.owners(try!(req.tx()))); + if try!(rights(req.app(), &owners, &user)) < Rights::Publish { + return Err(human("crate name has already been claimed by \ + another user")) } + if krate.name != name { return Err(human(format!("crate was previously named `{}`", krate.name))) } @@ -655,11 +685,7 @@ pub fn new(req: &mut Request) -> CargoResult { try!(Keyword::update_crate(try!(req.tx()), &krate, &keywords)); // Upload the crate to S3 - let handle = http::handle(); - let mut handle = match req.app().s3_proxy { - Some(ref proxy) => handle.proxy(&proxy[..]), - None => handle, - }; + let mut handle = req.app().handle(); let path = krate.s3_path(&vers.to_string()); let (resp, cksum) = { let length = try!(read_le_u32(req.body())); @@ -953,10 +979,10 @@ pub fn owners(req: &mut Request) -> CargoResult { let tx = try!(req.tx()); let krate = try!(Crate::find_by_name(tx, crate_name)); let owners = try!(krate.owners(tx)); - let owners = owners.into_iter().map(|u| u.encodable()).collect(); + let owners = owners.into_iter().map(|o| o.encodable()).collect(); #[derive(RustcEncodable)] - struct R { users: Vec } + struct R { users: Vec } Ok(req.json(&R{ users: owners })) } @@ -974,26 +1000,45 @@ fn modify_owners(req: &mut Request, add: bool) -> CargoResult { let (user, krate) = try!(user_and_crate(req)); let tx = try!(req.tx()); let owners = try!(krate.owners(tx)); - if !owners.iter().any(|u| u.id == user.id) { - return Err(human("must already be an owner to modify owners")) + + match try!(rights(req.app(), &owners, &user)) { + Rights::Full => {} // Yes! + Rights::Publish => { + return Err(human("team members don't have permission to modify owners")); + } + Rights::None => { + return Err(human("only owners have permission to modify owners")); + } + } + + #[derive(RustcDecodable)] + struct Request { + // identical, for back-compat (owners preferred) + users: Option>, + owners: Option>, } - #[derive(RustcDecodable)] struct Request { users: Vec } let request: Request = try!(json::decode(&body).map_err(|_| { human("invalid json request") })); - for login in request.users.iter() { + let logins = try!(request.owners.or(request.users).ok_or_else(|| { + human("invalid json request") + })); + + for login in &logins { if add { - if owners.iter().any(|u| u.gh_login == *login) { - return Err(human(format!("user `{}` is already an owner", login))) + if owners.iter().any(|owner| owner.login() == *login) { + return Err(human(format!("`{}` is already an owner", login))) } - try!(krate.owner_add(tx, user.id, &login)); + try!(krate.owner_add(req.app(), tx, &user, &login)); } else { + // Removing the team that gives you rights is prevented because + // team members only have Rights::Publish if *login == user.gh_login { return Err(human("cannot remove yourself as an owner")) } - try!(krate.owner_remove(tx, user.id, &login)); + try!(krate.owner_remove(tx, &user, &login)); } } diff --git a/src/lib.rs b/src/lib.rs index de62cbac770..bfb11191d21 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,8 +54,10 @@ pub mod krate; pub mod model; pub mod upload; pub mod user; +pub mod owner; pub mod util; pub mod version; +pub mod http; mod licenses; #[derive(PartialEq, Eq, Clone, Copy)] diff --git a/src/owner.rs b/src/owner.rs new file mode 100644 index 00000000000..595b286695a --- /dev/null +++ b/src/owner.rs @@ -0,0 +1,313 @@ +use {Model, User}; +use util::{RequestUtils, CargoResult, ChainError, human}; +use db::Connection; +use pg::rows::Row; +use util::errors::NotFound; +use http; +use app::App; + +#[repr(u32)] +pub enum OwnerKind { + User = 0, + Team = 1, +} + +/// Unifies the notion of a User or a Team. +pub enum Owner { + User(User), + Team(Team), +} + +/// For now, just a Github Team. Can be upgraded to other teams +/// later if desirable. +pub struct Team { + /// We're assuming these are stable + pub github_id: i32, + /// Unique table id + pub id: i32, + /// "github:org:team" + /// An opaque unique ID, that was at one point parsed out to query Github. + /// We only query membership with github using the github_id, though. + /// This is the only name we should ever talk to Cargo about. + pub login: String, + /// Sugary goodness + pub name: Option, + pub avatar: Option, + +} + +#[derive(RustcEncodable)] +pub struct EncodableOwner { + pub id: i32, + pub login: String, + pub kind: String, + pub email: Option, + pub url: Option, + pub name: Option, + pub avatar: Option, +} + +/// Access rights to the crate (publishing and ownership management) +/// NOTE: The order of these variants matters! +#[derive(PartialEq, Eq, PartialOrd, Ord)] +pub enum Rights { + None, + Publish, + Full, +} + +impl Team { + /// Just gets the Team from the database by name. + pub fn find_by_login(conn: &Connection, login: &str) -> CargoResult { + let stmt = try!(conn.prepare("SELECT * FROM teams + WHERE login = $1")); + let rows = try!(stmt.query(&[&login])); + let row = try!(rows.iter().next().chain_error(|| { + NotFound + })); + Ok(Model::from_row(&row)) + } + + /// Tries to create the Team in the DB (assumes a `:` has already been found). + pub fn create(app: &App, conn: &Connection, login: &str, req_user: &User) + -> CargoResult { + // must look like system:xxxxxxx + let mut chunks = login.split(":"); + match chunks.next().unwrap() { + // github:rust-lang:owners + "github" => { + // Ok to unwrap since we know one ":" is contained + let org = chunks.next().unwrap(); + let team = try!(chunks.next().ok_or_else(|| + human("missing github team argument; \ + format is github:org:team") + )); + Team::create_github_team(app, conn, login, org, team, req_user) + } + _ => { + Err(human("unknown organization handler, \ + only 'github:org:team' is supported")) + } + } + } + + /// Tries to create a Github Team from scratch. Assumes `org` and `team` are + /// correctly parsed out of the full `name`. `name` is passed as a + /// convenience to avoid rebuilding it. + pub fn create_github_team(app: &App, conn: &Connection, login: &str, + org_name: &str, team_name: &str, req_user: &User) + -> CargoResult { + // GET orgs/:org/teams + // check that `team` is the `slug` in results, and grab its data + + // "sanitization" + fn whitelist(c: &char) -> bool { + match *c { + 'a'...'z' | 'A'...'Z' | '0'...'9' | '-' | '_' => false, + _ => true + } + } + + if let Some(c) = org_name.chars().find(whitelist) { + return Err(human(format!("organization cannot contain special \ + characters like {}", c))); + } + + #[derive(RustcDecodable)] + struct GithubTeam { + slug: String, // the name we want to find + id: i32, // unique GH id (needed for membership queries) + name: Option, // Pretty name + } + + // FIXME: we just set per_page=100 and don't bother chasing pagination + // links. A hundred teams should be enough for any org, right? + let url = format!("/orgs/{}/teams", org_name); + let token = http::token(req_user.gh_access_token.clone()); + let resp = try!(http::github(app, &url, &token)); + let teams: Vec = try!(http::parse_github_response(resp)); + + let team = try!(teams.into_iter().find(|team| team.slug == team_name) + .ok_or_else(||{ + human(format!("could not find the github team {}/{}", + org_name, team_name)) + }) + ); + + if !try!(team_with_gh_id_contains_user(app, team.id, req_user)) { + return Err(human("only members of a team can add it as an owner")); + } + + #[derive(RustcDecodable)] + struct Org { + avatar_url: Option, + } + + let url = format!("/orgs/{}", org_name); + let resp = try!(http::github(app, &url, &token)); + let org: Org = try!(http::parse_github_response(resp)); + + Team::insert(conn, login, team.id, team.name, org.avatar_url) + } + + pub fn insert(conn: &Connection, + login: &str, + github_id: i32, + name: Option, + avatar: Option) + -> CargoResult { + + let stmt = try!(conn.prepare("INSERT INTO teams + (login, github_id, name, avatar) + VALUES ($1, $2, $3, $4) + RETURNING *")); + + let rows = try!(stmt.query(&[&login, &github_id, &name, &avatar])); + let row = rows.iter().next().unwrap(); + Ok(Model::from_row(&row)) + } + + /// Phones home to Github to ask if this User is a member of the given team. + /// Note that we're assuming that the given user is the one interested in + /// the answer. If this is not the case, then we could accidentally leak + /// private membership information here. + pub fn contains_user(&self, app: &App, user: &User) -> CargoResult { + team_with_gh_id_contains_user(app, self.github_id, user) + } +} + +fn team_with_gh_id_contains_user(app: &App, github_id: i32, user: &User) + -> CargoResult { + // GET teams/:team_id/memberships/:user_name + // check that "state": "active" + + #[derive(RustcDecodable)] + struct Membership { + state: String, + } + + let url = format!("/teams/{}/memberships/{}", + &github_id, &user.gh_login); + let token = http::token(user.gh_access_token.clone()); + let resp = try!(http::github(app, &url, &token)); + + // Officially how `false` is returned + if resp.get_code() == 404 { return Ok(false) } + + let membership: Membership = try!(http::parse_github_response(resp)); + + // There is also `state: pending` for which we could possibly give + // some feedback, but it's not obvious how that should work. + Ok(membership.state == "active") +} + +impl Model for Team { + fn from_row(row: &Row) -> Self { + Team { + id: row.get("id"), + name: row.get("name"), + github_id: row.get("github_id"), + login: row.get("login"), + avatar: row.get("avatar"), + } + } + + fn table_name(_: Option) -> &'static str { "teams" } +} + +impl Owner { + /// Finds the owner by name, failing out if it doesn't exist. + /// May be a user's GH login, or a full team name. This is case + /// sensitive. + pub fn find_by_login(conn: &Connection, name: &str) -> CargoResult { + let owner = if name.contains(":") { + Owner::Team(try!(Team::find_by_login(conn, name).map_err(|_| + human(format!("could not find team with name {}", name)) + ))) + } else { + Owner::User(try!(User::find_by_login(conn, name).map_err(|_| + human(format!("could not find user with login `{}`", name)) + ))) + }; + Ok(owner) + } + + pub fn kind(&self) -> i32 { + match *self { + Owner::User(_) => OwnerKind::User as i32, + Owner::Team(_) => OwnerKind::Team as i32, + } + } + + pub fn login(&self) -> &str { + match *self { + Owner::User(ref user) => &user.gh_login, + Owner::Team(ref team) => &team.login, + } + } + + pub fn id(&self) -> i32 { + match *self { + Owner::User(ref user) => user.id, + Owner::Team(ref team) => team.id, + } + } + + pub fn encodable(self) -> EncodableOwner { + match self { + Owner::User(User { id, email, name, gh_login, avatar, .. }) => { + let url = format!("https://github.com/{}", gh_login); + EncodableOwner { + id: id, + login: gh_login, + email: email, + avatar: avatar, + url: Some(url), + name: name, + kind: String::from("user"), + } + } + Owner::Team(Team { id, name, login, avatar, .. }) => { + let url = { + let mut parts = login.split(":"); + parts.next(); // discard github + format!("https://github.com/{}/teams/{}", + parts.next().unwrap(), parts.next().unwrap()) + }; + EncodableOwner { + id: id, + login: login, + email: None, + url: Some(url), + avatar: avatar, + name: name, + kind: String::from("team"), + } + } + } + } +} + +/// Given this set of owners, determines the strongest rights the +/// given user has. +/// +/// Shortcircuits on `Full` because you can't beat it. In practice we'll always +/// see `[user, user, user, ..., team, team, team]`, so we could shortcircuit on +/// `Publish` as well, but this is a non-obvious invariant so we don't bother. +/// Sweet free optimization if teams are proving burdensome to check. +/// More than one team isn't really expected, though. +pub fn rights(app: &App, owners: &[Owner], user: &User) -> CargoResult { + let mut best = Rights::None; + for owner in owners { + match *owner { + Owner::User(ref other_user) => if other_user.id == user.id { + return Ok(Rights::Full); + }, + Owner::Team(ref team) => if try!(team.contains_user(app, user)) { + best = Rights::Publish; + }, + } + } + Ok(best) +} + diff --git a/src/tests/all.rs b/src/tests/all.rs index c58111486fa..e226285391e 100755 --- a/src/tests/all.rs +++ b/src/tests/all.rs @@ -20,12 +20,13 @@ use std::env; use std::sync::{Once, ONCE_INIT, Arc}; use rustc_serialize::json::{self, Json}; -use conduit::Request; +use conduit::{Request, Method}; use conduit_test::MockRequest; use cargo_registry::app::App; use cargo_registry::db::{self, RequestTransaction}; use cargo_registry::dependency::Kind; use cargo_registry::{User, Crate, Version, Keyword, Dependency}; +use cargo_registry::upload as u; macro_rules! t{ ($e:expr) => ( match $e { @@ -64,6 +65,7 @@ mod user; mod record; mod git; mod version; +mod team; fn app() -> (record::Bomb, Arc, conduit_middleware::MiddlewareBuilder) { struct NoCommit; @@ -251,3 +253,49 @@ fn mock_keyword(req: &mut Request, name: &str) -> Keyword { fn logout(req: &mut Request) { req.mut_extensions().pop::(); } + + +fn new_req(app: Arc, krate: &str, version: &str) -> MockRequest { + new_req_full(app, ::krate(krate), version, Vec::new()) +} + +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)); + return req; +} + +fn new_req_body(krate: Crate, version: &str, deps: Vec) + -> Vec { + let kws = krate.keywords.into_iter().map(u::Keyword).collect(); + new_crate_to_body(&u::NewCrate { + name: u::CrateName(krate.name), + vers: u::CrateVersion(semver::Version::parse(version).unwrap()), + features: HashMap::new(), + deps: deps, + authors: vec!["foo".to_string()], + description: Some("description".to_string()), + homepage: krate.homepage, + documentation: krate.documentation, + readme: krate.readme, + keywords: Some(u::KeywordList(kws)), + license: Some("MIT".to_string()), + license_file: None, + repository: krate.repository, + }) +} + +fn new_crate_to_body(new_crate: &u::NewCrate) -> Vec { + let json = json::encode(&new_crate).unwrap(); + let mut body = Vec::new(); + body.extend([ + (json.len() >> 0) 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()); + body.extend([0, 0, 0, 0].iter().cloned()); + body +} diff --git a/src/tests/http-data/krate_new_crate_owner b/src/tests/http-data/krate_new_crate_owner index 509e7190c38..e6649ac4665 100644 --- a/src/tests/http-data/krate_new_crate_owner +++ b/src/tests/http-data/krate_new_crate_owner @@ -1,4 +1,4 @@ -===REQUEST 331 +===REQUEST 331 PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-1.0.0.crate HTTP/1.1 Accept: */* Proxy-Connection: Keep-Alive @@ -8,8 +8,8 @@ Authorization: AWS AKIAJF3GEK7N44BACDZA:GDxGb6r3SIqo9wXuzHrgMNWekwk= Date: Sun, 28 Jun 2015 14:07:17 -0700 Content-Type: application/x-tar - -===RESPONSE 258 + +===RESPONSE 258 HTTP/1.1 200 x-amz-id-2: 2gW1MBpo8abH102drqw/LsJGidfGUTfFcRqEJfzelXBwgXqMBS1O/GvDnu5StO1n0kNHWnBL6IQ= server: AmazonS3 @@ -18,8 +18,8 @@ x-amz-request-id: B260A3AF8A2BEA4D etag: "d41d8cd98f00b204e9800998ecf8427e" date: Sun, 28 Jun 2015 21:07:51 GMT - -===REQUEST 331 + +===REQUEST 331 PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-2.0.0.crate HTTP/1.1 Accept: */* Proxy-Connection: Keep-Alive @@ -29,8 +29,8 @@ Content-Length: 0 Date: Sun, 28 Jun 2015 14:07:17 -0700 Content-Type: application/x-tar - -===RESPONSE 258 + +===RESPONSE 258 HTTP/1.1 200 etag: "d41d8cd98f00b204e9800998ecf8427e" x-amz-id-2: qAvAgvb6vV1a9oqOdTc6xJGINzdslcmfmg0VAeBvxR58W1y77iIAnQhq5P4Th5h0S08Q6D00uKQ= @@ -39,4 +39,4 @@ content-length: 0 server: AmazonS3 x-amz-request-id: B4CFD4002D00FB5F - + diff --git a/src/tests/http-data/team_add_owners_as_team_owner b/src/tests/http-data/team_add_owners_as_team_owner new file mode 100644 index 00000000000..a035ed35520 --- /dev/null +++ b/src/tests/http-data/team_add_owners_as_team_owner @@ -0,0 +1,157 @@ +===REQUEST 240 +GET http://api.github.com/orgs/crates-test-org/teams HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Accept: application/vnd.github.v3+json +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 +User-Agent: hello! + + +===RESPONSE 1995 +HTTP/1.1 200 +access-control-allow-origin: * +x-oauth-scopes: read:org +date: Tue, 18 Aug 2015 00:18:47 GMT +content-length: 852 +x-ratelimit-reset: 1439860497 +etag: "c5a75e367565d032956a3cedfe950504" +x-github-media-type: github.v3; format=json +content-security-policy: default-src 'none' +access-control-allow-credentials: true +x-accepted-oauth-scopes: admin:org, read:org, repo, user, write:org +x-oauth-client-id: 89b6afdeaa6c6c7506ec +x-frame-options: deny +status: 200 OK +content-type: application/json; charset=utf-8 +x-ratelimit-remaining: 4953 +cache-control: private, max-age=60, s-maxage=60 +strict-transport-security: max-age=31536000; includeSubdomains; preload +x-ratelimit-limit: 5000 +x-served-by: 474556b853193c38f1b14328ce2d1b7d +x-content-type-options: nosniff +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +server: GitHub.com +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +x-github-request-id: 3FF5DB35:3D4C:9D6B6FD:55D279E7 +x-xss-protection: 1; mode=block + +[{"name":"Owners","id":1699377,"slug":"owners","description":null,"permission":"admin","url":"https://api.github.com/teams/1699377","members_url":"https://api.github.com/teams/1699377/members{/member}","repositories_url":"https://api.github.com/teams/1699377/repos"},{"name":"just-for-crates-2","id":1699379,"slug":"just-for-crates-2","description":"Just for Crates 2","permission":"pull","url":"https://api.github.com/teams/1699379","members_url":"https://api.github.com/teams/1699379/members{/member}","repositories_url":"https://api.github.com/teams/1699379/repos"},{"name":"just-for-crates1","id":1699378,"slug":"just-for-crates1","description":"","permission":"pull","url":"https://api.github.com/teams/1699378","members_url":"https://api.github.com/teams/1699378/members{/member}","repositories_url":"https://api.github.com/teams/1699378/repos"}] +===REQUEST 255 +GET http://api.github.com/teams/1699377/memberships/crates-tester-2 HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +User-Agent: hello! +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 +Accept: application/vnd.github.v3+json + + +===RESPONSE 1227 +HTTP/1.1 200 +access-control-allow-origin: * +x-served-by: 8dd185e423974a7e13abbbe6e060031e +cache-control: private, max-age=60, s-maxage=60 +content-type: application/json; charset=utf-8 +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +content-security-policy: default-src 'none' +x-oauth-client-id: 89b6afdeaa6c6c7506ec +etag: "c43e7230d4c92c2a6369014be0d352d3" +x-xss-protection: 1; mode=block +strict-transport-security: max-age=31536000; includeSubdomains; preload +x-content-type-options: nosniff +x-ratelimit-remaining: 4952 +date: Tue, 18 Aug 2015 00:18:48 GMT +x-accepted-oauth-scopes: admin:org, read:org, repo, write:org +x-github-request-id: 3FF5DB35:3D4B:D10AA8A:55D279E8 +x-ratelimit-reset: 1439860497 +server: GitHub.com +x-oauth-scopes: read:org +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +x-github-media-type: github.v3; format=json +status: 200 OK +x-frame-options: deny +x-ratelimit-limit: 5000 +content-length: 91 +access-control-allow-credentials: true + +{"state":"active","url":"https://api.github.com/teams/1699377/memberships/crates-tester-2"} +===REQUEST 234 +GET http://api.github.com/orgs/crates-test-org HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +User-Agent: hello! +Accept: application/vnd.github.v3+json +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 + + +===RESPONSE 2079 +HTTP/1.1 200 +x-github-media-type: github.v3; format=json +x-xss-protection: 1; mode=block +x-content-type-options: nosniff +cache-control: private, max-age=60, s-maxage=60 +x-accepted-oauth-scopes: admin:org, read:org, repo, user, write:org +x-ratelimit-limit: 5000 +server: GitHub.com +x-served-by: 318e55760cf7cdb40e61175a4d36cd32 +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +x-ratelimit-remaining: 4951 +etag: "fed47c4a794f484872c57aaeaff7feac" +last-modified: Mon, 17 Aug 2015 23:27:29 GMT +access-control-allow-credentials: true +content-type: application/json; charset=utf-8 +access-control-allow-origin: * +strict-transport-security: max-age=31536000; includeSubdomains; preload +x-oauth-client-id: 89b6afdeaa6c6c7506ec +x-oauth-scopes: read:org +status: 200 OK +x-github-request-id: 3FF5DB35:3D4B:D10AB34:55D279E8 +x-frame-options: deny +date: Tue, 18 Aug 2015 00:18:48 GMT +x-ratelimit-reset: 1439860497 +content-security-policy: default-src 'none' +content-length: 890 +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding + +{"login":"crates-test-org","id":13804222,"url":"https://api.github.com/orgs/crates-test-org","repos_url":"https://api.github.com/orgs/crates-test-org/repos","events_url":"https://api.github.com/orgs/crates-test-org/events","members_url":"https://api.github.com/orgs/crates-test-org/members{/member}","public_members_url":"https://api.github.com/orgs/crates-test-org/public_members{/member}","avatar_url":"https://avatars.githubusercontent.com/u/13804222?v=3","description":null,"public_repos":0,"public_gists":0,"followers":0,"following":0,"html_url":"https://github.com/crates-test-org","created_at":"2015-08-15T00:07:30Z","updated_at":"2015-08-17T23:27:29Z","type":"Organization","total_private_repos":0,"owned_private_repos":0,"private_gists":0,"disk_usage":0,"collaborators":0,"billing_email":"a.beingessner+crates2@gmail.com","plan":{"name":"free","space":976562499,"private_repos":0}} +===REQUEST 255 +GET http://api.github.com/teams/1699377/memberships/crates-tester-1 HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Authorization: token e5452e00fd329a8acddce59aabc4bf95abf276a0 +User-Agent: hello! +Accept: application/vnd.github.v3+json + + +===RESPONSE 1227 +HTTP/1.1 200 +status: 200 OK +x-github-media-type: github.v3; format=json +x-ratelimit-limit: 5000 +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +strict-transport-security: max-age=31536000; includeSubdomains; preload +x-oauth-scopes: read:org +content-type: application/json; charset=utf-8 +content-length: 91 +x-github-request-id: 3FF5DB35:3D4C:9D6B86B:55D279E8 +date: Tue, 18 Aug 2015 00:18:49 GMT +etag: "f6ec304b608781ecc639b246dea422ef" +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +access-control-allow-origin: * +server: GitHub.com +x-ratelimit-remaining: 4986 +x-oauth-client-id: 89b6afdeaa6c6c7506ec +access-control-allow-credentials: true +x-xss-protection: 1; mode=block +x-frame-options: deny +x-accepted-oauth-scopes: admin:org, read:org, repo, write:org +x-ratelimit-reset: 1439860516 +x-served-by: a7f8a126c9ed3f1c4715a34c0ddc7290 +cache-control: private, max-age=60, s-maxage=60 +content-security-policy: default-src 'none' +x-content-type-options: nosniff + +{"state":"active","url":"https://api.github.com/teams/1699377/memberships/crates-tester-1"} diff --git a/src/tests/http-data/team_add_team_as_member b/src/tests/http-data/team_add_team_as_member new file mode 100644 index 00000000000..15db97110c7 --- /dev/null +++ b/src/tests/http-data/team_add_team_as_member @@ -0,0 +1,118 @@ +===REQUEST 240 +GET http://api.github.com/orgs/crates-test-org/teams HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +User-Agent: hello! +Accept: application/vnd.github.v3+json +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 + + +===RESPONSE 1995 +HTTP/1.1 200 +x-xss-protection: 1; mode=block +x-ratelimit-remaining: 4962 +x-served-by: 2811da37fbdda4367181b328b22b2499 +x-oauth-client-id: 89b6afdeaa6c6c7506ec +strict-transport-security: max-age=31536000; includeSubdomains; preload +server: GitHub.com +access-control-allow-origin: * +x-accepted-oauth-scopes: admin:org, read:org, repo, user, write:org +content-type: application/json; charset=utf-8 +x-ratelimit-reset: 1439860497 +x-oauth-scopes: read:org +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +content-security-policy: default-src 'none' +x-content-type-options: nosniff +date: Tue, 18 Aug 2015 00:18:42 GMT +status: 200 OK +etag: "c5a75e367565d032956a3cedfe950504" +x-ratelimit-limit: 5000 +content-length: 852 +x-github-media-type: github.v3; format=json +x-frame-options: deny +x-github-request-id: 3FF5DB35:3D49:BF91AC3:55D279E2 +cache-control: private, max-age=60, s-maxage=60 +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +access-control-allow-credentials: true + +[{"name":"Owners","id":1699377,"slug":"owners","description":null,"permission":"admin","url":"https://api.github.com/teams/1699377","members_url":"https://api.github.com/teams/1699377/members{/member}","repositories_url":"https://api.github.com/teams/1699377/repos"},{"name":"just-for-crates-2","id":1699379,"slug":"just-for-crates-2","description":"Just for Crates 2","permission":"pull","url":"https://api.github.com/teams/1699379","members_url":"https://api.github.com/teams/1699379/members{/member}","repositories_url":"https://api.github.com/teams/1699379/repos"},{"name":"just-for-crates1","id":1699378,"slug":"just-for-crates1","description":"","permission":"pull","url":"https://api.github.com/teams/1699378","members_url":"https://api.github.com/teams/1699378/members{/member}","repositories_url":"https://api.github.com/teams/1699378/repos"}] +===REQUEST 255 +GET http://api.github.com/teams/1699377/memberships/crates-tester-2 HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 +User-Agent: hello! +Accept: application/vnd.github.v3+json + + +===RESPONSE 1227 +HTTP/1.1 200 +x-served-by: c6c65e5196703428e7641f7d1e9bc353 +status: 200 OK +x-github-request-id: 3FF5DB35:3D4B:D10A1E1:55D279E3 +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +x-ratelimit-reset: 1439860497 +content-length: 91 +server: GitHub.com +etag: "c43e7230d4c92c2a6369014be0d352d3" +x-accepted-oauth-scopes: admin:org, read:org, repo, write:org +date: Tue, 18 Aug 2015 00:18:43 GMT +content-security-policy: default-src 'none' +access-control-allow-origin: * +cache-control: private, max-age=60, s-maxage=60 +x-oauth-scopes: read:org +x-frame-options: deny +x-github-media-type: github.v3; format=json +access-control-allow-credentials: true +x-content-type-options: nosniff +strict-transport-security: max-age=31536000; includeSubdomains; preload +x-ratelimit-remaining: 4961 +x-ratelimit-limit: 5000 +x-oauth-client-id: 89b6afdeaa6c6c7506ec +content-type: application/json; charset=utf-8 +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +x-xss-protection: 1; mode=block + +{"state":"active","url":"https://api.github.com/teams/1699377/memberships/crates-tester-2"} +===REQUEST 234 +GET http://api.github.com/orgs/crates-test-org HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Accept: application/vnd.github.v3+json +User-Agent: hello! +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 + + +===RESPONSE 2079 +HTTP/1.1 200 +x-github-media-type: github.v3; format=json +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +status: 200 OK +access-control-allow-credentials: true +x-served-by: d594a23ec74671eba905bf91ef329026 +date: Tue, 18 Aug 2015 00:18:43 GMT +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +x-github-request-id: 3FF5DB35:3D4A:CF5AFB5:55D279E3 +x-content-type-options: nosniff +x-ratelimit-remaining: 4960 +x-xss-protection: 1; mode=block +x-frame-options: deny +x-oauth-scopes: read:org +etag: "fed47c4a794f484872c57aaeaff7feac" +last-modified: Mon, 17 Aug 2015 23:27:29 GMT +x-ratelimit-limit: 5000 +content-security-policy: default-src 'none' +access-control-allow-origin: * +server: GitHub.com +cache-control: private, max-age=60, s-maxage=60 +content-length: 890 +x-accepted-oauth-scopes: admin:org, read:org, repo, user, write:org +x-ratelimit-reset: 1439860497 +strict-transport-security: max-age=31536000; includeSubdomains; preload +content-type: application/json; charset=utf-8 +x-oauth-client-id: 89b6afdeaa6c6c7506ec + +{"login":"crates-test-org","id":13804222,"url":"https://api.github.com/orgs/crates-test-org","repos_url":"https://api.github.com/orgs/crates-test-org/repos","events_url":"https://api.github.com/orgs/crates-test-org/events","members_url":"https://api.github.com/orgs/crates-test-org/members{/member}","public_members_url":"https://api.github.com/orgs/crates-test-org/public_members{/member}","avatar_url":"https://avatars.githubusercontent.com/u/13804222?v=3","description":null,"public_repos":0,"public_gists":0,"followers":0,"following":0,"html_url":"https://github.com/crates-test-org","created_at":"2015-08-15T00:07:30Z","updated_at":"2015-08-17T23:27:29Z","type":"Organization","total_private_repos":0,"owned_private_repos":0,"private_gists":0,"disk_usage":0,"collaborators":0,"billing_email":"a.beingessner+crates2@gmail.com","plan":{"name":"free","space":976562499,"private_repos":0}} diff --git a/src/tests/http-data/team_add_team_as_non_member b/src/tests/http-data/team_add_team_as_non_member new file mode 100644 index 00000000000..7f931da9913 --- /dev/null +++ b/src/tests/http-data/team_add_team_as_non_member @@ -0,0 +1,73 @@ +===REQUEST 240 +GET http://api.github.com/orgs/crates-test-org/teams HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Accept: application/vnd.github.v3+json +Authorization: token e5452e00fd329a8acddce59aabc4bf95abf276a0 +User-Agent: hello! + + +===RESPONSE 1995 +HTTP/1.1 200 +content-length: 852 +x-served-by: 7f48e2f7761567e923121f17538d7a6d +x-github-media-type: github.v3; format=json +x-content-type-options: nosniff +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +content-type: application/json; charset=utf-8 +x-oauth-client-id: 89b6afdeaa6c6c7506ec +server: GitHub.com +x-accepted-oauth-scopes: admin:org, read:org, repo, user, write:org +cache-control: private, max-age=60, s-maxage=60 +x-ratelimit-remaining: 4988 +x-xss-protection: 1; mode=block +x-frame-options: deny +access-control-allow-credentials: true +date: Tue, 18 Aug 2015 00:18:47 GMT +access-control-allow-origin: * +strict-transport-security: max-age=31536000; includeSubdomains; preload +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +x-ratelimit-reset: 1439860516 +x-ratelimit-limit: 5000 +content-security-policy: default-src 'none' +status: 200 OK +x-github-request-id: 3FF5DB35:3D4A:CF5B61F:55D279E6 +x-oauth-scopes: read:org +etag: "c25241104113e88a92c0d35a18a7e24e" + +[{"name":"Owners","id":1699377,"slug":"owners","description":null,"permission":"admin","url":"https://api.github.com/teams/1699377","members_url":"https://api.github.com/teams/1699377/members{/member}","repositories_url":"https://api.github.com/teams/1699377/repos"},{"name":"just-for-crates-2","id":1699379,"slug":"just-for-crates-2","description":"Just for Crates 2","permission":"pull","url":"https://api.github.com/teams/1699379","members_url":"https://api.github.com/teams/1699379/members{/member}","repositories_url":"https://api.github.com/teams/1699379/repos"},{"name":"just-for-crates1","id":1699378,"slug":"just-for-crates1","description":"","permission":"pull","url":"https://api.github.com/teams/1699378","members_url":"https://api.github.com/teams/1699378/members{/member}","repositories_url":"https://api.github.com/teams/1699378/repos"}] +===REQUEST 255 +GET http://api.github.com/teams/1699379/memberships/crates-tester-1 HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +User-Agent: hello! +Accept: application/vnd.github.v3+json +Authorization: token e5452e00fd329a8acddce59aabc4bf95abf276a0 + + +===RESPONSE 1008 +HTTP/1.1 404 +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +x-xss-protection: 1; mode=block +x-content-type-options: nosniff +content-type: application/json; charset=utf-8 +x-github-request-id: 3FF5DB35:3D43:39E5A29:55D279E7 +x-github-media-type: github.v3; format=json +x-ratelimit-limit: 5000 +content-security-policy: default-src 'none' +access-control-allow-origin: * +x-frame-options: deny +x-ratelimit-remaining: 4987 +x-oauth-client-id: 89b6afdeaa6c6c7506ec +content-length: 77 +x-oauth-scopes: read:org +status: 404 Not Found +date: Tue, 18 Aug 2015 00:18:47 GMT +access-control-allow-credentials: true +strict-transport-security: max-age=31536000; includeSubdomains; preload +x-accepted-oauth-scopes: admin:org, read:org, repo, write:org +server: GitHub.com +x-ratelimit-reset: 1439860516 + +{"message":"Not Found","documentation_url":"https://developer.github.com/v3"} diff --git a/src/tests/http-data/team_publish_not_owned b/src/tests/http-data/team_publish_not_owned new file mode 100644 index 00000000000..b6dc574dd46 --- /dev/null +++ b/src/tests/http-data/team_publish_not_owned @@ -0,0 +1,152 @@ +===REQUEST 240 +GET http://api.github.com/orgs/crates-test-org/teams HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 +User-Agent: hello! +Accept: application/vnd.github.v3+json + + +===RESPONSE 1995 +HTTP/1.1 200 +x-oauth-scopes: read:org +content-type: application/json; charset=utf-8 +content-length: 852 +x-frame-options: deny +server: GitHub.com +etag: "c5a75e367565d032956a3cedfe950504" +x-content-type-options: nosniff +date: Tue, 18 Aug 2015 00:18:44 GMT +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +x-github-request-id: 3FF5DB35:3D47:8F24277:55D279E3 +x-accepted-oauth-scopes: admin:org, read:org, repo, user, write:org +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +x-oauth-client-id: 89b6afdeaa6c6c7506ec +cache-control: private, max-age=60, s-maxage=60 +x-xss-protection: 1; mode=block +status: 200 OK +content-security-policy: default-src 'none' +x-served-by: 2811da37fbdda4367181b328b22b2499 +access-control-allow-origin: * +access-control-allow-credentials: true +x-ratelimit-limit: 5000 +x-ratelimit-reset: 1439860497 +x-github-media-type: github.v3; format=json +x-ratelimit-remaining: 4959 +strict-transport-security: max-age=31536000; includeSubdomains; preload + +[{"name":"Owners","id":1699377,"slug":"owners","description":null,"permission":"admin","url":"https://api.github.com/teams/1699377","members_url":"https://api.github.com/teams/1699377/members{/member}","repositories_url":"https://api.github.com/teams/1699377/repos"},{"name":"just-for-crates-2","id":1699379,"slug":"just-for-crates-2","description":"Just for Crates 2","permission":"pull","url":"https://api.github.com/teams/1699379","members_url":"https://api.github.com/teams/1699379/members{/member}","repositories_url":"https://api.github.com/teams/1699379/repos"},{"name":"just-for-crates1","id":1699378,"slug":"just-for-crates1","description":"","permission":"pull","url":"https://api.github.com/teams/1699378","members_url":"https://api.github.com/teams/1699378/members{/member}","repositories_url":"https://api.github.com/teams/1699378/repos"}] +===REQUEST 255 +GET http://api.github.com/teams/1699379/memberships/crates-tester-2 HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +User-Agent: hello! +Accept: application/vnd.github.v3+json +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 + + +===RESPONSE 1227 +HTTP/1.1 200 +server: GitHub.com +x-ratelimit-limit: 5000 +content-length: 91 +etag: "f8cdd924e598005a783881fc67a196b6" +date: Tue, 18 Aug 2015 00:18:44 GMT +x-github-media-type: github.v3; format=json +status: 200 OK +x-ratelimit-remaining: 4958 +x-frame-options: deny +x-oauth-client-id: 89b6afdeaa6c6c7506ec +access-control-allow-origin: * +strict-transport-security: max-age=31536000; includeSubdomains; preload +x-accepted-oauth-scopes: admin:org, read:org, repo, write:org +access-control-allow-credentials: true +x-oauth-scopes: read:org +x-content-type-options: nosniff +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +content-type: application/json; charset=utf-8 +x-xss-protection: 1; mode=block +x-served-by: 13d09b732ebe76f892093130dc088652 +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +cache-control: private, max-age=60, s-maxage=60 +x-github-request-id: 3FF5DB35:3D4A:CF5B152:55D279E4 +content-security-policy: default-src 'none' +x-ratelimit-reset: 1439860497 + +{"state":"active","url":"https://api.github.com/teams/1699379/memberships/crates-tester-2"} +===REQUEST 234 +GET http://api.github.com/orgs/crates-test-org HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Accept: application/vnd.github.v3+json +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 +User-Agent: hello! + + +===RESPONSE 2079 +HTTP/1.1 200 +etag: "fed47c4a794f484872c57aaeaff7feac" +access-control-allow-credentials: true +x-content-type-options: nosniff +content-security-policy: default-src 'none' +date: Tue, 18 Aug 2015 00:18:44 GMT +status: 200 OK +x-ratelimit-remaining: 4957 +x-ratelimit-limit: 5000 +x-accepted-oauth-scopes: admin:org, read:org, repo, user, write:org +x-ratelimit-reset: 1439860497 +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +x-frame-options: deny +x-served-by: 5aeb3f30c9e3ef6ef7bcbcddfd9a68f7 +strict-transport-security: max-age=31536000; includeSubdomains; preload +server: GitHub.com +content-length: 890 +x-github-media-type: github.v3; format=json +x-oauth-client-id: 89b6afdeaa6c6c7506ec +content-type: application/json; charset=utf-8 +last-modified: Mon, 17 Aug 2015 23:27:29 GMT +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +x-github-request-id: 3FF5DB35:3D47:8F24367:55D279E4 +cache-control: private, max-age=60, s-maxage=60 +x-xss-protection: 1; mode=block +x-oauth-scopes: read:org +access-control-allow-origin: * + +{"login":"crates-test-org","id":13804222,"url":"https://api.github.com/orgs/crates-test-org","repos_url":"https://api.github.com/orgs/crates-test-org/repos","events_url":"https://api.github.com/orgs/crates-test-org/events","members_url":"https://api.github.com/orgs/crates-test-org/members{/member}","public_members_url":"https://api.github.com/orgs/crates-test-org/public_members{/member}","avatar_url":"https://avatars.githubusercontent.com/u/13804222?v=3","description":null,"public_repos":0,"public_gists":0,"followers":0,"following":0,"html_url":"https://github.com/crates-test-org","created_at":"2015-08-15T00:07:30Z","updated_at":"2015-08-17T23:27:29Z","type":"Organization","total_private_repos":0,"owned_private_repos":0,"private_gists":0,"disk_usage":0,"collaborators":0,"billing_email":"a.beingessner+crates2@gmail.com","plan":{"name":"free","space":976562499,"private_repos":0}} +===REQUEST 255 +GET http://api.github.com/teams/1699379/memberships/crates-tester-1 HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Accept: application/vnd.github.v3+json +Authorization: token e5452e00fd329a8acddce59aabc4bf95abf276a0 +User-Agent: hello! + + +===RESPONSE 1008 +HTTP/1.1 404 +x-ratelimit-remaining: 4989 +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +x-github-request-id: 3FF5DB35:3D4A:CF5B2FC:55D279E5 +x-oauth-scopes: read:org +access-control-allow-credentials: true +content-type: application/json; charset=utf-8 +x-frame-options: deny +x-ratelimit-limit: 5000 +status: 404 Not Found +x-accepted-oauth-scopes: admin:org, read:org, repo, write:org +content-security-policy: default-src 'none' +server: GitHub.com +access-control-allow-origin: * +x-ratelimit-reset: 1439860516 +date: Tue, 18 Aug 2015 00:18:45 GMT +content-length: 77 +x-github-media-type: github.v3; format=json +x-content-type-options: nosniff +strict-transport-security: max-age=31536000; includeSubdomains; preload +x-xss-protection: 1; mode=block +x-oauth-client-id: 89b6afdeaa6c6c7506ec + +{"message":"Not Found","documentation_url":"https://developer.github.com/v3"} diff --git a/src/tests/http-data/team_publish_owned b/src/tests/http-data/team_publish_owned new file mode 100644 index 00000000000..e801fc22f39 --- /dev/null +++ b/src/tests/http-data/team_publish_owned @@ -0,0 +1,178 @@ +===REQUEST 240 +GET http://api.github.com/orgs/crates-test-org/teams HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Accept: application/vnd.github.v3+json +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 +User-Agent: hello! + + +===RESPONSE 1995 +HTTP/1.1 200 +cache-control: private, max-age=60, s-maxage=60 +server: GitHub.com +x-ratelimit-reset: 1439860497 +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +x-oauth-scopes: read:org +content-length: 852 +date: Tue, 18 Aug 2015 00:18:49 GMT +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +access-control-allow-origin: * +x-xss-protection: 1; mode=block +x-ratelimit-limit: 5000 +x-github-request-id: 3FF5DB35:3D4C:9D6B8E6:55D279E9 +x-accepted-oauth-scopes: admin:org, read:org, repo, user, write:org +status: 200 OK +x-served-by: 474556b853193c38f1b14328ce2d1b7d +x-github-media-type: github.v3; format=json +x-oauth-client-id: 89b6afdeaa6c6c7506ec +x-ratelimit-remaining: 4950 +x-content-type-options: nosniff +content-type: application/json; charset=utf-8 +x-frame-options: deny +access-control-allow-credentials: true +etag: "c5a75e367565d032956a3cedfe950504" +content-security-policy: default-src 'none' +strict-transport-security: max-age=31536000; includeSubdomains; preload + +[{"name":"Owners","id":1699377,"slug":"owners","description":null,"permission":"admin","url":"https://api.github.com/teams/1699377","members_url":"https://api.github.com/teams/1699377/members{/member}","repositories_url":"https://api.github.com/teams/1699377/repos"},{"name":"just-for-crates-2","id":1699379,"slug":"just-for-crates-2","description":"Just for Crates 2","permission":"pull","url":"https://api.github.com/teams/1699379","members_url":"https://api.github.com/teams/1699379/members{/member}","repositories_url":"https://api.github.com/teams/1699379/repos"},{"name":"just-for-crates1","id":1699378,"slug":"just-for-crates1","description":"","permission":"pull","url":"https://api.github.com/teams/1699378","members_url":"https://api.github.com/teams/1699378/members{/member}","repositories_url":"https://api.github.com/teams/1699378/repos"}] +===REQUEST 255 +GET http://api.github.com/teams/1699377/memberships/crates-tester-2 HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 +User-Agent: hello! +Accept: application/vnd.github.v3+json + + +===RESPONSE 1227 +HTTP/1.1 200 +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +x-served-by: 474556b853193c38f1b14328ce2d1b7d +x-accepted-oauth-scopes: admin:org, read:org, repo, write:org +x-content-type-options: nosniff +x-oauth-scopes: read:org +server: GitHub.com +date: Tue, 18 Aug 2015 00:18:49 GMT +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +content-type: application/json; charset=utf-8 +x-ratelimit-remaining: 4949 +x-oauth-client-id: 89b6afdeaa6c6c7506ec +x-ratelimit-reset: 1439860497 +status: 200 OK +strict-transport-security: max-age=31536000; includeSubdomains; preload +x-github-media-type: github.v3; format=json +cache-control: private, max-age=60, s-maxage=60 +x-frame-options: deny +x-xss-protection: 1; mode=block +access-control-allow-credentials: true +access-control-allow-origin: * +x-github-request-id: 3FF5DB35:3D4A:CF5BB36:55D279E9 +etag: "c43e7230d4c92c2a6369014be0d352d3" +content-security-policy: default-src 'none' +content-length: 91 +x-ratelimit-limit: 5000 + +{"state":"active","url":"https://api.github.com/teams/1699377/memberships/crates-tester-2"} +===REQUEST 234 +GET http://api.github.com/orgs/crates-test-org HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Accept: application/vnd.github.v3+json +User-Agent: hello! +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 + + +===RESPONSE 2079 +HTTP/1.1 200 +access-control-allow-origin: * +server: GitHub.com +content-type: application/json; charset=utf-8 +x-served-by: dc1ce2bfb41810a06c705e83b388572d +x-github-request-id: 3FF5DB35:3D48:A9A50E5:55D279E9 +content-security-policy: default-src 'none' +x-oauth-scopes: read:org +content-length: 890 +last-modified: Mon, 17 Aug 2015 23:27:29 GMT +x-frame-options: deny +x-ratelimit-limit: 5000 +x-oauth-client-id: 89b6afdeaa6c6c7506ec +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +etag: "fed47c4a794f484872c57aaeaff7feac" +x-github-media-type: github.v3; format=json +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +cache-control: private, max-age=60, s-maxage=60 +status: 200 OK +date: Tue, 18 Aug 2015 00:18:50 GMT +x-ratelimit-reset: 1439860497 +strict-transport-security: max-age=31536000; includeSubdomains; preload +x-ratelimit-remaining: 4948 +x-content-type-options: nosniff +x-accepted-oauth-scopes: admin:org, read:org, repo, user, write:org +access-control-allow-credentials: true +x-xss-protection: 1; mode=block + +{"login":"crates-test-org","id":13804222,"url":"https://api.github.com/orgs/crates-test-org","repos_url":"https://api.github.com/orgs/crates-test-org/repos","events_url":"https://api.github.com/orgs/crates-test-org/events","members_url":"https://api.github.com/orgs/crates-test-org/members{/member}","public_members_url":"https://api.github.com/orgs/crates-test-org/public_members{/member}","avatar_url":"https://avatars.githubusercontent.com/u/13804222?v=3","description":null,"public_repos":0,"public_gists":0,"followers":0,"following":0,"html_url":"https://github.com/crates-test-org","created_at":"2015-08-15T00:07:30Z","updated_at":"2015-08-17T23:27:29Z","type":"Organization","total_private_repos":0,"owned_private_repos":0,"private_gists":0,"disk_usage":0,"collaborators":0,"billing_email":"a.beingessner+crates2@gmail.com","plan":{"name":"free","space":976562499,"private_repos":0}} +===REQUEST 255 +GET http://api.github.com/teams/1699377/memberships/crates-tester-1 HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +User-Agent: hello! +Authorization: token e5452e00fd329a8acddce59aabc4bf95abf276a0 +Accept: application/vnd.github.v3+json + + +===RESPONSE 1227 +HTTP/1.1 200 +strict-transport-security: max-age=31536000; includeSubdomains; preload +server: GitHub.com +content-length: 91 +x-accepted-oauth-scopes: admin:org, read:org, repo, write:org +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +content-type: application/json; charset=utf-8 +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +x-oauth-scopes: read:org +access-control-allow-credentials: true +x-ratelimit-limit: 5000 +x-github-request-id: 3FF5DB35:3D48:A9A5182:55D279EA +x-xss-protection: 1; mode=block +x-oauth-client-id: 89b6afdeaa6c6c7506ec +content-security-policy: default-src 'none' +x-served-by: dc1ce2bfb41810a06c705e83b388572d +date: Tue, 18 Aug 2015 00:18:50 GMT +x-ratelimit-reset: 1439860516 +cache-control: private, max-age=60, s-maxage=60 +x-github-media-type: github.v3; format=json +x-frame-options: deny +status: 200 OK +access-control-allow-origin: * +x-content-type-options: nosniff +etag: "f6ec304b608781ecc639b246dea422ef" +x-ratelimit-remaining: 4985 + +{"state":"active","url":"https://api.github.com/teams/1699377/memberships/crates-tester-1"} +===REQUEST 331 +PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-2.0.0.crate HTTP/1.1 +Accept: */* +Proxy-Connection: Keep-Alive +Host: alexcrichton-test.s3.amazonaws.com +Date: Mon, 17 Aug 2015 17:18:50 -0700 +Authorization: AWS AKIAJF3GEK7N44BACDZA:IESAcFsCMRHuwl5DaFdEcQDrlZQ= +Content-Length: 0 +Content-Type: application/x-tar + + +===RESPONSE 258 +HTTP/1.1 200 +server: AmazonS3 +x-amz-id-2: 4uE7gp4rdv1H3gUhh0jV9HVxaGDi4bqEkuxeGPTTUFLD8WYjpnXF833Mr12rNRxpZ/B9wlgcXlU= +date: Tue, 18 Aug 2015 00:18:52 GMT +x-amz-request-id: C574F412BCFA076D +content-length: 0 +etag: "d41d8cd98f00b204e9800998ecf8427e" + + diff --git a/src/tests/http-data/team_remove_team_as_named_owner b/src/tests/http-data/team_remove_team_as_named_owner new file mode 100644 index 00000000000..33e92d14a83 --- /dev/null +++ b/src/tests/http-data/team_remove_team_as_named_owner @@ -0,0 +1,118 @@ +===REQUEST 240 +GET http://api.github.com/orgs/crates-test-org/teams HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Accept: application/vnd.github.v3+json +User-Agent: hello! +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 + + +===RESPONSE 1995 +HTTP/1.1 200 +etag: "c5a75e367565d032956a3cedfe950504" +x-github-request-id: 3FF5DB35:3D4B:D10A62A:55D279E5 +date: Tue, 18 Aug 2015 00:18:45 GMT +x-accepted-oauth-scopes: admin:org, read:org, repo, user, write:org +x-content-type-options: nosniff +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +x-ratelimit-reset: 1439860497 +strict-transport-security: max-age=31536000; includeSubdomains; preload +status: 200 OK +x-ratelimit-limit: 5000 +cache-control: private, max-age=60, s-maxage=60 +x-oauth-scopes: read:org +x-served-by: 8a5c38021a5cd7cef7b8f49a296fee40 +x-oauth-client-id: 89b6afdeaa6c6c7506ec +access-control-allow-credentials: true +x-ratelimit-remaining: 4956 +x-github-media-type: github.v3; format=json +content-length: 852 +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +content-type: application/json; charset=utf-8 +x-frame-options: deny +content-security-policy: default-src 'none' +x-xss-protection: 1; mode=block +server: GitHub.com +access-control-allow-origin: * + +[{"name":"Owners","id":1699377,"slug":"owners","description":null,"permission":"admin","url":"https://api.github.com/teams/1699377","members_url":"https://api.github.com/teams/1699377/members{/member}","repositories_url":"https://api.github.com/teams/1699377/repos"},{"name":"just-for-crates-2","id":1699379,"slug":"just-for-crates-2","description":"Just for Crates 2","permission":"pull","url":"https://api.github.com/teams/1699379","members_url":"https://api.github.com/teams/1699379/members{/member}","repositories_url":"https://api.github.com/teams/1699379/repos"},{"name":"just-for-crates1","id":1699378,"slug":"just-for-crates1","description":"","permission":"pull","url":"https://api.github.com/teams/1699378","members_url":"https://api.github.com/teams/1699378/members{/member}","repositories_url":"https://api.github.com/teams/1699378/repos"}] +===REQUEST 255 +GET http://api.github.com/teams/1699377/memberships/crates-tester-2 HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 +Accept: application/vnd.github.v3+json +User-Agent: hello! + + +===RESPONSE 1227 +HTTP/1.1 200 +content-security-policy: default-src 'none' +date: Tue, 18 Aug 2015 00:18:46 GMT +status: 200 OK +cache-control: private, max-age=60, s-maxage=60 +x-accepted-oauth-scopes: admin:org, read:org, repo, write:org +server: GitHub.com +content-type: application/json; charset=utf-8 +x-github-media-type: github.v3; format=json +x-served-by: 139317cebd6caf9cd03889139437f00b +x-ratelimit-limit: 5000 +x-oauth-scopes: read:org +x-ratelimit-reset: 1439860497 +access-control-allow-credentials: true +x-github-request-id: 3FF5DB35:3D49:BF92049:55D279E5 +x-oauth-client-id: 89b6afdeaa6c6c7506ec +x-frame-options: deny +etag: "c43e7230d4c92c2a6369014be0d352d3" +strict-transport-security: max-age=31536000; includeSubdomains; preload +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +access-control-allow-origin: * +x-xss-protection: 1; mode=block +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +x-content-type-options: nosniff +x-ratelimit-remaining: 4955 +content-length: 91 + +{"state":"active","url":"https://api.github.com/teams/1699377/memberships/crates-tester-2"} +===REQUEST 234 +GET http://api.github.com/orgs/crates-test-org HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +User-Agent: hello! +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 +Accept: application/vnd.github.v3+json + + +===RESPONSE 2079 +HTTP/1.1 200 +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +server: GitHub.com +x-accepted-oauth-scopes: admin:org, read:org, repo, user, write:org +status: 200 OK +x-oauth-scopes: read:org +x-oauth-client-id: 89b6afdeaa6c6c7506ec +x-content-type-options: nosniff +etag: "fed47c4a794f484872c57aaeaff7feac" +access-control-allow-credentials: true +x-xss-protection: 1; mode=block +x-ratelimit-remaining: 4954 +last-modified: Mon, 17 Aug 2015 23:27:29 GMT +strict-transport-security: max-age=31536000; includeSubdomains; preload +cache-control: private, max-age=60, s-maxage=60 +x-ratelimit-limit: 5000 +content-type: application/json; charset=utf-8 +access-control-allow-origin: * +x-github-media-type: github.v3; format=json +x-served-by: cee4c0729c8e9147e7abcb45b9d69689 +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +content-security-policy: default-src 'none' +date: Tue, 18 Aug 2015 00:18:46 GMT +x-github-request-id: 3FF5DB35:3D42:7013D71:55D279E6 +x-frame-options: deny +content-length: 890 +x-ratelimit-reset: 1439860497 + +{"login":"crates-test-org","id":13804222,"url":"https://api.github.com/orgs/crates-test-org","repos_url":"https://api.github.com/orgs/crates-test-org/repos","events_url":"https://api.github.com/orgs/crates-test-org/events","members_url":"https://api.github.com/orgs/crates-test-org/members{/member}","public_members_url":"https://api.github.com/orgs/crates-test-org/public_members{/member}","avatar_url":"https://avatars.githubusercontent.com/u/13804222?v=3","description":null,"public_repos":0,"public_gists":0,"followers":0,"following":0,"html_url":"https://github.com/crates-test-org","created_at":"2015-08-15T00:07:30Z","updated_at":"2015-08-17T23:27:29Z","type":"Organization","total_private_repos":0,"owned_private_repos":0,"private_gists":0,"disk_usage":0,"collaborators":0,"billing_email":"a.beingessner+crates2@gmail.com","plan":{"name":"free","space":976562499,"private_repos":0}} diff --git a/src/tests/http-data/team_remove_team_as_team_owner b/src/tests/http-data/team_remove_team_as_team_owner new file mode 100644 index 00000000000..ef8940e00cb --- /dev/null +++ b/src/tests/http-data/team_remove_team_as_team_owner @@ -0,0 +1,217 @@ +===REQUEST 240 +GET http://api.github.com/orgs/crates-test-org/teams HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 +User-Agent: hello! +Accept: application/vnd.github.v3+json + + +===RESPONSE 1995 +HTTP/1.1 200 +x-served-by: a7f8a126c9ed3f1c4715a34c0ddc7290 +x-ratelimit-reset: 1439860497 +x-xss-protection: 1; mode=block +server: GitHub.com +cache-control: private, max-age=60, s-maxage=60 +x-github-request-id: 3FF5DB35:3D46:521241A:55D279EB +x-accepted-oauth-scopes: admin:org, read:org, repo, user, write:org +content-type: application/json; charset=utf-8 +x-github-media-type: github.v3; format=json +date: Tue, 18 Aug 2015 00:18:51 GMT +x-ratelimit-limit: 5000 +x-oauth-client-id: 89b6afdeaa6c6c7506ec +x-frame-options: deny +access-control-allow-origin: * +x-oauth-scopes: read:org +access-control-allow-credentials: true +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +x-content-type-options: nosniff +etag: "c5a75e367565d032956a3cedfe950504" +x-ratelimit-remaining: 4947 +content-security-policy: default-src 'none' +strict-transport-security: max-age=31536000; includeSubdomains; preload +status: 200 OK +content-length: 852 + +[{"name":"Owners","id":1699377,"slug":"owners","description":null,"permission":"admin","url":"https://api.github.com/teams/1699377","members_url":"https://api.github.com/teams/1699377/members{/member}","repositories_url":"https://api.github.com/teams/1699377/repos"},{"name":"just-for-crates-2","id":1699379,"slug":"just-for-crates-2","description":"Just for Crates 2","permission":"pull","url":"https://api.github.com/teams/1699379","members_url":"https://api.github.com/teams/1699379/members{/member}","repositories_url":"https://api.github.com/teams/1699379/repos"},{"name":"just-for-crates1","id":1699378,"slug":"just-for-crates1","description":"","permission":"pull","url":"https://api.github.com/teams/1699378","members_url":"https://api.github.com/teams/1699378/members{/member}","repositories_url":"https://api.github.com/teams/1699378/repos"}] +===REQUEST 255 +GET http://api.github.com/teams/1699377/memberships/crates-tester-2 HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +User-Agent: hello! +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 +Accept: application/vnd.github.v3+json + + +===RESPONSE 1227 +HTTP/1.1 200 +date: Tue, 18 Aug 2015 00:18:52 GMT +status: 200 OK +x-ratelimit-remaining: 4946 +x-xss-protection: 1; mode=block +strict-transport-security: max-age=31536000; includeSubdomains; preload +x-github-request-id: 3FF5DB35:3D43:39E5CF3:55D279EB +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +x-served-by: b0ef53392caa42315c6206737946d931 +etag: "c43e7230d4c92c2a6369014be0d352d3" +x-accepted-oauth-scopes: admin:org, read:org, repo, write:org +content-security-policy: default-src 'none' +content-length: 91 +access-control-allow-credentials: true +x-content-type-options: nosniff +x-ratelimit-limit: 5000 +cache-control: private, max-age=60, s-maxage=60 +x-oauth-scopes: read:org +content-type: application/json; charset=utf-8 +x-ratelimit-reset: 1439860497 +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +x-oauth-client-id: 89b6afdeaa6c6c7506ec +x-frame-options: deny +access-control-allow-origin: * +x-github-media-type: github.v3; format=json +server: GitHub.com + +{"state":"active","url":"https://api.github.com/teams/1699377/memberships/crates-tester-2"} +===REQUEST 234 +GET http://api.github.com/orgs/crates-test-org HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Accept: application/vnd.github.v3+json +Authorization: token e6d126e00feb3960bd52506092497e1a59d15263 +User-Agent: hello! + + +===RESPONSE 2079 +HTTP/1.1 200 +last-modified: Mon, 17 Aug 2015 23:27:29 GMT +status: 200 OK +x-ratelimit-limit: 5000 +x-ratelimit-remaining: 4945 +server: GitHub.com +date: Tue, 18 Aug 2015 00:18:52 GMT +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +etag: "fed47c4a794f484872c57aaeaff7feac" +content-length: 890 +strict-transport-security: max-age=31536000; includeSubdomains; preload +x-oauth-client-id: 89b6afdeaa6c6c7506ec +x-xss-protection: 1; mode=block +x-oauth-scopes: read:org +access-control-allow-origin: * +access-control-allow-credentials: true +x-github-media-type: github.v3; format=json +x-frame-options: deny +x-content-type-options: nosniff +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +x-accepted-oauth-scopes: admin:org, read:org, repo, user, write:org +cache-control: private, max-age=60, s-maxage=60 +x-github-request-id: 3FF5DB35:3D4B:D10B219:55D279EC +x-served-by: 2d7a5e35115884240089368322196939 +content-security-policy: default-src 'none' +content-type: application/json; charset=utf-8 +x-ratelimit-reset: 1439860497 + +{"login":"crates-test-org","id":13804222,"url":"https://api.github.com/orgs/crates-test-org","repos_url":"https://api.github.com/orgs/crates-test-org/repos","events_url":"https://api.github.com/orgs/crates-test-org/events","members_url":"https://api.github.com/orgs/crates-test-org/members{/member}","public_members_url":"https://api.github.com/orgs/crates-test-org/public_members{/member}","avatar_url":"https://avatars.githubusercontent.com/u/13804222?v=3","description":null,"public_repos":0,"public_gists":0,"followers":0,"following":0,"html_url":"https://github.com/crates-test-org","created_at":"2015-08-15T00:07:30Z","updated_at":"2015-08-17T23:27:29Z","type":"Organization","total_private_repos":0,"owned_private_repos":0,"private_gists":0,"disk_usage":0,"collaborators":0,"billing_email":"a.beingessner+crates2@gmail.com","plan":{"name":"free","space":976562499,"private_repos":0}} +===REQUEST 255 +GET http://api.github.com/teams/1699377/memberships/crates-tester-1 HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Accept: application/vnd.github.v3+json +User-Agent: hello! +Authorization: token e5452e00fd329a8acddce59aabc4bf95abf276a0 + + +===RESPONSE 1227 +HTTP/1.1 200 +strict-transport-security: max-age=31536000; includeSubdomains; preload +x-content-type-options: nosniff +content-length: 91 +x-xss-protection: 1; mode=block +x-github-request-id: 3FF5DB35:3D4B:D10B2C9:55D279EC +x-oauth-client-id: 89b6afdeaa6c6c7506ec +status: 200 OK +x-oauth-scopes: read:org +x-github-media-type: github.v3; format=json +x-served-by: 5aeb3f30c9e3ef6ef7bcbcddfd9a68f7 +x-frame-options: deny +access-control-allow-origin: * +content-security-policy: default-src 'none' +content-type: application/json; charset=utf-8 +server: GitHub.com +etag: "f6ec304b608781ecc639b246dea422ef" +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +access-control-allow-credentials: true +date: Tue, 18 Aug 2015 00:18:52 GMT +cache-control: private, max-age=60, s-maxage=60 +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding +x-accepted-oauth-scopes: admin:org, read:org, repo, write:org +x-ratelimit-limit: 5000 +x-ratelimit-reset: 1439860516 +x-ratelimit-remaining: 4984 + +{"state":"active","url":"https://api.github.com/teams/1699377/memberships/crates-tester-1"} +===REQUEST 255 +GET http://api.github.com/teams/1699377/memberships/crates-tester-1 HTTP/1.1 +Host: api.github.com +Proxy-Connection: Keep-Alive +Accept: application/vnd.github.v3+json +Authorization: token e5452e00fd329a8acddce59aabc4bf95abf276a0 +User-Agent: hello! + + +===RESPONSE 1227 +HTTP/1.1 200 +strict-transport-security: max-age=31536000; includeSubdomains; preload +server: GitHub.com +x-content-type-options: nosniff +content-security-policy: default-src 'none' +x-oauth-client-id: 89b6afdeaa6c6c7506ec +date: Tue, 18 Aug 2015 00:18:53 GMT +x-frame-options: deny +x-served-by: 8dd185e423974a7e13abbbe6e060031e +content-type: application/json; charset=utf-8 +x-xss-protection: 1; mode=block +x-oauth-scopes: read:org +x-ratelimit-remaining: 4983 +x-github-media-type: github.v3; format=json +access-control-allow-origin: * +cache-control: private, max-age=60, s-maxage=60 +x-ratelimit-limit: 5000 +access-control-expose-headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval +x-accepted-oauth-scopes: admin:org, read:org, repo, write:org +access-control-allow-credentials: true +content-length: 91 +status: 200 OK +x-github-request-id: 3FF5DB35:3D49:BF92BE3:55D279ED +etag: "f6ec304b608781ecc639b246dea422ef" +x-ratelimit-reset: 1439860516 +vary: Accept, Authorization, Cookie, X-GitHub-OTP +vary: Accept-Encoding + +{"state":"active","url":"https://api.github.com/teams/1699377/memberships/crates-tester-1"} +===REQUEST 331 +PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-2.0.0.crate HTTP/1.1 +Accept: */* +Proxy-Connection: Keep-Alive +Authorization: AWS AKIAJF3GEK7N44BACDZA:XsFE7+SIiw6XizkDMEIooaiZlXY= +Content-Type: application/x-tar +Content-Length: 0 +Date: Mon, 17 Aug 2015 17:18:53 -0700 +Host: alexcrichton-test.s3.amazonaws.com + + +===RESPONSE 258 +HTTP/1.1 200 +server: AmazonS3 +date: Tue, 18 Aug 2015 00:18:54 GMT +etag: "d41d8cd98f00b204e9800998ecf8427e" +x-amz-id-2: +dHcxABn2YaCcCH2flBHHwuXWoGK/O11XZJXcKCSphq0aJlyxFh4rPk9CUI6oASSRuwpJOvMOpc= +content-length: 0 +x-amz-request-id: C80E4CECB9D29C04 + + diff --git a/src/tests/krate.rs b/src/tests/krate.rs index 59099f29a5a..0041ba07d69 100644 --- a/src/tests/krate.rs +++ b/src/tests/krate.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use std::io::prelude::*; use std::fs::{self, File}; use std::iter::repeat; -use std::sync::Arc; use conduit::{Handler, Request, Method}; use conduit_test::MockRequest; @@ -10,7 +9,6 @@ use git2; use rustc_serialize::{json, Decoder}; use semver; -use cargo_registry::App; use cargo_registry::dependency::EncodableDependency; use cargo_registry::download::EncodableVersionDownload; use cargo_registry::krate::{Crate, EncodableCrate}; @@ -148,64 +146,19 @@ fn versions() { assert_eq!(json.versions.len(), 1); } -fn new_req(app: Arc, krate: &str, version: &str) -> MockRequest { - new_req_full(app, ::krate(krate), version, Vec::new()) -} - -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)); - return req; -} - -fn new_req_body(krate: Crate, version: &str, deps: Vec) - -> Vec { - let kws = krate.keywords.into_iter().map(u::Keyword).collect(); - new_crate_to_body(&u::NewCrate { - name: u::CrateName(krate.name), - vers: u::CrateVersion(semver::Version::parse(version).unwrap()), - features: HashMap::new(), - deps: deps, - authors: vec!["foo".to_string()], - description: Some("description".to_string()), - homepage: krate.homepage, - documentation: krate.documentation, - readme: krate.readme, - keywords: Some(u::KeywordList(kws)), - license: Some("MIT".to_string()), - license_file: None, - repository: krate.repository, - }) -} - -fn new_crate_to_body(new_crate: &u::NewCrate) -> Vec { - let json = json::encode(&new_crate).unwrap(); - let mut body = Vec::new(); - body.extend([ - (json.len() >> 0) 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()); - body.extend([0, 0, 0, 0].iter().cloned()); - body -} - #[test] fn new_wrong_token() { let (_b, app, middle) = ::app(); - let mut req = new_req(app.clone(), "foo", "1.0.0"); + let mut req = ::new_req(app.clone(), "foo", "1.0.0"); bad_resp!(middle.call(&mut req)); drop(req); - let mut req = new_req(app.clone(), "foo", "1.0.0"); + let mut req = ::new_req(app.clone(), "foo", "1.0.0"); req.header("Authorization", "bad"); bad_resp!(middle.call(&mut req)); drop(req); - let mut req = new_req(app, "foo", "1.0.0"); + let mut req = ::new_req(app, "foo", "1.0.0"); ::mock_user(&mut req, ::user("foo")); ::logout(&mut req); req.header("Authorization", "bad"); @@ -217,7 +170,7 @@ fn new_bad_names() { fn bad_name(name: &str) { println!("testing: `{}`", name); let (_b, app, middle) = ::app(); - let mut req = new_req(app, name, "1.0.0"); + let mut req = ::new_req(app, name, "1.0.0"); ::mock_user(&mut req, ::user("foo")); ::logout(&mut req); let json = bad_resp!(middle.call(&mut req)); @@ -232,7 +185,7 @@ fn new_bad_names() { #[test] fn new_krate() { let (_b, app, middle) = ::app(); - let mut req = new_req(app, "foo", "1.0.0"); + let mut req = ::new_req(app, "foo", "1.0.0"); let user = ::mock_user(&mut req, ::user("foo")); ::logout(&mut req); req.header("Authorization", &user.api_token); @@ -245,7 +198,7 @@ fn new_krate() { #[test] fn new_krate_weird_version() { let (_b, app, middle) = ::app(); - let mut req = new_req(app, "foo", "0.0.0-pre"); + let mut req = ::new_req(app, "foo", "0.0.0-pre"); let user = ::mock_user(&mut req, ::user("foo")); ::logout(&mut req); req.header("Authorization", &user.api_token); @@ -267,7 +220,7 @@ fn new_krate_with_dependency() { target: None, kind: None, }; - let mut req = new_req_full(app, ::krate("new"), "1.0.0", vec![dep]); + let mut req = ::new_req_full(app, ::krate("new"), "1.0.0", vec![dep]); ::mock_user(&mut req, ::user("foo")); ::mock_crate(&mut req, ::krate("foo")); let mut response = ok_resp!(middle.call(&mut req)); @@ -279,7 +232,7 @@ fn new_krate_twice() { let (_b, app, middle) = ::app(); let mut krate = ::krate("foo"); krate.description = Some("description".to_string()); - let mut req = new_req_full(app, krate.clone(), "2.0.0", Vec::new()); + let mut req = ::new_req_full(app, krate.clone(), "2.0.0", Vec::new()); ::mock_user(&mut req, ::user("foo")); ::mock_crate(&mut req, ::krate("foo")); let mut response = ok_resp!(middle.call(&mut req)); @@ -292,7 +245,7 @@ fn new_krate_twice() { fn new_krate_wrong_user() { let (_b, app, middle) = ::app(); - let mut req = new_req(app, "foo", "2.0.0"); + let mut req = ::new_req(app, "foo", "2.0.0"); // Create the 'foo' crate with one user ::mock_user(&mut req, ::user("foo")); @@ -311,14 +264,14 @@ fn new_krate_bad_name() { let (_b, app, middle) = ::app(); { - let mut req = new_req(app.clone(), "snow☃", "2.0.0"); + let mut req = ::new_req(app.clone(), "snow☃", "2.0.0"); ::mock_user(&mut req, ::user("foo")); let json = bad_resp!(middle.call(&mut req)); assert!(json.errors[0].detail.contains("invalid crate name"), "{:?}", json.errors); } { - let mut req = new_req(app, "áccênts", "2.0.0"); + let mut req = ::new_req(app, "áccênts", "2.0.0"); ::mock_user(&mut req, ::user("foo")); let json = bad_resp!(middle.call(&mut req)); assert!(json.errors[0].detail.contains("invalid crate name"), @@ -333,7 +286,7 @@ fn new_crate_owner() { let (_b, app, middle) = ::app(); // Create a crate under one user - let mut req = new_req(app.clone(), "foo", "1.0.0"); + let mut req = ::new_req(app.clone(), "foo", "1.0.0"); let u2 = ::mock_user(&mut req, ::user("bar")); ::mock_user(&mut req, ::user("foo")); let mut response = ok_resp!(middle.call(&mut req)); @@ -357,7 +310,7 @@ fn new_crate_owner() { assert_eq!(::json::(&mut response).crates.len(), 1); // And upload a new crate as the first user - let body = new_req_body(::krate("foo"), "2.0.0", Vec::new()); + let body = ::new_req_body(::krate("foo"), "2.0.0", Vec::new()); req.mut_extensions().insert(u2); let mut response = ok_resp!(middle.call(req.with_path("/api/v1/crates/new") .with_method(Method::Put) @@ -377,7 +330,7 @@ fn valid_feature_names() { #[test] fn new_krate_too_big() { let (_b, app, middle) = ::app(); - let mut req = new_req(app, "foo", "1.0.0"); + let mut req = ::new_req(app, "foo", "1.0.0"); ::mock_user(&mut req, ::user("foo")); req.with_body(repeat("a").take(1000 * 1000).collect::().as_bytes()); bad_resp!(middle.call(&mut req)); @@ -386,7 +339,7 @@ fn new_krate_too_big() { #[test] fn new_krate_duplicate_version() { let (_b, app, middle) = ::app(); - let mut req = new_req(app, "foo", "1.0.0"); + let mut req = ::new_req(app, "foo", "1.0.0"); ::mock_user(&mut req, ::user("foo")); ::mock_crate(&mut req, ::krate("foo")); let json = bad_resp!(middle.call(&mut req)); @@ -397,7 +350,7 @@ fn new_krate_duplicate_version() { #[test] fn new_crate_similar_name() { let (_b, app, middle) = ::app(); - let mut req = new_req(app, "foo", "1.1.0"); + let mut req = ::new_req(app, "foo", "1.1.0"); ::mock_user(&mut req, ::user("foo")); ::mock_crate(&mut req, ::krate("Foo")); let json = bad_resp!(middle.call(&mut req)); @@ -409,7 +362,7 @@ fn new_crate_similar_name() { fn new_crate_similar_name_hyphen() { { let (_b, app, middle) = ::app(); - let mut req = new_req(app, "foo-bar", "1.1.0"); + let mut req = ::new_req(app, "foo-bar", "1.1.0"); ::mock_user(&mut req, ::user("foo")); ::mock_crate(&mut req, ::krate("foo_bar")); let json = bad_resp!(middle.call(&mut req)); @@ -418,7 +371,7 @@ fn new_crate_similar_name_hyphen() { } { let (_b, app, middle) = ::app(); - let mut req = new_req(app, "foo_bar", "1.1.0"); + let mut req = ::new_req(app, "foo_bar", "1.1.0"); ::mock_user(&mut req, ::user("foo")); ::mock_crate(&mut req, ::krate("foo-bar")); let json = bad_resp!(middle.call(&mut req)); @@ -430,7 +383,7 @@ fn new_crate_similar_name_hyphen() { #[test] fn new_krate_git_upload() { let (_b, app, middle) = ::app(); - let mut req = new_req(app, "foo", "1.0.0"); + let mut req = ::new_req(app, "foo", "1.0.0"); ::mock_user(&mut req, ::user("foo")); let mut response = ok_resp!(middle.call(&mut req)); ::json::(&mut response); @@ -456,7 +409,7 @@ fn new_krate_git_upload_appends() { br#"{"name":"FOO","vers":"0.0.1","deps":[],"cksum":"3j3"} "#).unwrap(); - let mut req = new_req(app, "FOO", "1.0.0"); + let mut req = ::new_req(app, "FOO", "1.0.0"); ::mock_user(&mut req, ::user("foo")); let mut response = ok_resp!(middle.call(&mut req)); ::json::(&mut response); @@ -489,7 +442,7 @@ fn new_krate_git_upload_with_conflicts() { &[&parent]).unwrap(); } - let mut req = new_req(app, "foo", "1.0.0"); + let mut req = ::new_req(app, "foo", "1.0.0"); ::mock_user(&mut req, ::user("foo")); let mut response = ok_resp!(middle.call(&mut req)); ::json::(&mut response); @@ -507,7 +460,7 @@ fn new_krate_dependency_missing() { target: None, kind: None, }; - let mut req = new_req_full(app, ::krate("foo"), "1.0.0", vec![dep]); + let mut req = ::new_req_full(app, ::krate("foo"), "1.0.0", vec![dep]); ::mock_user(&mut req, ::user("foo")); let mut response = ok_resp!(middle.call(&mut req)); let json = ::json::<::Bad>(&mut response); @@ -680,7 +633,7 @@ fn yank() { let path = ::git::checkout().join("3/f/foo"); // Upload a new crate, putting it in the git index - let mut req = new_req(app, "foo", "1.0.0"); + let mut req = ::new_req(app, "foo", "1.0.0"); ::mock_user(&mut req, ::user("foo")); let mut response = ok_resp!(middle.call(&mut req)); ::json::(&mut response); @@ -733,7 +686,7 @@ fn bad_keywords() { { let mut krate = ::krate("foo"); krate.keywords.push("super-long-keyword-name-oh-no".to_string()); - let mut req = new_req_full(app.clone(), krate, "1.0.0", Vec::new()); + let mut req = ::new_req_full(app.clone(), krate, "1.0.0", Vec::new()); ::mock_user(&mut req, ::user("foo")); let mut response = ok_resp!(middle.call(&mut req)); ::json::<::Bad>(&mut response); @@ -741,7 +694,7 @@ fn bad_keywords() { { let mut krate = ::krate("foo"); krate.keywords.push("?@?%".to_string()); - let mut req = new_req_full(app.clone(), krate, "1.0.0", Vec::new()); + let mut req = ::new_req_full(app.clone(), krate, "1.0.0", Vec::new()); ::mock_user(&mut req, ::user("foo")); let mut response = ok_resp!(middle.call(&mut req)); ::json::<::Bad>(&mut response); @@ -749,7 +702,7 @@ fn bad_keywords() { { let mut krate = ::krate("foo"); krate.keywords.push("?@?%".to_string()); - let mut req = new_req_full(app.clone(), krate, "1.0.0", Vec::new()); + let mut req = ::new_req_full(app.clone(), krate, "1.0.0", Vec::new()); ::mock_user(&mut req, ::user("foo")); let mut response = ok_resp!(middle.call(&mut req)); ::json::<::Bad>(&mut response); @@ -757,7 +710,7 @@ fn bad_keywords() { { let mut krate = ::krate("foo"); krate.keywords.push("áccênts".to_string()); - let mut req = new_req_full(app.clone(), krate, "1.0.0", Vec::new()); + let mut req = ::new_req_full(app.clone(), krate, "1.0.0", Vec::new()); ::mock_user(&mut req, ::user("foo")); let mut response = ok_resp!(middle.call(&mut req)); ::json::<::Bad>(&mut response); @@ -816,7 +769,7 @@ fn author_license_and_description_required() { license_file: None, repository: None, }; - req.with_body(&new_crate_to_body(&new_crate)); + req.with_body(&::new_crate_to_body(&new_crate)); let json = bad_resp!(middle.call(&mut req)); assert!(json.errors[0].detail.contains("author") && json.errors[0].detail.contains("description") && @@ -825,7 +778,7 @@ fn author_license_and_description_required() { new_crate.license = Some("MIT".to_string()); new_crate.authors.push("".to_string()); - req.with_body(&new_crate_to_body(&new_crate)); + req.with_body(&::new_crate_to_body(&new_crate)); let json = bad_resp!(middle.call(&mut req)); assert!(json.errors[0].detail.contains("author") && json.errors[0].detail.contains("description") && @@ -835,7 +788,7 @@ fn author_license_and_description_required() { new_crate.license = None; new_crate.license_file = Some("foo".to_string()); new_crate.authors.push("foo".to_string()); - req.with_body(&new_crate_to_body(&new_crate)); + req.with_body(&::new_crate_to_body(&new_crate)); let json = bad_resp!(middle.call(&mut req)); assert!(!json.errors[0].detail.contains("author") && json.errors[0].detail.contains("description") && diff --git a/src/tests/team.rs b/src/tests/team.rs new file mode 100644 index 00000000000..8d66ac09ecb --- /dev/null +++ b/src/tests/team.rs @@ -0,0 +1,224 @@ +use conduit::{Handler, Method}; + +use cargo_registry::User; + +// Users: `crates-tester-1` and `crates-tester-2` +// Passwords: ask acrichto or gankro +// Teams: `crates-test-org:owners`, `crates-test-org:just-for-crates-2` +// tester-1 is on owners only, tester-2 is on both + +fn mock_user_on_x_and_y() -> User { + User { + id: 10000, + gh_login: "crates-tester-2".to_string(), + email: None, + name: None, + avatar: None, + gh_access_token: "e6d126e00feb3960bd52506092497e1a59d15263".to_string(), + api_token: User::new_api_token(), + } +} + +fn mock_user_on_only_x() -> User { + User { + id: 10001, + gh_login: "crates-tester-1".to_string(), + email: None, + name: None, + avatar: None, + gh_access_token: "e5452e00fd329a8acddce59aabc4bf95abf276a0".to_string(), + api_token: User::new_api_token(), + } +} + +fn body_for_team_y() -> &'static str { + r#"{"users":["github:crates-test-org:just-for-crates-2"]}"# +} + +fn body_for_team_x() -> &'static str { + r#"{"users":["github:crates-test-org:owners"]}"# +} + + +// Test adding team without `github:` +#[test] +fn not_github() { + let (_b, app, middle) = ::app(); + let mut req = ::new_req(app, "foo", "2.0.0"); + ::mock_user(&mut req, mock_user_on_x_and_y()); + ::mock_crate(&mut req, ::krate("foo")); + + let body = r#"{"users":["dropbox:foo:foo"]}"#; + let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners") + .with_method(Method::Put) + .with_body(body.as_bytes()))); + assert!(json.errors[0].detail.contains("unknown organization"), + "{:?}", json.errors); +} + +// Test adding team without second `:` +#[test] +fn one_colon() { + let (_b, app, middle) = ::app(); + let mut req = ::new_req(app, "foo", "2.0.0"); + ::mock_user(&mut req, mock_user_on_x_and_y()); + ::mock_crate(&mut req, ::krate("foo")); + + let body = r#"{"users":["github:foo"]}"#; + let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners") + .with_method(Method::Put) + .with_body(body.as_bytes()))); + assert!(json.errors[0].detail.contains("missing github team"), + "{:?}", json.errors); +} + +// Test adding team as owner when on it +#[test] +fn add_team_as_member() { + let (_b, app, middle) = ::app(); + let mut req = ::new_req(app, "foo", "2.0.0"); + ::mock_user(&mut req, mock_user_on_x_and_y()); + ::mock_crate(&mut req, ::krate("foo")); + + let body = body_for_team_x(); + ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners") + .with_method(Method::Put) + .with_body(body.as_bytes()))); +} + +// Test adding team as owner when not on in +#[test] +fn add_team_as_non_member() { + let (_b, app, middle) = ::app(); + let mut req = ::new_req(app, "foo", "2.0.0"); + ::mock_user(&mut req, mock_user_on_only_x()); + ::mock_crate(&mut req, ::krate("foo")); + + let body = body_for_team_y(); + let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners") + .with_method(Method::Put) + .with_body(body.as_bytes()))); + assert!(json.errors[0].detail.contains("only members"), + "{:?}", json.errors); +} + +// Test removing team as named owner +#[test] +fn remove_team_as_named_owner() { + let (_b, app, middle) = ::app(); + let mut req = ::new_req(app, "foo", "1.0.0"); + ::mock_user(&mut req, mock_user_on_x_and_y()); + ::mock_crate(&mut req, ::krate("foo")); + + let body = body_for_team_x(); + ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners") + .with_method(Method::Put) + .with_body(body.as_bytes()))); + + let body = body_for_team_x(); + ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners") + .with_method(Method::Delete) + .with_body(body.as_bytes()))); + + ::mock_user(&mut req, mock_user_on_only_x()); + let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/new") + .with_body(&::new_req_body(::krate("foo"), + "2.0.0", vec![])) + .with_method(Method::Put))); + assert!(json.errors[0].detail.contains("another user"), + "{:?}", json.errors); +} + +// Test removing team as team owner +#[test] +fn remove_team_as_team_owner() { + let (_b, app, middle) = ::app(); + let mut req = ::new_req(app, "foo", "1.0.0"); + ::mock_user(&mut req, mock_user_on_x_and_y()); + ::mock_crate(&mut req, ::krate("foo")); + + let body = body_for_team_x(); + ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners") + .with_method(Method::Put) + .with_body(body.as_bytes()))); + + ::mock_user(&mut req, mock_user_on_only_x()); + let body = body_for_team_x(); + let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners") + .with_method(Method::Delete) + .with_body(body.as_bytes()))); + + assert!(json.errors[0].detail.contains("don't have permission"), + "{:?}", json.errors); + + ok_resp!(middle.call(req.with_path("/api/v1/crates/new") + .with_body(&::new_req_body(::krate("foo"), + "2.0.0", vec![])) + .with_method(Method::Put))); +} + +// Test trying to publish a krate we don't own +#[test] +fn publish_not_owned() { + let (_b, app, middle) = ::app(); + + let mut req = ::new_req(app.clone(), "foo", "1.0.0"); + ::mock_user(&mut req, mock_user_on_x_and_y()); + ::mock_crate(&mut req, ::krate("foo")); + + let body = body_for_team_y(); + ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners") + .with_method(Method::Put) + .with_body(body.as_bytes()))); + + ::mock_user(&mut req, mock_user_on_only_x()); + let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/new") + .with_body(&::new_req_body(::krate("foo"), + "2.0.0", vec![])) + .with_method(Method::Put))); + assert!(json.errors[0].detail.contains("another user"), + "{:?}", json.errors); +} + +// Test trying to publish a krate we do own (but only because of teams) +#[test] +fn publish_owned() { + let (_b, app, middle) = ::app(); + let mut req = ::new_req(app.clone(), "foo", "1.0.0"); + ::mock_user(&mut req, mock_user_on_x_and_y()); + ::mock_crate(&mut req, ::krate("foo")); + + let body = body_for_team_x(); + ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners") + .with_method(Method::Put) + .with_body(body.as_bytes()))); + + ::mock_user(&mut req, mock_user_on_only_x()); + ok_resp!(middle.call(req.with_path("/api/v1/crates/new") + .with_body(&::new_req_body(::krate("foo"), + "2.0.0", vec![])) + .with_method(Method::Put))); +} + +// Test trying to change owners (when only on an owning team) +#[test] +fn add_owners_as_team_owner() { + let (_b, app, middle) = ::app(); + let mut req = ::new_req(app.clone(), "foo", "1.0.0"); + ::mock_user(&mut req, mock_user_on_x_and_y()); + ::mock_crate(&mut req, ::krate("foo")); + + let body = body_for_team_x(); + ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners") + .with_method(Method::Put) + .with_body(body.as_bytes()))); + + ::mock_user(&mut req, mock_user_on_only_x()); + let body = r#"{"users":["FlashCat"]}"#; // User doesn't matter + let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners") + .with_method(Method::Put) + .with_body(body.as_bytes()))); + assert!(json.errors[0].detail.contains("don't have permission"), + "{:?}", json.errors); +} + diff --git a/src/user/mod.rs b/src/user/mod.rs index 05c70c495b8..32750866d64 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -1,14 +1,10 @@ use std::collections::HashMap; -use std::str; use conduit::{Request, Response}; use conduit_cookie::{RequestSession}; -use curl::http; -use oauth2::Authorization; use pg::rows::Row; use pg::types::Slice; use rand::{thread_rng, Rng}; -use rustc_serialize::json; use {Model, Version}; use app::RequestApp; @@ -17,6 +13,7 @@ use krate::{Crate, EncodableCrate}; use util::errors::NotFound; use util::{RequestUtils, CargoResult, internal, ChainError, human}; use version::EncodableVersion; +use http; pub use self::middleware::{Middleware, RequestUser}; @@ -166,22 +163,6 @@ pub fn github_access_token(req: &mut Request) -> CargoResult { } } - // Fetch the access token from github using the code we just got - let token = match req.app().github.exchange(code.clone()) { - Ok(token) => token, - Err(s) => return Err(human(s)), - }; - - let resp = try!(http::handle().get("https://api.github.com/user") - .header("Accept", "application/vnd.github.v3+json") - .header("User-Agent", "hello!") - .auth_with(&token) - .exec()); - if resp.get_code() != 200 { - return Err(internal(format!("didn't get a 200 result from github: {}", - resp))) - } - #[derive(RustcDecodable)] struct GithubUser { email: Option, @@ -189,12 +170,15 @@ pub fn github_access_token(req: &mut Request) -> CargoResult { login: String, avatar_url: Option, } - let json = try!(str::from_utf8(resp.get_body()).ok().chain_error(||{ - internal("github didn't send a utf8-response") - })); - let ghuser: GithubUser = try!(json::decode(json).chain_error(|| { - internal("github didn't send a valid json response") - })); + + // Fetch the access token from github using the code we just got + let token = match req.app().github.exchange(code.clone()) { + Ok(token) => token, + Err(s) => return Err(human(s)), + }; + + let resp = try!(http::github(req.app(), "/user", &token)); + let ghuser: GithubUser = try!(http::parse_github_response(resp)); // Into the database! let api_token = User::new_api_token(); diff --git a/src/version.rs b/src/version.rs index 10eea080dce..58008d1d5fd 100644 --- a/src/version.rs +++ b/src/version.rs @@ -18,6 +18,7 @@ use download::{VersionDownload, EncodableVersionDownload}; use git; use upload; use user::RequestUser; +use owner::{rights, Rights}; use util::{RequestUtils, CargoResult, ChainError, internal, human}; #[derive(Clone)] @@ -343,7 +344,7 @@ fn modify_yank(req: &mut Request, yanked: bool) -> CargoResult { let user = try!(req.user()); let tx = try!(req.tx()); let owners = try!(krate.owners(tx)); - if !owners.iter().any(|u| u.id == user.id) { + if try!(rights(req.app(), &owners, &user)) < Rights::Publish { return Err(human("must already be an owner to yank or unyank")) }