use std::fmt::{Debug, Display, Formatter};
use super::error::TelegramError;
#[derive(Clone, PartialEq, Eq)]
pub struct BotToken(String);
impl BotToken {
pub fn new(token: impl Into<String>) -> Result<Self, TelegramError> {
let token: String = token.into().trim().to_string();
if token.is_empty() {
return Err(TelegramError::InvalidToken(String::from(
"token is empty (or only whitespace)",
)));
}
let Some((bot_id, secret)) = token.split_once(':') else {
return Err(TelegramError::InvalidToken(String::from(
"token is missing the `<bot_id>:<secret>` separator",
)));
};
if bot_id.is_empty() || !bot_id.chars().all(|c: char| c.is_ascii_digit()) {
return Err(TelegramError::InvalidToken(String::from(
"token's bot id is not numeric",
)));
}
if secret.is_empty() {
return Err(TelegramError::InvalidToken(String::from(
"token's secret is empty",
)));
}
Ok(Self(token))
}
pub(crate) fn expose(&self) -> &str {
&self.0
}
}
impl Debug for BotToken {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "BotToken(***)")
}
}
impl Display for BotToken {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "***")
}
}
#[cfg(test)]
mod tests {
use super::BotToken;
use crate::telegram::error::TelegramError;
const VALID: &str = "123456789:AAFNpHzr6wq4YimAMwIjqVrFU8TO5kcayEI";
#[test]
fn valid_token_is_accepted() {
let token: BotToken = BotToken::new(VALID).expect("valid token should be accepted");
let expected: &str = VALID;
let actual: &str = token.expose();
assert_eq!(expected, actual);
}
#[test]
fn surrounding_whitespace_is_trimmed() {
let token: BotToken =
BotToken::new(format!(" {VALID}\n")).expect("valid token should be accepted");
let expected: &str = VALID;
let actual: &str = token.expose();
assert_eq!(expected, actual);
}
#[test]
fn empty_token_is_rejected() {
let error: TelegramError = BotToken::new("").expect_err("empty token should be rejected");
assert!(matches!(error, TelegramError::InvalidToken(_)));
}
#[test]
fn whitespace_only_token_is_rejected() {
let error: TelegramError =
BotToken::new(" \n\t").expect_err("blank token should be rejected");
assert!(matches!(error, TelegramError::InvalidToken(_)));
}
#[test]
fn token_without_separator_is_rejected() {
let error: TelegramError =
BotToken::new("nosemicolonhere").expect_err("malformed token should be rejected");
assert!(matches!(error, TelegramError::InvalidToken(_)));
}
#[test]
fn token_with_non_numeric_bot_id_is_rejected() {
let error: TelegramError =
BotToken::new("abcdef:AAFsecret").expect_err("malformed token should be rejected");
assert!(matches!(error, TelegramError::InvalidToken(_)));
}
#[test]
fn token_with_empty_secret_is_rejected() {
let error: TelegramError =
BotToken::new("123456789:").expect_err("malformed token should be rejected");
assert!(matches!(error, TelegramError::InvalidToken(_)));
}
#[test]
fn debug_does_not_leak_the_token() {
let token: BotToken = BotToken::new(VALID).expect("valid token should be accepted");
let expected: String = String::from("BotToken(***)");
let actual: String = format!("{token:?}");
assert_eq!(expected, actual);
assert!(!actual.contains("AAFNpHzr"));
}
#[test]
fn display_does_not_leak_the_token() {
let token: BotToken = BotToken::new(VALID).expect("valid token should be accepted");
let expected: String = String::from("***");
let actual: String = format!("{token}");
assert_eq!(expected, actual);
assert!(!actual.contains("AAFNpHzr"));
}
#[test]
fn debug_of_a_containing_struct_does_not_leak_the_token() {
#[derive(Debug)]
struct Holder {
#[expect(dead_code, reason = "only exercised through the derived Debug impl")]
token: BotToken,
}
let holder: Holder = Holder {
token: BotToken::new(VALID).expect("valid token should be accepted"),
};
let actual: String = format!("{holder:?}");
assert!(!actual.contains("AAFNpHzr"));
assert!(actual.contains("***"));
}
}