webserver_base/telegram/
token.rs1use std::fmt::{Debug, Display, Formatter};
2
3use super::error::TelegramError;
4
5#[derive(Clone, PartialEq, Eq)]
16pub struct BotToken(String);
17
18impl BotToken {
19 pub fn new(token: impl Into<String>) -> Result<Self, TelegramError> {
29 let token: String = token.into().trim().to_string();
30
31 if token.is_empty() {
32 return Err(TelegramError::InvalidToken(String::from(
33 "token is empty (or only whitespace)",
34 )));
35 }
36
37 let Some((bot_id, secret)) = token.split_once(':') else {
41 return Err(TelegramError::InvalidToken(String::from(
42 "token is missing the `<bot_id>:<secret>` separator",
43 )));
44 };
45
46 if bot_id.is_empty() || !bot_id.chars().all(|c: char| c.is_ascii_digit()) {
47 return Err(TelegramError::InvalidToken(String::from(
48 "token's bot id is not numeric",
49 )));
50 }
51
52 if secret.is_empty() {
53 return Err(TelegramError::InvalidToken(String::from(
54 "token's secret is empty",
55 )));
56 }
57
58 Ok(Self(token))
59 }
60
61 pub(crate) fn expose(&self) -> &str {
66 &self.0
67 }
68}
69
70impl Debug for BotToken {
71 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
72 write!(f, "BotToken(***)")
73 }
74}
75
76impl Display for BotToken {
77 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
78 write!(f, "***")
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::BotToken;
85 use crate::telegram::error::TelegramError;
86
87 const VALID: &str = "123456789:AAFNpHzr6wq4YimAMwIjqVrFU8TO5kcayEI";
88
89 #[test]
90 fn valid_token_is_accepted() {
91 let token: BotToken = BotToken::new(VALID).expect("valid token should be accepted");
92
93 let expected: &str = VALID;
94 let actual: &str = token.expose();
95 assert_eq!(expected, actual);
96 }
97
98 #[test]
99 fn surrounding_whitespace_is_trimmed() {
100 let token: BotToken =
101 BotToken::new(format!(" {VALID}\n")).expect("valid token should be accepted");
102
103 let expected: &str = VALID;
104 let actual: &str = token.expose();
105 assert_eq!(expected, actual);
106 }
107
108 #[test]
109 fn empty_token_is_rejected() {
110 let error: TelegramError = BotToken::new("").expect_err("empty token should be rejected");
111
112 assert!(matches!(error, TelegramError::InvalidToken(_)));
113 }
114
115 #[test]
116 fn whitespace_only_token_is_rejected() {
117 let error: TelegramError =
118 BotToken::new(" \n\t").expect_err("blank token should be rejected");
119
120 assert!(matches!(error, TelegramError::InvalidToken(_)));
121 }
122
123 #[test]
124 fn token_without_separator_is_rejected() {
125 let error: TelegramError =
126 BotToken::new("nosemicolonhere").expect_err("malformed token should be rejected");
127
128 assert!(matches!(error, TelegramError::InvalidToken(_)));
129 }
130
131 #[test]
132 fn token_with_non_numeric_bot_id_is_rejected() {
133 let error: TelegramError =
134 BotToken::new("abcdef:AAFsecret").expect_err("malformed token should be rejected");
135
136 assert!(matches!(error, TelegramError::InvalidToken(_)));
137 }
138
139 #[test]
140 fn token_with_empty_secret_is_rejected() {
141 let error: TelegramError =
142 BotToken::new("123456789:").expect_err("malformed token should be rejected");
143
144 assert!(matches!(error, TelegramError::InvalidToken(_)));
145 }
146
147 #[test]
148 fn debug_does_not_leak_the_token() {
149 let token: BotToken = BotToken::new(VALID).expect("valid token should be accepted");
150
151 let expected: String = String::from("BotToken(***)");
152 let actual: String = format!("{token:?}");
153 assert_eq!(expected, actual);
154 assert!(!actual.contains("AAFNpHzr"));
155 }
156
157 #[test]
158 fn display_does_not_leak_the_token() {
159 let token: BotToken = BotToken::new(VALID).expect("valid token should be accepted");
160
161 let expected: String = String::from("***");
162 let actual: String = format!("{token}");
163 assert_eq!(expected, actual);
164 assert!(!actual.contains("AAFNpHzr"));
165 }
166
167 #[test]
168 fn debug_of_a_containing_struct_does_not_leak_the_token() {
169 #[derive(Debug)]
170 struct Holder {
171 #[expect(dead_code, reason = "only exercised through the derived Debug impl")]
172 token: BotToken,
173 }
174
175 let holder: Holder = Holder {
176 token: BotToken::new(VALID).expect("valid token should be accepted"),
177 };
178
179 let actual: String = format!("{holder:?}");
180 assert!(!actual.contains("AAFNpHzr"));
181 assert!(actual.contains("***"));
182 }
183}