use std::collections::HashSet;
use std::env;
use std::time::Duration;
use futures::stream::TryStreamExt;
use once_cell::sync::Lazy;
use reqwest::StatusCode;
use reqwest_retry::policies::ExponentialBackoff;
use serde_json::json;
use test_log::test;
use tracing::info;
use uuid::Uuid;
use wiremock::{matchers, Mock, MockServer, ResponseTemplate};
use frontegg::{
ApiError, Client, ClientConfig, Error, TenantRequest, UserListConfig, UserListPartConfig,
UserRequest,
};
pub static CLIENT_ID: Lazy<String> =
Lazy::new(|| env::var("FRONTEGG_CLIENT_ID").expect("missing FRONTEGG_CLIENT_ID"));
pub static SECRET_KEY: Lazy<String> =
Lazy::new(|| env::var("FRONTEGG_SECRET_KEY").expect("missing FRONTEGG_SECRET_KEY"));
const TENANT_NAME_PREFIX: &str = "test tenant";
fn new_client() -> Client {
Client::builder()
.with_retry_policy(
ExponentialBackoff::builder()
.retry_bounds(Duration::from_millis(500), Duration::from_secs(20))
.build_with_max_retries(20),
)
.build(ClientConfig {
client_id: CLIENT_ID.clone(),
secret_key: SECRET_KEY.clone(),
})
}
async fn delete_existing_tenants(client: &Client) {
for tenant in client.list_tenants().await.unwrap() {
if tenant.name.starts_with(TENANT_NAME_PREFIX) {
info!(%tenant.id, "deleting existing tenant");
client.delete_tenant(tenant.id).await.unwrap();
}
}
}
#[test(tokio::test)]
async fn test_retries_with_mock_server() {
const MAX_RETRIES: u32 = 3;
let server = MockServer::start().await;
let client = Client::builder()
.with_vendor_endpoint(server.uri().parse().unwrap())
.with_retry_policy(
ExponentialBackoff::builder()
.retry_bounds(Duration::from_millis(1), Duration::from_millis(1))
.build_with_max_retries(MAX_RETRIES),
)
.build(ClientConfig {
client_id: "".into(),
secret_key: "".into(),
});
let mock = Mock::given(matchers::path("/auth/vendor"))
.and(matchers::method("POST"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string("{\"token\":\"test\", \"expiresIn\":2687784526}"),
)
.expect(1)
.named("auth");
server.register(mock).await;
let mock = Mock::given(matchers::method("GET"))
.and(matchers::path_regex("/tenants/.*"))
.respond_with(ResponseTemplate::new(429))
.expect(u64::from(MAX_RETRIES) + 1)
.named("get tenants");
server.register(mock).await;
let res = client.get_tenant(Uuid::new_v4()).await;
assert!(res.is_err());
let mock = Mock::given(matchers::method("POST"))
.and(matchers::path_regex("/tenants/.*"))
.respond_with(ResponseTemplate::new(429))
.expect(1)
.named("post tenants");
server.register(mock).await;
let _ = client
.create_tenant(&TenantRequest {
id: Uuid::new_v4(),
name: &format!("{TENANT_NAME_PREFIX} 1"),
metadata: json!({
"tenant_number": 1,
}),
..Default::default()
})
.await;
}
#[test(tokio::test)]
async fn test_tenants_and_users() {
let client = new_client();
delete_existing_tenants(&client).await;
let tenant_id_1 = Uuid::new_v4();
let tenant_id_2 = Uuid::new_v4();
client
.create_tenant(&TenantRequest {
id: tenant_id_1,
name: &format!("{TENANT_NAME_PREFIX} 1"),
metadata: json!({
"tenant_number": 1,
}),
creator_name: Some("tenant 1"),
creator_email: Some("creator@tenant1.com"),
})
.await
.unwrap();
client
.create_tenant(&TenantRequest {
id: tenant_id_2,
name: &format!("{TENANT_NAME_PREFIX} 2"),
metadata: json!(42),
..Default::default()
})
.await
.unwrap();
let mut tenants: Vec<_> = client
.list_tenants()
.await
.unwrap()
.into_iter()
.filter(|e| e.name.starts_with(TENANT_NAME_PREFIX))
.collect();
tenants.sort_by(|a, b| a.name.cmp(&b.name));
assert_eq!(tenants.len(), 2);
assert_eq!(tenants[0].id, tenant_id_1);
assert_eq!(tenants[1].id, tenant_id_2);
assert_eq!(tenants[0].name, format!("{TENANT_NAME_PREFIX} 1"));
assert_eq!(tenants[1].name, format!("{TENANT_NAME_PREFIX} 2"));
assert_eq!(tenants[0].metadata, json!({"tenant_number": 1}));
assert_eq!(tenants[1].metadata, json!(42));
assert_eq!(tenants[0].creator_name, Some("tenant 1".into()));
assert_eq!(tenants[1].creator_name, None);
assert_eq!(tenants[0].creator_email, Some("creator@tenant1.com".into()));
assert_eq!(tenants[1].creator_email, None);
assert_eq!(tenants[0].deleted_at, None);
assert_eq!(tenants[1].deleted_at, None);
let tenant = client.get_tenant(tenants[0].id).await.unwrap();
assert_eq!(tenant.id, tenants[0].id);
client
.set_tenant_metadata(
tenants[0].id,
&json!({
"tenant_name": tenants[0].name,
}),
)
.await
.unwrap();
let tenant = client.get_tenant(tenants[0].id).await.unwrap();
assert_eq!(
tenant.metadata,
json!({"tenant_name": tenant.name, "tenant_number": 1})
);
let set_tenant = client
.set_tenant_metadata(tenants[0].id, &json!({"tenant_name": "set test"}))
.await
.unwrap();
assert_eq!(
set_tenant.metadata,
json!({"tenant_name": "set test", "tenant_number": 1})
);
let tenant = client.get_tenant(tenants[0].id).await.unwrap();
assert_eq!(
tenant.metadata,
json!({"tenant_name": "set test", "tenant_number": 1})
);
let delete_tenant = client
.delete_tenant_metadata(tenants[0].id, "tenant_name")
.await
.unwrap();
assert_eq!(delete_tenant.metadata, json!({"tenant_number": 1}));
let tenant = client.get_tenant(tenants[0].id).await.unwrap();
assert_eq!(tenant.metadata, json!({"tenant_number": 1}));
let tenant_result = client
.get_tenant(uuid::uuid!("00000000-0000-0000-0000-000000000000"))
.await;
match tenant_result {
Err(Error::Api(ApiError { status_code, .. })) if status_code == StatusCode::NOT_FOUND => (),
_ => panic!("unexpected response: {tenant_result:?}"),
};
let mut users = vec![];
for (tenant_idx, tenant) in tenants.iter().enumerate() {
for user_idx in 0..3 {
let name = format!("user-{tenant_idx}-{user_idx}");
let email = format!("frontegg-test-{tenant_idx}-{user_idx}@example.com");
let created_user = client
.create_user(&UserRequest {
tenant_id: tenant.id,
name: &name,
email: &email,
skip_invite_email: true,
..Default::default()
})
.await
.unwrap();
assert_eq!(created_user.name, name);
assert_eq!(created_user.email, email);
let user = client.get_user(created_user.id).await.unwrap();
assert_eq!(created_user.id, user.id);
assert_eq!(user.name, name);
assert_eq!(user.email, email);
assert_eq!(user.tenants.len(), 1);
assert_eq!(user.tenants[0].tenant_id, tenant.id);
users.push(user);
}
}
for page_size in [1, 2, 10] {
let expected: HashSet<_> = users.iter().map(|u| u.id).collect();
let actual: HashSet<_> = client
.list_users(UserListConfig::default().page_size(page_size))
.map_ok(|u| u.id)
.try_collect()
.await
.unwrap();
assert!(expected.difference(&actual).collect::<Vec<_>>().is_empty());
}
let pages: Result<Vec<_>, Error> = client
.list_users_part(
UserListPartConfig::default()
.page_size(1)
.max_pages(1)
.starting_page(0),
)
.map_ok(|u| u.id)
.try_collect()
.await;
if let frontegg::Error::PaginationHault(v) = pages.as_ref().unwrap_err() {
assert_eq!(v, &1);
} else {
panic!("{:?} should be PaginationHault Error", pages);
};
let pages: Result<Vec<_>, Error> = client
.list_users_part(
UserListPartConfig::default()
.page_size(1)
.max_pages(1)
.starting_page(1),
)
.map_ok(|u| u.id)
.try_collect()
.await;
if let frontegg::Error::PaginationHault(v) = pages.as_ref().unwrap_err() {
assert_eq!(v, &2);
} else {
panic!("{:?} should be PaginationHault Error", pages);
};
let pages: Result<Vec<_>, Error> = client
.list_users_part(
UserListPartConfig::default()
.page_size(100)
.max_pages(100)
.starting_page(0),
)
.map_ok(|u| u.id)
.try_collect()
.await;
assert!(pages.is_ok());
{
let expected: HashSet<_> = users.iter().take(3).map(|u| u.id).collect();
let actual: HashSet<_> = client
.list_users(UserListConfig::default().tenant_id(tenant_id_1))
.map_ok(|u| u.id)
.try_collect()
.await
.unwrap();
assert_eq!(expected, actual);
}
for user in &users {
client.delete_user(user.id).await.unwrap();
}
{
let users: Vec<_> = client
.list_users(Default::default())
.try_collect::<Vec<_>>()
.await
.unwrap()
.into_iter()
.filter(|u| u.email.starts_with("frontegg-test-"))
.collect();
assert_eq!(users.len(), 0);
}
}