Skip to main content

moq_token/
claims.rs

1use serde::{Deserialize, Serialize};
2use serde_with::{OneOrMany, TimestampSeconds, formats::PreferMany, serde_as};
3
4#[serde_with::skip_serializing_none]
5#[serde_as]
6#[derive(Debug, Serialize, Deserialize, Default, Clone)]
7#[serde(default)]
8pub struct Claims {
9	/// The root for the publish/subscribe options below.
10	/// It's mostly for compression and is optional, defaulting to the empty string.
11	#[serde(default, rename = "root", skip_serializing_if = "String::is_empty")]
12	pub root: String,
13
14	/// If specified, the user can publish any matching broadcasts.
15	/// If not specified, the user will not publish any broadcasts.
16	#[serde(default, rename = "put", skip_serializing_if = "Vec::is_empty")]
17	#[serde_as(as = "OneOrMany<_, PreferMany>")]
18	pub publish: Vec<String>,
19
20	/// If specified, the user can subscribe to any matching broadcasts.
21	/// If not specified, the user will not receive announcements and cannot subscribe to any broadcasts.
22	// NOTE: This can't be renamed to "sub" because that's a reserved JWT field.
23	#[serde(default, rename = "get", skip_serializing_if = "Vec::is_empty")]
24	#[serde_as(as = "OneOrMany<_, PreferMany>")]
25	pub subscribe: Vec<String>,
26
27	/// The expiration time of the token as a unix timestamp.
28	#[serde(rename = "exp")]
29	#[serde_as(as = "Option<TimestampSeconds<i64>>")]
30	pub expires: Option<std::time::SystemTime>,
31
32	/// The issued time of the token as a unix timestamp.
33	#[serde(rename = "iat")]
34	#[serde_as(as = "Option<TimestampSeconds<i64>>")]
35	pub issued: Option<std::time::SystemTime>,
36}
37
38impl Claims {
39	pub fn validate(&self) -> crate::Result<()> {
40		if self.publish.is_empty() && self.subscribe.is_empty() {
41			return Err(crate::Error::UselessToken);
42		}
43
44		Ok(())
45	}
46}
47
48#[cfg(test)]
49mod tests {
50	use super::*;
51
52	use std::time::{Duration, SystemTime};
53
54	fn create_test_claims() -> Claims {
55		Claims {
56			root: "test-path".to_string(),
57			publish: vec!["test-pub".into()],
58			subscribe: vec!["test-sub".into()],
59			expires: Some(SystemTime::now() + Duration::from_secs(3600)),
60			issued: Some(SystemTime::now()),
61		}
62	}
63
64	#[test]
65	fn test_claims_validation_success() {
66		let claims = create_test_claims();
67		assert!(claims.validate().is_ok());
68	}
69
70	#[test]
71	fn test_claims_validation_no_publish_or_subscribe() {
72		let claims = Claims {
73			root: "test-path".to_string(),
74			publish: vec![],
75			subscribe: vec![],
76			expires: None,
77			issued: None,
78		};
79
80		let result = claims.validate();
81		assert!(result.is_err());
82		assert!(
83			result
84				.unwrap_err()
85				.to_string()
86				.contains("no publish or subscribe allowed; token is useless")
87		);
88	}
89
90	#[test]
91	fn test_claims_validation_only_publish() {
92		let claims = Claims {
93			root: "test-path".to_string(),
94			publish: vec!["test-pub".into()],
95			subscribe: vec![],
96			expires: None,
97			issued: None,
98		};
99
100		assert!(claims.validate().is_ok());
101	}
102
103	#[test]
104	fn test_claims_validation_only_subscribe() {
105		let claims = Claims {
106			root: "test-path".to_string(),
107			publish: vec![],
108			subscribe: vec!["test-sub".into()],
109			expires: None,
110			issued: None,
111		};
112
113		assert!(claims.validate().is_ok());
114	}
115
116	#[test]
117	fn test_claims_validation_path_not_prefix_relative_publish() {
118		let claims = Claims {
119			root: "test-path".to_string(),        // no trailing slash
120			publish: vec!["relative-pub".into()], // relative path without leading slash
121			subscribe: vec![],
122			expires: None,
123			issued: None,
124		};
125
126		let result = claims.validate();
127		assert!(result.is_ok()); // Now passes because slashes are implicitly added
128	}
129
130	#[test]
131	fn test_claims_validation_path_not_prefix_relative_subscribe() {
132		let claims = Claims {
133			root: "test-path".to_string(), // no trailing slash
134			publish: vec![],
135			subscribe: vec!["relative-sub".into()], // relative path without leading slash
136			expires: None,
137			issued: None,
138		};
139
140		let result = claims.validate();
141		assert!(result.is_ok()); // Now passes because slashes are implicitly added
142	}
143
144	#[test]
145	fn test_claims_validation_path_not_prefix_absolute_publish() {
146		let claims = Claims {
147			root: "test-path".to_string(),         // no trailing slash
148			publish: vec!["/absolute-pub".into()], // absolute path with leading slash
149			subscribe: vec![],
150			expires: None,
151			issued: None,
152		};
153
154		assert!(claims.validate().is_ok());
155	}
156
157	#[test]
158	fn test_claims_validation_path_not_prefix_absolute_subscribe() {
159		let claims = Claims {
160			root: "test-path".to_string(), // no trailing slash
161			publish: vec![],
162			subscribe: vec!["/absolute-sub".into()], // absolute path with leading slash
163			expires: None,
164			issued: None,
165		};
166
167		assert!(claims.validate().is_ok());
168	}
169
170	#[test]
171	fn test_claims_validation_path_not_prefix_empty_publish() {
172		let claims = Claims {
173			root: "test-path".to_string(), // no trailing slash
174			publish: vec!["".into()],      // empty string
175			subscribe: vec![],
176			expires: None,
177			issued: None,
178		};
179
180		assert!(claims.validate().is_ok());
181	}
182
183	#[test]
184	fn test_claims_validation_path_not_prefix_empty_subscribe() {
185		let claims = Claims {
186			root: "test-path".to_string(), // no trailing slash
187			publish: vec![],
188			subscribe: vec!["".into()], // empty string
189			expires: None,
190			issued: None,
191		};
192
193		assert!(claims.validate().is_ok());
194	}
195
196	#[test]
197	fn test_claims_validation_path_is_prefix() {
198		let claims = Claims {
199			root: "test-path".to_string(),          // with trailing slash
200			publish: vec!["relative-pub".into()],   // relative path is ok when path is prefix
201			subscribe: vec!["relative-sub".into()], // relative path is ok when path is prefix
202			expires: None,
203			issued: None,
204		};
205
206		assert!(claims.validate().is_ok());
207	}
208
209	#[test]
210	fn test_claims_validation_empty_path() {
211		let claims = Claims {
212			root: "".to_string(), // empty path
213			publish: vec!["test-pub".into()],
214			subscribe: vec![],
215			expires: None,
216			issued: None,
217		};
218
219		assert!(claims.validate().is_ok());
220	}
221
222	#[test]
223	fn test_claims_serde() {
224		let claims = create_test_claims();
225		let json = serde_json::to_string(&claims).unwrap();
226		let deserialized: Claims = serde_json::from_str(&json).unwrap();
227
228		assert_eq!(deserialized.root, claims.root);
229		assert_eq!(deserialized.publish, claims.publish);
230		assert_eq!(deserialized.subscribe, claims.subscribe);
231	}
232
233	#[test]
234	fn test_claims_default() {
235		let claims = Claims::default();
236		assert_eq!(claims.root, "");
237		assert!(claims.publish.is_empty());
238		assert!(claims.subscribe.is_empty());
239		assert_eq!(claims.expires, None);
240		assert_eq!(claims.issued, None);
241	}
242
243	#[test]
244	fn test_deserialize_string_as_vec() {
245		let json = r#"{
246			"root": "test",
247			"put": "single-publish",
248			"get": "single-subscribe"
249		}"#;
250
251		let claims: Claims = serde_json::from_str(json).unwrap();
252		assert_eq!(claims.publish, vec!["single-publish"]);
253		assert_eq!(claims.subscribe, vec!["single-subscribe"]);
254	}
255
256	#[test]
257	fn test_deserialize_vec_as_vec() {
258		let json = r#"{
259			"root": "test",
260			"put": ["pub1", "pub2"],
261			"get": ["sub1", "sub2"]
262		}"#;
263
264		let claims: Claims = serde_json::from_str(json).unwrap();
265		assert_eq!(claims.publish, vec!["pub1", "pub2"]);
266		assert_eq!(claims.subscribe, vec!["sub1", "sub2"]);
267	}
268
269	#[test]
270	fn test_deserialize_mixed() {
271		let json = r#"{
272			"root": "test",
273			"put": "single",
274			"get": ["multi1", "multi2"]
275		}"#;
276
277		let claims: Claims = serde_json::from_str(json).unwrap();
278		assert_eq!(claims.publish, vec!["single"]);
279		assert_eq!(claims.subscribe, vec!["multi1", "multi2"]);
280	}
281}