Skip to main content

firebase_admin/auth/
mode.rs

1//! Runtime live/emulator mode selection.
2//!
3//! Unlike an approach that makes the client generic over a credentials or
4//! mode type (which forces every method signature to diverge between live
5//! and emulator variants), [`ClientMode`] is a plain runtime value. Every
6//! [`crate::auth::AuthClient`] method is defined exactly once and branches
7//! internally on `self.mode` only where behavior genuinely differs.
8
9use crate::auth::identity_toolkit::IdentityToolkitEndpoints;
10
11/// The environment variable Firebase's own SDKs use to auto-detect a running
12/// Auth Emulator.
13pub const EMULATOR_HOST_ENV_VAR: &str = "FIREBASE_AUTH_EMULATOR_HOST";
14
15/// Selects whether an [`crate::auth::AuthClient`] talks to production
16/// Firebase or a local emulator instance.
17#[derive(Debug, Clone)]
18pub enum ClientMode {
19    /// Talk to production Firebase Authentication.
20    Live,
21    /// Talk to a local Firebase Auth Emulator at `host` (e.g. `localhost:9099`).
22    Emulator {
23        /// The emulator's host and port.
24        host: String,
25    },
26}
27
28impl ClientMode {
29    /// Resolves the mode to use: an explicitly-requested emulator host takes
30    /// priority, then the `FIREBASE_AUTH_EMULATOR_HOST` environment variable,
31    /// and otherwise [`ClientMode::Live`].
32    pub fn resolve(explicit_emulator_host: Option<String>) -> Self {
33        if let Some(host) = explicit_emulator_host {
34            return ClientMode::Emulator { host };
35        }
36        if let Ok(host) = std::env::var(EMULATOR_HOST_ENV_VAR) {
37            if !host.trim().is_empty() {
38                return ClientMode::Emulator { host };
39            }
40        }
41        ClientMode::Live
42    }
43
44    /// Builds the Identity Toolkit endpoint set for this mode.
45    pub fn endpoints(&self) -> IdentityToolkitEndpoints {
46        match self {
47            ClientMode::Live => IdentityToolkitEndpoints::live(),
48            ClientMode::Emulator { host } => IdentityToolkitEndpoints::emulator(host),
49        }
50    }
51
52    /// Whether requests in this mode require an OAuth2 bearer token.
53    ///
54    /// The Firebase Auth Emulator does not enforce authentication.
55    pub fn requires_bearer_token(&self) -> bool {
56        matches!(self, ClientMode::Live)
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use std::sync::Mutex;
64
65    // `std::env::var` reads process-global state; serialize tests that touch
66    // `EMULATOR_HOST_ENV_VAR` so they can't observe each other's values when
67    // the test binary runs them concurrently.
68    static ENV_VAR_TEST_LOCK: Mutex<()> = Mutex::new(());
69
70    fn with_emulator_env_var<T>(value: Option<&str>, f: impl FnOnce() -> T) -> T {
71        let _guard = ENV_VAR_TEST_LOCK.lock().unwrap();
72        let previous = std::env::var(EMULATOR_HOST_ENV_VAR).ok();
73        match value {
74            Some(v) => std::env::set_var(EMULATOR_HOST_ENV_VAR, v),
75            None => std::env::remove_var(EMULATOR_HOST_ENV_VAR),
76        }
77
78        let result = f();
79
80        match previous {
81            Some(v) => std::env::set_var(EMULATOR_HOST_ENV_VAR, v),
82            None => std::env::remove_var(EMULATOR_HOST_ENV_VAR),
83        }
84        result
85    }
86
87    #[test]
88    fn explicit_host_wins_over_env_var() {
89        with_emulator_env_var(Some("env-host:9099"), || {
90            let mode = ClientMode::resolve(Some("explicit-host:9099".to_string()));
91            assert!(matches!(mode, ClientMode::Emulator { host } if host == "explicit-host:9099"));
92        });
93    }
94
95    #[test]
96    fn env_var_is_used_when_no_explicit_host_given() {
97        with_emulator_env_var(Some("env-host:9099"), || {
98            let mode = ClientMode::resolve(None);
99            assert!(matches!(mode, ClientMode::Emulator { host } if host == "env-host:9099"));
100        });
101    }
102
103    #[test]
104    fn whitespace_only_env_var_is_treated_as_unset() {
105        with_emulator_env_var(Some("   "), || {
106            let mode = ClientMode::resolve(None);
107            assert!(matches!(mode, ClientMode::Live));
108        });
109    }
110
111    #[test]
112    fn defaults_to_live_with_nothing_set() {
113        with_emulator_env_var(None, || {
114            let mode = ClientMode::resolve(None);
115            assert!(matches!(mode, ClientMode::Live));
116        });
117    }
118
119    #[test]
120    fn requires_bearer_token_only_in_live_mode() {
121        assert!(ClientMode::Live.requires_bearer_token());
122        assert!(!ClientMode::Emulator {
123            host: "localhost:9099".to_string()
124        }
125        .requires_bearer_token());
126    }
127}