diff --git a/tests/src/scim/acceptance.rs b/tests/src/scim/acceptance.rs index f65c3c3..29379e2 100644 --- a/tests/src/scim/acceptance.rs +++ b/tests/src/scim/acceptance.rs @@ -1586,3 +1586,65 @@ async fn authority(test: &TestServer, scim: &ScimTest, closed_id: Id) { scim.destroy(&format!("/Users/{id}")).await; let _ = closed_id; } + +/// SCIM-14: the authenticated rate limit, per principal, with +/// `unlimitedRequests` exempt. +async fn rate_limits(test: &TestServer, scim: &ScimTest) { + let admin = test.account("admin"); + admin + .registry_update_setting( + structs::Http { + rate_limit_authenticated: Some(structs::Rate { + count: 2, + period: Duration::from_millis(60_000), + }), + ..Default::default() + }, + &[Property::RateLimitAuthenticated], + ) + .await; + admin.reload_settings().await; + + let principal = Account::new( + "scim-svc@scim.example.com", + PRINCIPAL_SECRET, + &[], + "", + scim.principal_id, + ); + let limited = ScimClient::bearer( + &api_key( + admin, + &principal, + json!({"@type": "Disable", "permissions": {"unlimitedRequests": true}}), + ) + .await, + ); + let mut refused = None; + for _ in 0..6 { + let reply = limited.get("/Users?count=1").await; + if reply.status == 429 { + refused = Some(reply); + break; + } + reply.assert_status(200); + } + let refused = refused.expect("SCIM-14: never rate limited"); + refused.assert_error(429, None); + assert!( + refused.header("retry-after").is_some(), + "SCIM-14: Retry-After" + ); + scim.client.get("/Users?count=1").await.assert_status(200); + + admin + .registry_update_setting( + structs::Http { + rate_limit_authenticated: None, + ..Default::default() + }, + &[Property::RateLimitAuthenticated], + ) + .await; + admin.reload_settings().await; +} diff --git a/tests/src/scim/mod.rs b/tests/src/scim/mod.rs index 886021b..d469aad 100644 --- a/tests/src/scim/mod.rs +++ b/tests/src/scim/mod.rs @@ -451,3 +451,50 @@ pub async fn scim_oidc_tests() { let scim = ScimTest::new(&test).await; oidc::test(&test, &scim).await; } + +/// Acceptance test 31 (compat): on a copy of INBUXA's data, the SCIM +/// fields read back as observed 1: no domain open to SCIM, and no account +/// with an `externalId`. Run with `INBUXA_COMPAT_ADMIN` (`name:password`), +/// `NO_INSERT=1`, and the store's `TMPDIR`/`STORE` pointing at the copy. +#[ignore] +#[tokio::test(flavor = "multi_thread")] +pub async fn scim_compat() { + let admin = std::env::var("INBUXA_COMPAT_ADMIN").expect("INBUXA_COMPAT_ADMIN"); + assert!(std::env::var("NO_INSERT").is_ok(), "NO_INSERT must be set"); + let _test = crate::utils::server::TestServerBuilder::new("scim_compat") + .await + .with_default_listeners() + .await + .build_with_opts(false) + .await; + let (name, secret) = admin.split_once(':').expect("name:password"); + let admin = Account::new( + Box::leak(name.to_string().into_boxed_str()), + Box::leak(secret.to_string().into_boxed_str()), + &[], + "Compat admin", + Id::from(u32::MAX), + ); + let domains = admin + .jmap_method_call("x:Domain/get", json!({"ids": null})) + .await; + for domain in domains.list() { + assert_eq!( + domain["allowScimProvisioning"], + json!(false), + "observed 1: {}", + domain["name"] + ); + } + let accounts = admin + .jmap_method_call("x:Account/get", json!({"ids": null})) + .await; + assert!(!accounts.list().is_empty(), "the copy has accounts"); + for account in accounts.list() { + assert!( + account["externalId"].is_null(), + "observed 1: {}", + account["name"] + ); + } +}