diff --git a/src/models/token.rs b/src/models/token.rs index 6a1eacb5514..1b88810a4dd 100644 --- a/src/models/token.rs +++ b/src/models/token.rs @@ -27,10 +27,8 @@ pub struct ApiToken { #[serde(skip)] pub revoked: bool, /// `None` or a list of crate scope patterns (see RFC #2947) - #[serde(skip)] pub crate_scopes: Option>, /// A list of endpoint scopes or `None` for the `legacy` endpoint scope (see RFC #2947) - #[serde(skip)] pub endpoint_scopes: Option>, } diff --git a/src/models/token/scopes.rs b/src/models/token/scopes.rs index 270f0ea14f5..2a4127c39ca 100644 --- a/src/models/token/scopes.rs +++ b/src/models/token/scopes.rs @@ -5,8 +5,9 @@ use diesel::serialize::{self, IsNull, Output, ToSql}; use diesel::sql_types::Text; use std::io::Write; -#[derive(Clone, Copy, Debug, PartialEq, Eq, AsExpression)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, AsExpression, Serialize)] #[diesel(sql_type = Text)] +#[serde(rename_all = "kebab-case")] pub enum EndpointScope { PublishNew, PublishUpdate, @@ -53,7 +54,8 @@ impl FromSql for EndpointScope { } } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(transparent)] pub struct CrateScope { pattern: String, } @@ -125,6 +127,34 @@ impl CrateScope { mod tests { use super::*; + #[test] + fn endpoint_scope_serialization() { + fn assert(scope: EndpointScope, expected: &str) { + assert_ok_eq!(serde_json::to_string(&scope), expected); + } + + assert(EndpointScope::ChangeOwners, "\"change-owners\""); + assert(EndpointScope::PublishNew, "\"publish-new\""); + assert(EndpointScope::PublishUpdate, "\"publish-update\""); + assert(EndpointScope::Yank, "\"yank\""); + } + + #[test] + fn crate_scope_serialization() { + fn assert(scope: &str, expected: &str) { + let scope = assert_ok!(CrateScope::try_from(scope)); + assert_ok_eq!(serde_json::to_string(&scope), expected); + } + + assert("foo", "\"foo\""); + assert("foo*", "\"foo*\""); + assert("f*", "\"f*\""); + assert("*", "\"*\""); + assert("foo-bar", "\"foo-bar\""); + assert("foo_bar", "\"foo_bar\""); + assert("FooBar", "\"FooBar\""); + } + #[test] fn crate_scope_validation() { assert_ok!(CrateScope::try_from("foo")); diff --git a/src/tests/routes/me/tokens/list.rs b/src/tests/routes/me/tokens/list.rs index 744f381c61f..ba7062ed4e6 100644 --- a/src/tests/routes/me/tokens/list.rs +++ b/src/tests/routes/me/tokens/list.rs @@ -1,17 +1,8 @@ -use crate::routes::me::tokens::delete::RevokedResponse; +use crate::util::insta::{self, assert_yaml_snapshot}; use crate::util::{RequestHelper, TestApp}; +use cargo_registry::models::token::{CrateScope, EndpointScope}; use cargo_registry::models::ApiToken; -use std::collections::HashSet; - -#[derive(Deserialize)] -struct DecodableApiToken { - name: String, -} - -#[derive(Deserialize)] -struct ListResponse { - api_tokens: Vec, -} +use http::StatusCode; #[test] fn list_logged_out() { @@ -28,33 +19,40 @@ fn list_with_api_token_is_forbidden() { #[test] fn list_empty() { let (_, _, user) = TestApp::init().with_user(); - let json: ListResponse = user.get("/api/v1/me/tokens").good(); - assert_eq!(json.api_tokens.len(), 0); + let response = user.get::<()>("/api/v1/me/tokens"); + assert_eq!(response.status(), StatusCode::OK); + let json = response.into_json(); + let response_tokens = json["api_tokens"].as_array().unwrap(); + assert_eq!(response_tokens.len(), 0); } #[test] fn list_tokens() { let (app, _, user) = TestApp::init().with_user(); let id = user.as_model().id; - let tokens = app.db(|conn| { + app.db(|conn| { vec![ assert_ok!(ApiToken::insert(conn, id, "bar")), - assert_ok!(ApiToken::insert(conn, id, "baz")), + assert_ok!(ApiToken::insert_with_scopes( + conn, + id, + "baz", + Some(vec![ + CrateScope::try_from("serde").unwrap(), + CrateScope::try_from("serde-*").unwrap() + ]), + Some(vec![EndpointScope::PublishUpdate]) + )), ] }); - let json: ListResponse = user.get("/api/v1/me/tokens").good(); - assert_eq!(json.api_tokens.len(), tokens.len()); - assert_eq!( - json.api_tokens - .into_iter() - .map(|t| t.name) - .collect::>(), - tokens - .into_iter() - .map(|t| t.model.name) - .collect::>() - ); + let response = user.get::<()>("/api/v1/me/tokens"); + assert_eq!(response.status(), StatusCode::OK); + assert_yaml_snapshot!(response.into_json(), { + ".api_tokens[].id" => insta::any_id_redaction(), + ".api_tokens[].created_at" => "[datetime]", + ".api_tokens[].last_used_at" => "[datetime]", + }); } #[test] @@ -69,19 +67,21 @@ fn list_tokens_exclude_revoked() { }); // List tokens expecting them all to be there. - let json: ListResponse = user.get("/api/v1/me/tokens").good(); - assert_eq!(json.api_tokens.len(), tokens.len()); + let response = user.get::<()>("/api/v1/me/tokens"); + assert_eq!(response.status(), StatusCode::OK); + let json = response.into_json(); + let response_tokens = json["api_tokens"].as_array().unwrap(); + assert_eq!(response_tokens.len(), 2); // Revoke the first token. - let _json: RevokedResponse = user - .delete(&format!("/api/v1/me/tokens/{}", tokens[0].model.id)) - .good(); + let response = user.delete::<()>(&format!("/api/v1/me/tokens/{}", tokens[0].model.id)); + assert_eq!(response.status(), StatusCode::OK); // Check that we now have one less token being listed. - let json: ListResponse = user.get("/api/v1/me/tokens").good(); - assert_eq!(json.api_tokens.len(), tokens.len() - 1); - assert!(!json - .api_tokens - .iter() - .any(|token| token.name == tokens[0].model.name)); + let response = user.get::<()>("/api/v1/me/tokens"); + assert_eq!(response.status(), StatusCode::OK); + let json = response.into_json(); + let response_tokens = json["api_tokens"].as_array().unwrap(); + assert_eq!(response_tokens.len(), 1); + assert_eq!(response_tokens[0]["name"], json!("baz")); } diff --git a/src/tests/routes/me/tokens/snapshots/all__routes__me__tokens__list__list_tokens.snap b/src/tests/routes/me/tokens/snapshots/all__routes__me__tokens__list__list_tokens.snap new file mode 100644 index 00000000000..88f065c693f --- /dev/null +++ b/src/tests/routes/me/tokens/snapshots/all__routes__me__tokens__list__list_tokens.snap @@ -0,0 +1,21 @@ +--- +source: src/tests/routes/me/tokens/list.rs +expression: response.into_json() +--- +api_tokens: + - crate_scopes: ~ + created_at: "[datetime]" + endpoint_scopes: ~ + id: "[id]" + last_used_at: "[datetime]" + name: bar + - crate_scopes: + - serde + - serde-* + created_at: "[datetime]" + endpoint_scopes: + - publish-update + id: "[id]" + last_used_at: "[datetime]" + name: baz +