Skip to main content

moq_token/
claims.rs

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