firebase_rs_sdk/app_check/
interop.rs

1use std::sync::{Arc, Mutex};
2
3use crate::app_check::api;
4use crate::app_check::errors::AppCheckResult;
5use crate::app_check::types::{
6    AppCheck, AppCheckInternalListener, AppCheckTokenError, AppCheckTokenResult, ListenerHandle,
7    ListenerType,
8};
9//#[cfg(feature = "firestore")]
10use crate::firestore::TokenProviderArc;
11
12//#[cfg(feature = "firestore")]
13use super::token_provider::app_check_token_provider_arc;
14
15#[derive(Clone)]
16pub struct FirebaseAppCheckInternal {
17    app_check: AppCheck,
18    listeners: Arc<Mutex<Vec<(AppCheckInternalListener, ListenerHandle)>>>,
19}
20
21impl FirebaseAppCheckInternal {
22    pub fn new(app_check: AppCheck) -> Self {
23        Self {
24            app_check,
25            listeners: Arc::new(Mutex::new(Vec::new())),
26        }
27    }
28
29    pub fn app_check(&self) -> &AppCheck {
30        &self.app_check
31    }
32
33    pub async fn get_token(
34        &self,
35        force_refresh: bool,
36    ) -> Result<AppCheckTokenResult, AppCheckTokenError> {
37        api::get_token(&self.app_check, force_refresh).await
38    }
39
40    pub async fn get_limited_use_token(&self) -> Result<AppCheckTokenResult, AppCheckTokenError> {
41        api::get_limited_use_token(&self.app_check).await
42    }
43
44    pub async fn heartbeat_header(&self) -> AppCheckResult<Option<String>> {
45        self.app_check.heartbeat_header().await
46    }
47
48    pub fn add_token_listener(&self, listener: AppCheckInternalListener) -> AppCheckResult<()> {
49        let listeners = Arc::clone(&self.listeners);
50        let listener_clone = Arc::clone(&listener);
51        let bridge = Arc::new(move |result: &AppCheckTokenResult| {
52            (*listener_clone)(result.clone());
53        });
54
55        let handle =
56            api::add_token_listener(&self.app_check, bridge, None, ListenerType::Internal)?;
57        listeners.lock().unwrap().push((listener, handle));
58        Ok(())
59    }
60
61    pub fn remove_token_listener(&self, listener: &AppCheckInternalListener) {
62        let mut listeners = self.listeners.lock().unwrap();
63        if let Some(pos) = listeners
64            .iter()
65            .position(|(stored, _)| Arc::ptr_eq(stored, listener))
66        {
67            let (_, handle) = listeners.remove(pos);
68            handle.unsubscribe();
69        }
70    }
71
72    /// Exposes the internal App Check instance as a Firestore token provider.
73    //#[cfg(feature = "firestore")]
74    pub fn token_provider(&self) -> TokenProviderArc {
75        app_check_token_provider_arc(self.clone())
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::app::{FirebaseApp, FirebaseAppConfig, FirebaseOptions};
83    use crate::app_check::api::{
84        clear_registry, clear_state_for_tests, initialize_app_check, test_guard, token_with_ttl,
85    };
86    use crate::app_check::types::{
87        box_app_check_future, AppCheckOptions, AppCheckProvider, AppCheckProviderFuture,
88        AppCheckToken,
89    };
90    use crate::component::ComponentContainer;
91    use std::sync::atomic::{AtomicUsize, Ordering};
92    use std::time::Duration;
93
94    #[derive(Clone)]
95    struct TestProvider;
96
97    impl AppCheckProvider for TestProvider {
98        fn get_token(&self) -> AppCheckProviderFuture<'_, AppCheckResult<AppCheckToken>> {
99            box_app_check_future(async { token_with_ttl("token", Duration::from_secs(60)) })
100        }
101
102        fn get_limited_use_token(
103            &self,
104        ) -> AppCheckProviderFuture<'_, AppCheckResult<AppCheckToken>> {
105            box_app_check_future(async { token_with_ttl("limited", Duration::from_secs(60)) })
106        }
107    }
108
109    fn test_app(name: &str) -> FirebaseApp {
110        FirebaseApp::new(
111            FirebaseOptions::default(),
112            FirebaseAppConfig::new(name.to_string(), false),
113            ComponentContainer::new(name.to_string()),
114        )
115    }
116
117    async fn setup_internal(name: &str) -> FirebaseAppCheckInternal {
118        clear_state_for_tests();
119        clear_registry();
120        let app = test_app(name);
121        let provider = Arc::new(TestProvider);
122        let options = AppCheckOptions::new(provider);
123        let app_check = initialize_app_check(Some(app), options)
124            .await
125            .expect("initialize app check");
126        FirebaseAppCheckInternal::new(app_check)
127    }
128
129    #[tokio::test(flavor = "current_thread")]
130    async fn get_token_returns_value() {
131        let _guard = test_guard();
132        let internal = setup_internal("app-check-internal-test").await;
133        let result = internal.get_token(false).await.unwrap();
134        assert_eq!(result.token, "token");
135
136        let heartbeat = internal.heartbeat_header().await.unwrap();
137        assert!(heartbeat.is_none());
138    }
139
140    #[tokio::test(flavor = "current_thread")]
141    async fn listener_receives_updates_and_can_be_removed() {
142        let _guard = test_guard();
143        let internal = setup_internal("app-check-listener-test").await;
144        let counter = Arc::new(AtomicUsize::new(0));
145        let listener: AppCheckInternalListener = {
146            let counter = counter.clone();
147            Arc::new(move |result: AppCheckTokenResult| {
148                assert_eq!(result.token, "token");
149                counter.fetch_add(1, Ordering::SeqCst);
150            })
151        };
152
153        // populate token cache
154        internal.get_token(false).await.unwrap();
155        internal.add_token_listener(listener.clone()).unwrap();
156        assert_eq!(counter.load(Ordering::SeqCst), 1);
157
158        internal.get_token(true).await.unwrap();
159        assert_eq!(counter.load(Ordering::SeqCst), 2);
160
161        internal.remove_token_listener(&listener);
162        internal.get_token(true).await.unwrap();
163        assert_eq!(counter.load(Ordering::SeqCst), 2);
164    }
165}