From 3df72b355a67186a9c7f4c7fd22499bb8f697753 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Sat, 19 Sep 2026 09:53:36 -0700 Subject: [PATCH] SCIM tests: the authenticated rate limit, and the compat check on INBUXA's data (SCIM-14, test 31) A principal's key without unlimitedRequests gets 429 with Retry-After over the limit, while its unlimited key still works. scim_compat, ignored, checks a copy of INBUXA's data reads back as observed: no domain open to SCIM and no externalId. --- tests/src/scim/acceptance.rs | 62 ++++++++++++++++++++++++++++++++++++ tests/src/scim/mod.rs | 47 +++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) 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"] + ); + } +}