use std::fmt;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::Scope;
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Message {
pub message: String,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DistributionCredentialsList {
#[serde(default)]
pub distribution_credentials: Vec<DistributionCredentialsEntry>,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DistributionCredentialsEntry {
pub member: Member,
pub distribution_credentials: DistributionCredentials,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Member {
pub member_id: Uuid,
pub email: String,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DistributionCredentials {
pub distribution_credentials_id: Uuid,
pub provider: String,
pub comment: Option<String>,
#[serde(default)]
pub scopes: Vec<Scope>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
pub created: String,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreatedDistributionCredentials {
pub distribution_credentials_id: Uuid,
pub provider: String,
pub username: String,
pub secret: SecretString,
pub comment: Option<String>,
#[serde(default)]
pub scopes: Vec<Scope>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
pub created: String,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SecretString(String);
impl SecretString {
pub fn new(secret: impl Into<String>) -> Self {
Self(secret.into())
}
pub fn expose_secret(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl fmt::Debug for SecretString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("***")
}
}
impl From<String> for SecretString {
fn from(secret: String) -> Self {
Self(secret)
}
}
#[cfg(test)]
mod tests {
use super::{
CreatedDistributionCredentials, DistributionCredentialsEntry, DistributionCredentialsList,
Message, Scope, SecretString,
};
#[test]
fn deserializes_documented_response() {
let json = serde_json::json!({
"distribution_credentials": [{
"member": {
"member_id": "3376abcd-8e5e-49d3-92d4-876d3a4f0363",
"email": "email@example.com"
},
"distribution_credentials": {
"distribution_credentials_id": "8b36cfd0-472f-4a21-833f-2d6343c3a2f3",
"provider": "quay",
"scopes": ["self-hosted:product:api", "self-hosted:product:engine"],
"created": "2023-06-28T15:36:59.609841Z",
"comment": "My Self-Hosted Distribution Credentials"
}
}]
});
let list: DistributionCredentialsList = serde_json::from_value(json).unwrap();
assert_eq!(list.distribution_credentials.len(), 1);
let entry = &list.distribution_credentials[0];
assert_eq!(entry.member.email, "email@example.com");
assert_eq!(entry.distribution_credentials.provider, "quay");
assert_eq!(
entry.distribution_credentials.scopes,
[Scope::Api, Scope::Engine]
);
assert_eq!(
entry.distribution_credentials.comment.as_deref(),
Some("My Self-Hosted Distribution Credentials")
);
assert!(entry.distribution_credentials.tags.is_empty());
}
#[test]
fn deserializes_single_entry_response() {
let json = serde_json::json!({
"member": {
"member_id": "3376abcd-8e5e-49d3-92d4-876d3a4f0363",
"email": "email@example.com"
},
"distribution_credentials": {
"distribution_credentials_id": "8b36cfd0-472f-4a21-833f-2d6343c3a2f3",
"provider": "quay",
"scopes": ["self-hosted:product:api"],
"tags": ["prod", "us-east"],
"created": "2023-06-28T15:36:59.609841Z",
"comment": "primary"
}
});
let entry: DistributionCredentialsEntry = serde_json::from_value(json).unwrap();
assert_eq!(entry.member.email, "email@example.com");
assert_eq!(
entry
.distribution_credentials
.distribution_credentials_id
.to_string(),
"8b36cfd0-472f-4a21-833f-2d6343c3a2f3"
);
assert_eq!(
entry.distribution_credentials.comment.as_deref(),
Some("primary")
);
assert_eq!(entry.distribution_credentials.tags, ["prod", "us-east"]);
}
#[test]
fn deserializes_response_without_comment() {
let json = serde_json::json!({
"distribution_credentials": [{
"member": { "member_id": "3376abcd-8e5e-49d3-92d4-876d3a4f0363", "email": "e@x.com" },
"distribution_credentials": {
"distribution_credentials_id": "8b36cfd0-472f-4a21-833f-2d6343c3a2f3",
"provider": "quay",
"scopes": [],
"created": "2023-06-28T15:36:59.609841Z"
}
}]
});
let list: DistributionCredentialsList = serde_json::from_value(json).unwrap();
assert!(list.distribution_credentials[0]
.distribution_credentials
.comment
.is_none());
}
#[test]
fn deserializes_create_response_with_one_time_secret() {
let json = serde_json::json!({
"distribution_credentials_id": "8b36cfd0-472f-4a21-833f-2d6343c3a2f3",
"provider": "quay",
"username": "deepgram+8b36cfd0",
"secret": "s3cr3t-registry-password",
"comment": "created from the rust sdk",
"scopes": ["self-hosted:product:api", "onprem:product:api"],
"created": "2023-06-28T15:36:59.609841Z"
});
let created: CreatedDistributionCredentials = serde_json::from_value(json).unwrap();
assert_eq!(
created.distribution_credentials_id.to_string(),
"8b36cfd0-472f-4a21-833f-2d6343c3a2f3"
);
assert_eq!(created.provider, "quay");
assert_eq!(created.username, "deepgram+8b36cfd0");
assert_eq!(created.secret.expose_secret(), "s3cr3t-registry-password");
assert_eq!(
created.comment.as_deref(),
Some("created from the rust sdk")
);
assert_eq!(created.scopes, [Scope::Api, Scope::Api]);
assert!(created.tags.is_empty());
let debug = format!("{created:?}");
assert!(
!debug.contains("s3cr3t-registry-password"),
"secret leaked into Debug: {debug}"
);
assert!(debug.contains("***"), "{debug}");
let json = serde_json::to_value(&created).unwrap();
assert_eq!(json["secret"], "s3cr3t-registry-password");
assert!(json.get("tags").is_none(), "empty tags are omitted");
}
#[test]
fn deserializes_create_response_with_tags() {
let json = serde_json::json!({
"distribution_credentials_id": "8b36cfd0-472f-4a21-833f-2d6343c3a2f3",
"provider": "quay",
"username": "u",
"secret": "s",
"comment": "",
"scopes": ["self-hosted:product:api", "self-hosted:product:engine"],
"tags": ["staging"],
"created": "2023-06-28T15:36:59.609841Z"
});
let created: CreatedDistributionCredentials = serde_json::from_value(json).unwrap();
assert_eq!(created.tags, ["staging"]);
assert_eq!(created.scopes, [Scope::Api, Scope::Engine]);
}
#[test]
fn deserializes_create_response_for_the_products_shorthand() {
let products = [
"api",
"engine",
"license-proxy",
"billing",
"dgtools",
"hotpepper",
"metrics-server",
];
let scopes: Vec<String> = products
.iter()
.flat_map(|p| {
[
format!("self-hosted:product:{p}"),
format!("onprem:product:{p}"),
]
})
.collect();
let json = serde_json::json!({
"distribution_credentials_id": "8b36cfd0-472f-4a21-833f-2d6343c3a2f3",
"provider": "quay",
"username": "deepgram+8b36cfd0-472f-4a21-833f-2d6343c3a2f3",
"secret": "s3cr3t",
"comment": "every product",
"scopes": scopes,
"created": "2023-06-28T15:36:59.609841Z"
});
let created: CreatedDistributionCredentials = serde_json::from_value(json).unwrap();
assert_eq!(created.scopes.len(), 14);
assert!(
!created
.scopes
.iter()
.any(|s| matches!(s, Scope::Unknown(_) | Scope::Products)),
"every expanded scope resolves to a named product variant: {:?}",
created.scopes
);
for scope in [
Scope::Api,
Scope::Engine,
Scope::LicenseProxy,
Scope::Billing,
Scope::Dgtools,
Scope::Hotpepper,
Scope::MetricsServer,
] {
assert_eq!(created.scopes.iter().filter(|s| **s == scope).count(), 2);
}
}
#[test]
fn create_response_without_a_secret_is_an_error() {
let json = serde_json::json!({
"distribution_credentials_id": "8b36cfd0-472f-4a21-833f-2d6343c3a2f3",
"provider": "quay",
"comment": "",
"scopes": [],
"created": "2023-06-28T15:36:59.609841Z"
});
assert!(serde_json::from_value::<CreatedDistributionCredentials>(json).is_err());
}
#[test]
fn deserializes_response_with_an_unknown_scope() {
let json = serde_json::json!({
"member": { "member_id": "3376abcd-8e5e-49d3-92d4-876d3a4f0363", "email": "e@x.com" },
"distribution_credentials": {
"distribution_credentials_id": "8b36cfd0-472f-4a21-833f-2d6343c3a2f3",
"provider": "quay",
"scopes": ["self-hosted:products", "self-hosted:product:not-invented-yet"],
"created": "2023-06-28T15:36:59.609841Z"
}
});
let entry: DistributionCredentialsEntry = serde_json::from_value(json).unwrap();
assert_eq!(
entry.distribution_credentials.scopes,
[
Scope::Products,
Scope::Unknown("self-hosted:product:not-invented-yet".to_string())
]
);
let round_tripped = serde_json::to_value(&entry).unwrap();
assert_eq!(
round_tripped["distribution_credentials"]["scopes"],
serde_json::json!([
"self-hosted:products",
"self-hosted:product:not-invented-yet"
])
);
}
#[test]
fn deserializes_delete_message() {
let json = serde_json::json!({
"message": "Successfully deleted the distribution credentials!"
});
let message: Message = serde_json::from_value(json).unwrap();
assert_eq!(
message.message,
"Successfully deleted the distribution credentials!"
);
}
#[test]
fn secret_string_redacts_debug() {
let secret = SecretString::new("hunter2");
assert_eq!(format!("{secret:?}"), "***");
assert_eq!(secret.expose_secret(), "hunter2");
assert_eq!(secret.clone().into_inner(), "hunter2");
assert_eq!(SecretString::from("hunter2".to_string()), secret);
}
}