serial-test après fmt en vu dagger

EBO <eric.bouhana@softalys.com> committé le 2026-05-19 06:58

85537ac38c67395fa177ac5b037014832e32a46f

1 parent(s)

6 fichiers modifiés +107 -25
M Cargo.lock
+43 -0
@@ -4154,6 +4154,7 @@ dependencies = [
4154 4154 "sea-orm-migration",
4155 4155 "serde",
4156 4156 "serde_json",
4157 + "serial_test",
4157 4158 "sha2",
4158 4159 "subtle",
4159 4160 "thiserror 2.0.18",
@@ -4195,6 +4196,7 @@ dependencies = [
4195 4196 "sea-orm-migration",
4196 4197 "serde",
4197 4198 "serde_json",
4199 + "serial_test",
4198 4200 "sha2",
4199 4201 "subtle",
4200 4202 "thiserror 2.0.18",
@@ -4287,6 +4289,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
4287 4289 checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
4288 4290
4289 4291 [[package]]
4292 +name = "scc"
4293 +version = "2.4.0"
4294 +source = "registry+https://github.com/rust-lang/crates.io-index"
4295 +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc"
4296 +dependencies = [
4297 + "sdd",
4298 +]
4299 +
4300 +[[package]]
4290 4301 name = "schannel"
4291 4302 version = "0.1.28"
4292 4303 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4326,6 +4337,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
4326 4337 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
4327 4338
4328 4339 [[package]]
4340 +name = "sdd"
4341 +version = "3.0.10"
4342 +source = "registry+https://github.com/rust-lang/crates.io-index"
4343 +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
4344 +
4345 +[[package]]
4329 4346 name = "sea-bae"
4330 4347 version = "0.2.1"
4331 4348 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4667,6 +4684,32 @@ dependencies = [
4667 4684 ]
4668 4685
4669 4686 [[package]]
4687 +name = "serial_test"
4688 +version = "3.4.0"
4689 +source = "registry+https://github.com/rust-lang/crates.io-index"
4690 +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f"
4691 +dependencies = [
4692 + "futures-executor",
4693 + "futures-util",
4694 + "log",
4695 + "once_cell",
4696 + "parking_lot",
4697 + "scc",
4698 + "serial_test_derive",
4699 +]
4700 +
4701 +[[package]]
4702 +name = "serial_test_derive"
4703 +version = "3.4.0"
4704 +source = "registry+https://github.com/rust-lang/crates.io-index"
4705 +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9"
4706 +dependencies = [
4707 + "proc-macro2",
4708 + "quote",
4709 + "syn 2.0.117",
4710 +]
4711 +
4712 +[[package]]
4670 4713 name = "sha1"
4671 4714 version = "0.10.6"
4672 4715 source = "registry+https://github.com/rust-lang/crates.io-index"
M crates/rustwarden-core/Cargo.toml
+5 -0
@@ -106,3 +106,8 @@ wiremock = "0.6"
106 106 # E2E tests against the docker-compose fediverse testbed
107 107 # (tests/fediverse-testbed/). Used by `cargo test --test e2e_fediverse`.
108 108 reqwest = { version = "0.12", features = ["json"] }
109 +# Serialise tests that mutate process-wide environment variables
110 +# (ENCRYPTION_KEY, OAUTH_ENCRYPTION_KEY). Without this, parallel
111 +# unit tests can race on `env::set_var` and produce intermittent
112 +# failures (notably `services::crypto_service::tests::test_roundtrip`).
113 +serial_test = "3"
M crates/rustwarden-core/src/services/crypto_service.rs
+16 -3
@@ -102,11 +102,24 @@ impl CryptoService {
102 102 mod tests {
103 103 use super::*;
104 104
105 + /// Roundtrip encryption / decryption with a fixed test key.
106 + ///
107 + /// Mutating `OAUTH_ENCRYPTION_KEY` via `env::set_var` is **process-wide**.
108 + /// Other tests in this binary (notably in `totp_service`) also write
109 + /// `ENCRYPTION_KEY`, and any concurrent reader of either variable can
110 + /// observe the wrong value mid-flight. The previous "race is benign"
111 + /// comment was incorrect: an intermittent failure was reported on
112 + /// 2026-05-18 when this test ran alongside the broader workspace suite.
113 + ///
114 + /// The `#[serial(encryption_key)]` attribute (from `serial_test`)
115 + /// serialises every test annotated with the same key, eliminating the
116 + /// race without requiring `--test-threads=1`.
105 117 #[test]
118 + #[serial_test::serial(encryption_key)]
106 119 fn test_roundtrip() {
107 - // SAFETY: test fixture writing a stable constant key; concurrent tests
108 - // in this binary either set the same value or do not read this var,
109 - // so the race is benign in practice.
120 + // SAFETY: env mutation is serialised across the suite by
121 + // `#[serial(encryption_key)]`, so no other thread reads the var
122 + // while this block runs.
110 123 unsafe {
111 124 env::set_var(
112 125 "OAUTH_ENCRYPTION_KEY",
M crates/rustwarden-core/src/services/totp_service.rs
+18 -4
@@ -474,10 +474,18 @@ mod tests {
474 474 let _ = result;
475 475 }
476 476
477 + /// Generates and decrypts backup codes against a fixed test key.
478 + ///
479 + /// Backup-code generation calls `CryptoService::encrypt`, which reads
480 + /// `ENCRYPTION_KEY` from the process environment. `env::set_var` is
481 + /// process-wide, so concurrent tests (e.g. `crypto_service::test_roundtrip`)
482 + /// can observe a partial update. `#[serial(encryption_key)]` from
483 + /// `serial_test` serialises every test sharing this key, removing the race.
477 484 #[test]
485 + #[serial_test::serial(encryption_key)]
478 486 fn test_generate_backup_codes() {
479 - // SAFETY: test fixture writing a stable constant key; concurrent tests
480 - // in this binary either set the same value or do not read this var.
487 + // SAFETY: serialised by `#[serial(encryption_key)]`; no other test
488 + // touches ENCRYPTION_KEY while this block runs.
481 489 unsafe {
482 490 std::env::set_var(
483 491 "ENCRYPTION_KEY",
@@ -506,10 +514,16 @@ mod tests {
506 514 }
507 515 }
508 516
517 + /// Asserts each generated backup code is unique.
518 + ///
519 + /// Same rationale as `test_generate_backup_codes`: the test reads
520 + /// `ENCRYPTION_KEY` indirectly via `CryptoService::encrypt`, so it shares
521 + /// the `encryption_key` serialisation tag with every other env-mutating
522 + /// crypto test in this crate.
509 523 #[test]
524 + #[serial_test::serial(encryption_key)]
510 525 fn test_backup_codes_are_unique() {
511 - // SAFETY: test fixture writing a stable constant key; concurrent tests
512 - // in this binary either set the same value or do not read this var.
526 + // SAFETY: serialised by `#[serial(encryption_key)]`.
513 527 unsafe {
514 528 std::env::set_var(
515 529 "ENCRYPTION_KEY",
M crates/rustwarden-fediverse/Cargo.toml
+5 -0
@@ -56,6 +56,11 @@ workspace = true
56 56 [dev-dependencies]
57 57 sea-orm = { version = "2.0.0-rc.20", features = ["mock"] }
58 58 wiremock = "0.6"
59 +# Serialise tests that mutate process-wide environment variables
60 +# (ENCRYPTION_KEY, OAUTH_ENCRYPTION_KEY) so async tests on `MockDatabase`
61 +# cannot trample each other's env. Replaces the ad-hoc `Mutex<()>` lock
62 +# previously held across `.await` in `actor_key_service::tests`.
63 +serial_test = "3"
59 64
60 65 [[bin]]
61 66 name = "migrate-fediverse"
M crates/rustwarden-fediverse/src/services/actor_key_service.rs
+20 -18
@@ -130,20 +130,21 @@ impl ActorKeyService {
130 130 }
131 131
132 132 #[cfg(test)]
133 -// `ENV_LOCK` is intentionally held across `.await` so that two
134 -// async tests cannot trample each other's `ENCRYPTION_KEY` env var
135 -// while a SeaORM mock query is in flight. Tests on a `MockDatabase`
136 -// never block; the lock is in practice released within microseconds.
137 -#[allow(clippy::await_holding_lock)]
138 133 mod tests {
134 + // Tests in this module mutate `ENCRYPTION_KEY` (a process-wide env var)
135 + // before exercising async code paths that read it. Each affected test is
136 + // tagged `#[serial_test::serial(encryption_key)]`; the shared key
137 + // ensures no two such tests run concurrently, even with `tokio::test`
138 + // and Rust's default parallel test runner. This replaces a previous
139 + // `Mutex<()>` held across `.await`, which required
140 + // `#[allow(clippy::await_holding_lock)]` and only protected this module.
141 + // The `serial_test` tag is workspace-wide for the same key, so e.g.
142 + // `crypto_service::test_roundtrip` in rustwarden-core would also
143 + // serialise here if both were ever linked into the same test binary.
144 +
139 145 use super::*;
140 146 use chrono::Utc;
141 147 use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult};
142 - use std::sync::Mutex;
143 -
144 - /// Serialise tests that mutate `ENCRYPTION_KEY` so that one test setting
145 - /// the env var does not race with another test clearing it.
146 - static ENV_LOCK: Mutex<()> = Mutex::new(());
147 148
148 149 fn user_row(id: Uuid, public: Option<String>, private: Option<String>) -> user::Model {
149 150 let now = Utc::now();
@@ -168,7 +169,8 @@ mod tests {
168 169
169 170 fn set_test_encryption_key() {
170 171 // 64 hex chars => 32 bytes => AES-256 key.
171 - // SAFETY: tests run sequentially (--test-threads=1 in CI for this crate).
172 + // SAFETY: callers must be tagged `#[serial_test::serial(encryption_key)]`
173 + // so no other test reads/writes the env var concurrently.
172 174 unsafe {
173 175 std::env::set_var(
174 176 "ENCRYPTION_KEY",
@@ -178,7 +180,7 @@ mod tests {
178 180 }
179 181
180 182 fn clear_encryption_key() {
181 - // SAFETY: tests run sequentially.
183 + // SAFETY: callers must be tagged `#[serial_test::serial(encryption_key)]`.
182 184 unsafe {
183 185 std::env::remove_var("ENCRYPTION_KEY");
184 186 std::env::remove_var("OAUTH_ENCRYPTION_KEY");
@@ -273,8 +275,8 @@ mod tests {
273 275 }
274 276
275 277 #[tokio::test]
278 + #[serial_test::serial(encryption_key)]
276 279 async fn ensure_keys_returns_existing_when_present() {
277 - let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
278 280 set_test_encryption_key();
279 281 let user_id = Uuid::new_v4();
280 282 let public_pem = "-----BEGIN PUBLIC KEY-----\nstored\n-----END PUBLIC KEY-----\n";
@@ -298,8 +300,8 @@ mod tests {
298 300 }
299 301
300 302 #[tokio::test]
303 + #[serial_test::serial(encryption_key)]
301 304 async fn ensure_keys_generates_when_missing() {
302 - let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
303 305 set_test_encryption_key();
304 306 let user_id = Uuid::new_v4();
305 307 let initial = user_row(user_id, None, None);
@@ -323,8 +325,8 @@ mod tests {
323 325 }
324 326
325 327 #[tokio::test]
328 + #[serial_test::serial(encryption_key)]
326 329 async fn ensure_keys_persists_encrypted_private_key() {
327 - let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
328 330 set_test_encryption_key();
329 331 let user_id = Uuid::new_v4();
330 332 let initial = user_row(user_id, None, None);
@@ -347,8 +349,8 @@ mod tests {
347 349 }
348 350
349 351 #[tokio::test]
352 + #[serial_test::serial(encryption_key)]
350 353 async fn ensure_keys_returns_decrypted_private_pem() {
351 - let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
352 354 set_test_encryption_key();
353 355 let user_id = Uuid::new_v4();
354 356 let initial = user_row(user_id, None, None);
@@ -372,8 +374,8 @@ mod tests {
372 374 }
373 375
374 376 #[tokio::test]
377 + #[serial_test::serial(encryption_key)]
375 378 async fn ensure_keys_errors_when_encryption_key_missing() {
376 - let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
377 379 clear_encryption_key();
378 380 let user_id = Uuid::new_v4();
379 381 let db: DatabaseConnection = MockDatabase::new(DatabaseBackend::Postgres)
@@ -387,8 +389,8 @@ mod tests {
387 389 }
388 390
389 391 #[tokio::test]
392 + #[serial_test::serial(encryption_key)]
390 393 async fn ensure_keys_returns_error_when_user_missing() {
391 - let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
392 394 set_test_encryption_key();
393 395 let user_id = Uuid::new_v4();
394 396 let db: DatabaseConnection = MockDatabase::new(DatabaseBackend::Postgres)