radion_sdk/realtime/
auth.rs1use std::fmt;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7
8use crate::error::Result;
9
10type BoxFuture = Pin<Box<dyn Future<Output = Result<String>> + Send>>;
11type ProviderFn = Arc<dyn Fn() -> BoxFuture + Send + Sync>;
12
13#[derive(Clone)]
16pub struct TokenProvider(ProviderFn);
17
18impl TokenProvider {
19 pub fn from_static(token: impl Into<String>) -> Self {
21 let token = token.into();
22 Self(Arc::new(move || {
23 let token = token.clone();
24 Box::pin(async move { Ok(token) })
25 }))
26 }
27
28 pub fn new<F, Fut>(f: F) -> Self
30 where
31 F: Fn() -> Fut + Send + Sync + 'static,
32 Fut: Future<Output = Result<String>> + Send + 'static,
33 {
34 Self(Arc::new(move || Box::pin(f())))
35 }
36
37 pub fn fetch(&self) -> BoxFuture {
39 (self.0)()
40 }
41}
42
43impl fmt::Debug for TokenProvider {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 f.write_str("TokenProvider(..)")
46 }
47}
48
49fn encode(value: &str) -> String {
51 let mut out = String::with_capacity(value.len());
52 for byte in value.bytes() {
53 match byte {
54 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
55 out.push(byte as char);
56 }
57 _ => out.push_str(&format!("%{byte:02X}")),
58 }
59 }
60 out
61}
62
63pub fn build_auth_query_url(url: &str, api_key: &str, token: Option<&str>) -> String {
66 let separator = if url.contains('?') { '&' } else { '?' };
67 let mut query = format!("api-key={}", encode(api_key));
68 if let Some(token) = token {
69 query.push_str(&format!("&token={}", encode(token)));
70 }
71 format!("{url}{separator}{query}")
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn build_url_appends_api_key() {
80 assert_eq!(
81 build_auth_query_url("wss://api.radion.app/ws", "k1", None),
82 "wss://api.radion.app/ws?api-key=k1"
83 );
84 }
85
86 #[test]
87 fn build_url_appends_token() {
88 assert_eq!(
89 build_auth_query_url("wss://api.radion.app/ws", "k1", Some("jwt-2")),
90 "wss://api.radion.app/ws?api-key=k1&token=jwt-2"
91 );
92 }
93
94 #[test]
95 fn build_url_preserves_existing_query() {
96 assert_eq!(
97 build_auth_query_url("wss://api.radion.app/ws?v=1", "k1", None),
98 "wss://api.radion.app/ws?v=1&api-key=k1"
99 );
100 }
101
102 #[test]
103 fn build_url_encodes_values() {
104 assert_eq!(
105 build_auth_query_url("wss://api.radion.app/ws", "a b", Some("x/y=")),
106 "wss://api.radion.app/ws?api-key=a%20b&token=x%2Fy%3D"
107 );
108 }
109
110 #[tokio::test]
111 async fn static_provider_returns_token() {
112 let provider = TokenProvider::from_static("jwt");
113 assert_eq!(provider.fetch().await.unwrap(), "jwt");
114 }
115
116 #[tokio::test]
117 async fn closure_provider_returns_token() {
118 let provider = TokenProvider::new(|| async { Ok("fresh".to_string()) });
119 assert_eq!(provider.fetch().await.unwrap(), "fresh");
120 }
121}