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) |
GitRust