diff --git a/docs/spec/compat-tests.md b/docs/spec/compat-tests.md index bb819e2..09a8b31 100644 --- a/docs/spec/compat-tests.md +++ b/docs/spec/compat-tests.md @@ -1,6 +1,6 @@ # Running the compat tests against a copy of INBUXA's data -Status: 2026-09-19. +Status: 2026-09-19 (dry run below; still unrun against INBUXA data). Each feature spec has one **(compat)** acceptance test: the check that INBUXA's own data opens in inbuxa-server and reads back as it did on the @@ -20,11 +20,20 @@ tests are not an exception. ## What you need 1. A copy of the data store, opened with the same `STORE` backend the copy - was taken from, and a `TMPDIR` pointing at it. `NO_INSERT=1` stops the - harness from resetting or seeding it. -2. `INBUXA_COMPAT_ADMIN`, as `name:password`, for a server-level + was taken from. **`TMPDIR` is the copy's parent, not the copy.** The + harness opens `$TMPDIR/` (`tests/src/utils/temp_dir.rs`), so + the copy for `tenant_compat` has to sit at `$TMPDIR/tenant_compat`, the + one for `scim_compat` at `$TMPDIR/scim_compat`, and so on. Point `TMPDIR` + at the copy itself and the harness quietly creates an empty store beside + it and the test reports INBUXA's data as missing — a cutover blocker that + isn't one. Each test wants its own copy anyway: `monitoring_compat` + purges what it reads. +2. `NO_INSERT=1`, which stops the harness resetting and seeding the store. + Every one of the eight refuses to run without it, before the store is + touched (verified, "The dry run" below). +3. `INBUXA_COMPAT_ADMIN`, as `name:password`, for a server-level administrator in that copy. -3. For `tenant_compat` only: `INBUXA_COMPAT_EXPECTED`, a JSON file recorded +4. For `tenant_compat` only: `INBUXA_COMPAT_EXPECTED`, a JSON file recorded from the Enterprise server before the move: ```json @@ -34,11 +43,18 @@ tests are not an exception. "domains": [""]}} } ``` +5. For `masked_email_compat` only: `INBUXA_COMPAT_MASKS`, recorded the same + way: + `[{"id": "...", "accountId": "...", "email": "...", "enabled": true}]`. +6. For `undelete_compat` only: `INBUXA_COMPAT_ARCHIVED`, the + `x:ArchivedItem/get` results, each with its `id` and `accountId`: + `[{"id": "...", "accountId": "..."}]`. ## Running one ``` -NO_INSERT=1 STORE= TMPDIR=/path/to/copy \ +# the copy is at $TMPDIR/, e.g. /srv/compat/tenant_compat +NO_INSERT=1 STORE= TMPDIR=/srv/compat \ INBUXA_COMPAT_ADMIN='admin@example.org:' \ RUST_MIN_STACK=8388608 \ cargo test -p tests --features -- --ignored --exact @@ -60,6 +76,31 @@ Run them one at a time: each starts a server on fixed ports. | `scim::scim_compat` | 7, SCIM | No domain open to SCIM, and no account with an `externalId`, as observed | | `directory::per_domain::per_domain_directory_compat` | 9, per-domain directories | No directory, no server default, and no domain with its own directory: any domain with one is a cutover blocker | +## The dry run + +None of the eight has ever run against INBUXA's data, so on 2026-09-19 all +eight were run against an empty store with synthetic inputs, to prove the +plumbing before the day the copy exists. What that established: + +- **`NO_INSERT` protects the copy.** A sentinel file was left in each store + directory. Every run kept it, and the run without `NO_INSERT` stopped at + "NO_INSERT must be set, or the copy of INBUXA's data is wiped" before the + harness deleted anything. The guard is ahead of the store in all eight. +- **`TMPDIR` is the parent.** The store appeared at + `$TMPDIR//rocks.db` in every run, which is where the finding in + "What you need" comes from. +- **The documented JSON shapes parse.** The three files above, written + exactly as this page gives them, were read without complaint by + `tenant_compat`, `masked_email_compat` and `undelete_compat`. +- **A bad administrator now says so.** Each test authenticates once before it + asserts anything, and fails with `INBUXA_COMPAT_ADMIN did not authenticate + as ` and the 401 body. Before that check the first call simply + panicked with "Missing list in response", which reads like INBUXA's data is + wrong when the login is what's wrong. + +What it can't establish is anything about INBUXA's data: every run ended at +the authentication check, since an empty store holds no such administrator. + ## What a failure means - `tenant_compat`, `masked_email_compat`, `undelete_compat`, diff --git a/tests/src/directory/per_domain.rs b/tests/src/directory/per_domain.rs index 90d2923..cb6dd79 100644 --- a/tests/src/directory/per_domain.rs +++ b/tests/src/directory/per_domain.rs @@ -494,6 +494,8 @@ pub async fn per_domain_directory_compat() { "Compat admin", Id::from(u32::MAX), ); + + admin.assert_authenticates("INBUXA_COMPAT_ADMIN").await; let domains = admin .jmap_method_call("x:Domain/get", json!({"ids": null})) .await; diff --git a/tests/src/scim/mod.rs b/tests/src/scim/mod.rs index a8cea88..2ba3bfb 100644 --- a/tests/src/scim/mod.rs +++ b/tests/src/scim/mod.rs @@ -476,6 +476,8 @@ pub async fn scim_compat() { "Compat admin", Id::from(u32::MAX), ); + + admin.assert_authenticates("INBUXA_COMPAT_ADMIN").await; let domains = admin .jmap_method_call("x:Domain/get", json!({"ids": null})) .await; diff --git a/tests/src/system/ai.rs b/tests/src/system/ai.rs index b4c7a29..a840c06 100644 --- a/tests/src/system/ai.rs +++ b/tests/src/system/ai.rs @@ -644,6 +644,8 @@ pub async fn ai_compat() { "Compat admin", Id::from(u32::MAX), ); + + admin.assert_authenticates("INBUXA_COMPAT_ADMIN").await; let tags = admin .jmap_method_call("x:SpamTag/get", json!({"ids": null})) .await; diff --git a/tests/src/system/branding.rs b/tests/src/system/branding.rs index e7f9317..39c1c6d 100644 --- a/tests/src/system/branding.rs +++ b/tests/src/system/branding.rs @@ -520,6 +520,8 @@ pub async fn branding_compat() { Id::from(u32::MAX), ); + admin.assert_authenticates("INBUXA_COMPAT_ADMIN").await; + // Every domain's logo reads back and is served or skipped, never an error let domains = admin .jmap_method_call( diff --git a/tests/src/system/masked_email.rs b/tests/src/system/masked_email.rs index e6eb130..9fb2de5 100644 --- a/tests/src/system/masked_email.rs +++ b/tests/src/system/masked_email.rs @@ -449,6 +449,8 @@ pub async fn masked_email_compat() { Id::from(u32::MAX), ); + admin.assert_authenticates("INBUXA_COMPAT_ADMIN").await; + for mask in masks { let id = Id::from_str(mask["id"].as_str().unwrap()).unwrap(); let account = mask["accountId"].as_str().unwrap(); diff --git a/tests/src/system/monitoring.rs b/tests/src/system/monitoring.rs index e21b465..e8e2fa4 100644 --- a/tests/src/system/monitoring.rs +++ b/tests/src/system/monitoring.rs @@ -410,6 +410,8 @@ pub async fn monitoring_compat() { Id::from(u32::MAX), ); + admin.assert_authenticates("INBUXA_COMPAT_ADMIN").await; + // Observed 1: 30 and 90 days, hourly, both stores Default, no alerts let retention = admin .jmap_method_call("x:DataRetention/get", json!({"ids": ["singleton"]})) diff --git a/tests/src/system/tenant.rs b/tests/src/system/tenant.rs index aac05c2..47f9e3c 100644 --- a/tests/src/system/tenant.rs +++ b/tests/src/system/tenant.rs @@ -926,6 +926,8 @@ pub async fn tenant_compat() { Id::from(u32::MAX), ); + admin.assert_authenticates("INBUXA_COMPAT_ADMIN").await; + // Tenants, their quotas and their members read back unchanged for (id, tenant) in expected["tenants"].as_object().expect("tenants") { let id = Id::from_str(id).expect("tenant id"); diff --git a/tests/src/system/undelete.rs b/tests/src/system/undelete.rs index 0c075e1..52d60e5 100644 --- a/tests/src/system/undelete.rs +++ b/tests/src/system/undelete.rs @@ -636,6 +636,8 @@ pub async fn undelete_compat() { Id::from(u32::MAX), ); + admin.assert_authenticates("INBUXA_COMPAT_ADMIN").await; + for recorded in &items { let id = recorded["id"].as_str().unwrap(); let account = recorded["accountId"].as_str().unwrap(); diff --git a/tests/src/utils/account.rs b/tests/src/utils/account.rs index ccf8e3e..18e0cc7 100644 --- a/tests/src/utils/account.rs +++ b/tests/src/utils/account.rs @@ -91,6 +91,23 @@ impl Account { self.secret = new_secret; } + /// inbuxa: the compat tests all act as `INBUXA_COMPAT_ADMIN` against a + /// copy of INBUXA's data (docs/spec/compat-tests.md). If those + /// credentials don't authenticate, every later call comes back 401 and + /// fails as "Missing list in response", which reads like the data is + /// wrong rather than the login. Check once, and say which it is. + pub async fn assert_authenticates(&self, env_var: &str) { + let session = self.jmap_session_object().await; + if session.0.pointer("/accounts").is_none() { + panic!( + "{env_var} did not authenticate as {}: {}\n\ + The copy has to hold that administrator, with that password.", + self.name(), + session.0 + ); + } + } + pub fn id(&self) -> Id { self.id }