#![cfg(not(target_arch = "wasm32"))]
use anyhow::{Context, Result};
use openrtc::client::{AuthMode, Client};
use openrtc::native_auth::{FirebaseAuthConfig, NativeAuthState};
use serde_json::{json, Value};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
const DEFAULT_PROJECT_ID: &str = "pluto-rtc-prod";
const DEFAULT_API_KEY: &str = "pk_test_integration_test";
const DEFAULT_SECRET_KEY: &str = "sk_test_integration_plutonium";
const DEFAULT_APP_TAG: &str = "app_integration_test";
fn enabled() -> bool {
std::env::var("OPENRTC_NATIVE_AUTH_EMULATOR")
.map(|value| value == "1" || value.eq_ignore_ascii_case("true"))
.unwrap_or(false)
}
fn env_or(name: &str, fallback: &str) -> String {
std::env::var(name)
.ok()
.map(|value| value.trim().trim_end_matches('/').to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| fallback.to_string())
}
fn now_millis() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time after epoch")
.as_millis()
}
async fn mint_custom_token(project_id: &str, user_id: &str) -> Result<String> {
let functions_host = env_or("OPENRTC_FUNCTIONS_EMULATOR_HOST", "127.0.0.1:5002");
let response = reqwest::Client::new()
.post(format!(
"http://{functions_host}/{project_id}/us-central1/v1Tokens"
))
.bearer_auth(env_or("OPENRTC_EMULATOR_SECRET_KEY", DEFAULT_SECRET_KEY))
.header(
"x-openrtc-idempotency-key",
format!("native-auth-gateway-boundary:{user_id}"),
)
.json(&json!({ "userId": user_id, "ttl": 3600 }))
.send()
.await
.context("v1Tokens request failed")?;
let status = response.status();
let body: Value = response.json().await.unwrap_or(Value::Null);
if !status.is_success() {
anyhow::bail!("v1Tokens failed status={status} body={body}");
}
body.get("token")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
.context("v1Tokens response missing token")
}
#[tokio::test]
async fn native_auth_emulator_requires_host_gateway_projection() -> Result<()> {
if !enabled() {
eprintln!("skipping native auth emulator test: set OPENRTC_NATIVE_AUTH_EMULATOR=1");
return Ok(());
}
rustls::crypto::ring::default_provider()
.install_default()
.ok();
let project_id = env_or("OPENRTC_EMULATOR_PROJECT_ID", DEFAULT_PROJECT_ID);
let api_key = env_or("OPENRTC_EMULATOR_API_KEY", DEFAULT_API_KEY);
let auth_host = env_or("FIREBASE_AUTH_EMULATOR_HOST", "127.0.0.1:9100");
let config = FirebaseAuthConfig::new(&api_key)
.identity_toolkit_base_url(format!("http://{auth_host}/identitytoolkit.googleapis.com"))
.secure_token_base_url(format!("http://{auth_host}/securetoken.googleapis.com"))
.refresh_margin(Duration::from_secs(30));
let user_id = format!("emulator_native_gateway_{}", now_millis());
let custom_token = mint_custom_token(&project_id, &user_id).await?;
let auth = NativeAuthState::new();
let session = auth
.sign_in_with_custom_token(config, &custom_token)
.await
.context("native custom-token exchange failed")?;
let firebase_uid = session
.user_id
.as_deref()
.context("managed custom-token exchange did not return a Firebase uid")?;
assert!(
firebase_uid.starts_with("pluto_app_integration_test_"),
"managed custom-token exchange must retain the app-scoped Firebase uid; actual={firebase_uid}"
);
let client = Client::builder_with_native_auth(project_id, api_key, auth)
.auth_mode(AuthMode::External)
.build();
assert_eq!(client.app_tag(), DEFAULT_APP_TAG);
let error = client
.search_devices(session.user_id.as_deref().unwrap_or(&user_id))
.await
.expect_err("standalone native clients must fail closed without a gateway provider");
assert!(
error
.to_string()
.contains("managed coordination gateway is required"),
"unexpected provider-boundary error: {error:#}"
);
Ok(())
}