use secrecy::{ExposeSecret, SecretString};
use crate::types::ValidationError;
#[derive(Clone)]
pub struct UploadToken(SecretString);
impl UploadToken {
pub fn new(token: impl Into<String>) -> Self {
Self(token.into().into())
}
pub fn try_new(token: impl Into<String>) -> Result<Self, ValidationError> {
let token = Self::new(token);
token.validate()?;
Ok(token)
}
pub fn validate(&self) -> Result<(), ValidationError> {
let value = self.expose();
if value.is_empty() || reqwest::header::HeaderValue::from_str(value).is_err() {
Err(ValidationError::InvalidUploadToken)
} else {
Ok(())
}
}
pub(crate) fn expose(&self) -> &str {
self.0.expose_secret()
}
pub(crate) fn header_value(&self) -> reqwest::header::HeaderValue {
let mut header = reqwest::header::HeaderValue::try_from(self.expose())
.expect("validated upload tokens are valid HTTP header values");
header.set_sensitive(true);
header
}
}
impl std::fmt::Debug for UploadToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("UploadToken([REDACTED])")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_does_not_leak_token() {
let token = UploadToken::new("super-secret-value");
let debug = format!("{:?}", token);
assert!(!debug.contains("super-secret-value"));
assert!(debug.contains("REDACTED"));
}
#[test]
fn expose_returns_original_value() {
let token = UploadToken::new("my-token");
assert_eq!(token.expose(), "my-token");
}
#[test]
fn try_new_rejects_invalid_header_values() {
assert!(UploadToken::try_new("").is_err());
assert!(UploadToken::try_new("bad\nheader").is_err());
}
#[test]
fn request_header_is_marked_sensitive() {
let token = UploadToken::try_new("my-token").unwrap();
assert!(token.header_value().is_sensitive());
}
}