#[cfg(test)]
mod tests {
use std::env;
use std::time::Duration;
use tokio::time::sleep;
use wiremock::{Mock, MockServer, ResponseTemplate};
use wiremock::matchers::{method, path};
use crate::cache::{Cache, MemoryCache};
use crate::{Auth, Client};
use crate::flag::FeatureFlag;
use serial_test::serial;
async fn create_test_client(server: &MockServer) -> Client {
Client::builder()
.with_base_url(&server.uri())
.with_auth(Auth {
project_id: "test-project".to_string(),
agent_id: "test-agent".to_string(),
environment_id: "test-env".to_string(),
})
.with_memory_cache()
.build()
.expect("Failed to build test client")
}
#[tokio::test]
async fn test_client_no_ids() {
let client = Client::builder()
.with_memory_cache()
.build()
.expect("Failed to build client");
assert_eq!(client.max_retries, 3)
}
#[tokio::test]
async fn test_client_initialization() {
let client = Client::builder()
.with_base_url("https://test-api.example.com")
.with_max_retries(5)
.with_auth(Auth {
project_id: "test-project".to_string(),
agent_id: "test-agent".to_string(),
environment_id: "test-env".to_string(),
})
.with_memory_cache()
.build()
.expect("Failed to build client");
assert_eq!(client.base_url, "https://test-api.example.com");
assert_eq!(client.max_retries, 5);
assert!(client.auth.is_some());
}
#[tokio::test]
async fn test_flag_enabled_from_api() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/flags"))
.respond_with(ResponseTemplate::new(200)
.set_body_json(serde_json::json!({
"intervalAllowed": 60,
"flags": [
{
"enabled": true,
"details": {
"name": "test-flag",
"id": "123"
}
},
{
"enabled": false,
"details": {
"name": "disabled-flag",
"id": "456"
}
}
]
}))
)
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server).await;
let enabled = client.is("test-flag").enabled().await;
assert!(enabled);
let disabled = client.is("disabled-flag").enabled().await;
assert!(!disabled);
let non_existent = client.is("non-existent-flag").enabled().await;
assert!(!non_existent);
}
#[tokio::test]
#[serial]
async fn test_flag_list() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/flags"))
.respond_with(ResponseTemplate::new(200)
.set_body_json(serde_json::json!({
"intervalAllowed": 60,
"flags": [
{
"enabled": true,
"details": {
"name": "flag1",
"id": "123"
}
},
{
"enabled": false,
"details": {
"name": "flag2",
"id": "456"
}
}
]
}))
)
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server).await;
let _ = client.is("flag1").enabled().await;
let flags = client.list().await.unwrap();
assert_eq!(flags.len(), 2);
let flag1 = flags.iter().find(|f| f.details.name == "flag1").unwrap();
assert!(flag1.enabled);
assert_eq!(flag1.details.id, "123");
let flag2 = flags.iter().find(|f| f.details.name == "flag2").unwrap();
assert!(!flag2.enabled);
assert_eq!(flag2.details.id, "456");
}
#[tokio::test]
#[serial]
async fn test_local_environment_flags() {
env::set_var("FLAGS_TEST_ENV_FLAG", "true");
env::set_var("FLAGS_ANOTHER_FLAG", "false");
let mock_server = MockServer::start().await;
let client = create_test_client(&mock_server).await;
let env_flag = client.is("test_env_flag").enabled().await;
assert!(env_flag);
let another_flag = client.is("another_flag").enabled().await;
assert!(!another_flag);
let env_flag_dash = client.is("test-env-flag").enabled().await;
assert!(env_flag_dash);
env::remove_var("FLAGS_TEST_ENV_FLAG");
env::remove_var("FLAGS_ANOTHER_FLAG");
}
#[tokio::test]
async fn test_cache_refresh_simple() {
let mut cache = MemoryCache::new();
let flags = vec![
FeatureFlag {
enabled: true,
details: crate::flag::Details {
name: "cache-test-flag".to_string(),
id: "123".to_string(),
},
},
];
cache.refresh(&flags, 1).await.unwrap();
let (enabled, exists) = cache.get("cache-test-flag").await.unwrap();
assert!(exists);
assert!(enabled);
sleep(Duration::from_secs(2)).await;
assert!(cache.should_refresh_cache().await);
let flags = vec![
FeatureFlag {
enabled: false,
details: crate::flag::Details {
name: "cache-test-flag".to_string(),
id: "123".to_string(),
},
},
];
cache.refresh(&flags, 60).await.unwrap();
let (enabled, exists) = cache.get("cache-test-flag").await.unwrap();
assert!(exists);
assert!(!enabled);
}
#[tokio::test]
async fn test_circuit_breaker() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/flags"))
.respond_with(ResponseTemplate::new(500)
.set_body_json(serde_json::json!({"error": "Internal Server Error"}))
)
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server).await;
let enabled = client.is("circuit-test-flag").enabled().await;
assert!(!enabled);
let circuit_state = client.circuit_state.read().await;
assert!(!circuit_state.is_open);
}
#[tokio::test]
async fn test_batch_operations() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/flags"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"intervalAllowed": 60,
"flags": [
{
"enabled": true,
"details": {
"name": "feature-a",
"id": "1"
}
},
{
"enabled": false,
"details": {
"name": "feature-b",
"id": "2"
}
},
{
"enabled": true,
"details": {
"name": "feature-c",
"id": "3"
}
}
]
})))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server).await;
let flags = client.get_multiple(&["feature-a", "feature-b", "feature-c", "feature-d"]).await;
assert_eq!(flags.get("feature-a"), Some(&true));
assert_eq!(flags.get("feature-b"), Some(&false));
assert_eq!(flags.get("feature-c"), Some(&true));
assert_eq!(flags.get("feature-d"), Some(&false));
assert!(client.all_enabled(&["feature-a", "feature-c"]).await); assert!(!client.all_enabled(&["feature-a", "feature-b"]).await);
assert!(client.any_enabled(&["feature-a", "feature-b"]).await); assert!(!client.any_enabled(&["feature-b", "feature-d"]).await); }
#[tokio::test]
async fn test_error_callback() {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let error_count = Arc::new(AtomicUsize::new(0));
let error_count_clone = Arc::clone(&error_count);
let client = Client::builder()
.with_base_url("http://invalid-url-that-will-fail")
.with_auth(Auth {
project_id: "test-project".to_string(),
agent_id: "test-agent".to_string(),
environment_id: "test-env".to_string(),
})
.with_error_callback(move |_error| {
error_count_clone.fetch_add(1, Ordering::SeqCst);
})
.build()
.expect("Failed to build client");
let _ = client.is("test-flag").enabled().await;
assert!(error_count.load(Ordering::SeqCst) > 0);
}
#[tokio::test]
async fn test_memory_cache() {
let mut cache = MemoryCache::new();
cache.init().await.unwrap();
let (enabled, exists) = cache.get("test-flag").await.unwrap();
assert!(!exists);
assert!(!enabled);
let flags = vec![
FeatureFlag {
enabled: true,
details: crate::flag::Details {
name: "test-flag".to_string(),
id: "123".to_string(),
},
},
];
cache.refresh(&flags, 60).await.unwrap();
let (enabled, exists) = cache.get("test-flag").await.unwrap();
assert!(exists);
assert!(enabled);
let all_flags = cache.get_all().await.unwrap();
assert_eq!(all_flags.len(), 1);
assert_eq!(all_flags[0].details.name, "test-flag");
assert!(!cache.should_refresh_cache().await);
}
}