max / makenotwork
7 files changed,
+283 insertions,
-163 deletions
| @@ -98,6 +98,39 @@ curl -X POST http://127.0.0.1:7766/promote/a \ | |||
| 98 | 98 | -d '{"version":"0.8.2"}' | |
| 99 | 99 | ``` | |
| 100 | 100 | ||
| 101 | + | ## Gates | |
| 102 | + | ||
| 103 | + | Build-time gates run on the host tier, once per build, against the worktree. | |
| 104 | + | Promote-time gates run against a tier's deployed nodes or its operator. | |
| 105 | + | ||
| 106 | + | | Kind | When | What it proves | | |
| 107 | + | |------|------|----------------| | |
| 108 | + | | `code_smoke` | build | Boots the fresh binary on a throwaway DB it migrates from scratch and seeds, then probes `/health`. Runs first: green here isolates a later red as an environment problem, not a code one. | | |
| 109 | + | | `cargo_test` | build | The server suite under `--features fast-tests`. | | |
| 110 | + | | `hardening_test` | build | What `cargo_test` structurally cannot reach (see below). | | |
| 111 | + | | `migration_dry_run` | build | Migrations apply cleanly to a restored production dump. | | |
| 112 | + | | `boot_smoke` | build | The staged artifact boots in minimal no-DB mode on the build host. | | |
| 113 | + | | `node_health` | post-deploy | Each deployed node's unit is active (and serves 2xx if `health_url` is set). Recorded at the end of a promote as the evidence the next promote checks. | | |
| 114 | + | | `burn_in` | promote | The tier has held its current version for N hours. Evaluated live against the clock. | | |
| 115 | + | | `manual_confirm` | promote | An operator signed off, at or after this version landed on the tier. | | |
| 116 | + | ||
| 117 | + | ### Why `hardening_test` exists | |
| 118 | + | ||
| 119 | + | `cargo_test` builds with `--features fast-tests`, which relaxes | |
| 120 | + | `AUTH_RATE_LIMIT_BURST` (5 to 20), `SANDBOX_RATE_LIMIT_MS` (30s to 10ms), and | |
| 121 | + | argon2 (46 MiB/t=2 down to 8 MiB/t=1) so the signup-heavy workflow suite | |
| 122 | + | finishes in reasonable time. On top of that, the rate-limiting tests are | |
| 123 | + | `#[cfg_attr(feature = "fast-tests", ignore)]`d, because a bucket refilling at | |
| 124 | + | 100/sec never depletes under parallel test threads. | |
| 125 | + | ||
| 126 | + | The net effect was that Sando's only code gate skipped every test of the auth | |
| 127 | + | hardening it exists to protect. `hardening_test` re-runs that suite with no | |
| 128 | + | features, single-threaded, against production constants. It costs a second | |
| 129 | + | compile of the server's test binary, since a different feature set is a | |
| 130 | + | different cfg and shares no artifacts. It also fails closed if its name filter | |
| 131 | + | matches zero tests, so renaming the suite cannot quietly turn the gate into a | |
| 132 | + | green no-op. | |
| 133 | + | ||
| 101 | 134 | ## API | |
| 102 | 135 | ||
| 103 | 136 | | Method | Path | Body | Purpose | |
| @@ -237,6 +237,14 @@ impl<'r> sqlx::Decode<'r, Sqlite> for GitSha { | |||
| 237 | 237 | #[serde(rename_all = "snake_case")] | |
| 238 | 238 | pub enum GateKind { | |
| 239 | 239 | CargoTest, | |
| 240 | + | /// The tests `cargo_test` structurally cannot run: `--features fast-tests` | |
| 241 | + | /// relaxes the auth/sandbox rate limits and argon2 cost so the signup-heavy | |
| 242 | + | /// workflow suite finishes, and the rate-limiting tests are | |
| 243 | + | /// `#[cfg_attr(feature = "fast-tests", ignore)]`d because a bucket that | |
| 244 | + | /// refills at 100/sec never depletes under parallel test threads. This gate | |
| 245 | + | /// runs that set WITHOUT the feature, against the production constants, so a | |
| 246 | + | /// rate-limiter regression cannot reach a node unblocked. | |
| 247 | + | HardeningTest, | |
| 240 | 248 | MigrationDryRun, | |
| 241 | 249 | /// Boots the freshly-built binary against a throwaway *empty* DB it | |
| 242 | 250 | /// migrates from scratch, seeds the example catalog into, and probes | |
| @@ -259,6 +267,7 @@ impl GateKind { | |||
| 259 | 267 | pub fn as_str(self) -> &'static str { | |
| 260 | 268 | match self { | |
| 261 | 269 | GateKind::CargoTest => "cargo_test", | |
| 270 | + | GateKind::HardeningTest => "hardening_test", | |
| 262 | 271 | GateKind::MigrationDryRun => "migration_dry_run", | |
| 263 | 272 | GateKind::CodeSmoke => "code_smoke", | |
| 264 | 273 | GateKind::BootSmoke => "boot_smoke", | |
| @@ -278,6 +287,7 @@ impl FromStr for GateKind { | |||
| 278 | 287 | fn from_str(s: &str) -> Result<Self, Self::Err> { | |
| 279 | 288 | match s { | |
| 280 | 289 | "cargo_test" => Ok(GateKind::CargoTest), | |
| 290 | + | "hardening_test" => Ok(GateKind::HardeningTest), | |
| 281 | 291 | "migration_dry_run" => Ok(GateKind::MigrationDryRun), | |
| 282 | 292 | "code_smoke" => Ok(GateKind::CodeSmoke), | |
| 283 | 293 | "boot_smoke" => Ok(GateKind::BootSmoke), | |
| @@ -427,6 +437,7 @@ mod tests { | |||
| 427 | 437 | // so step 3 (events use the types) doesn't change the wire shape. | |
| 428 | 438 | for k in [ | |
| 429 | 439 | GateKind::CargoTest, | |
| 440 | + | GateKind::HardeningTest, | |
| 430 | 441 | GateKind::MigrationDryRun, | |
| 431 | 442 | GateKind::CodeSmoke, | |
| 432 | 443 | GateKind::BootSmoke, | |
| @@ -442,6 +453,14 @@ mod tests { | |||
| 442 | 453 | } | |
| 443 | 454 | ||
| 444 | 455 | #[test] | |
| 456 | + | fn gate_kind_hardening_test_round_trips() { | |
| 457 | + | // It is written into gate_runs.gate_kind and read back by | |
| 458 | + | // unsatisfied_gates, so the two directions must agree exactly. | |
| 459 | + | assert_eq!(GateKind::HardeningTest.as_str(), "hardening_test"); | |
| 460 | + | assert_eq!("hardening_test".parse::<GateKind>().unwrap(), GateKind::HardeningTest); | |
| 461 | + | } | |
| 462 | + | ||
| 463 | + | #[test] | |
| 445 | 464 | fn gate_kind_from_str_rejects_unknown() { | |
| 446 | 465 | assert!("not_a_gate".parse::<GateKind>().is_err()); | |
| 447 | 466 | } |
| @@ -74,6 +74,8 @@ pub async fn run(ctx: &GateCtx, gate: &Gate) -> Result<GateOutcome> { | |||
| 74 | 74 | let outcome = match gate { | |
| 75 | 75 | // cargo_test bounds its own run internally (it kills the specific child). | |
| 76 | 76 | Gate::CargoTest => cargo_test(ctx, run_id).await, | |
| 77 | + | // hardening_test bounds its own run internally, same as cargo_test. | |
| 78 | + | Gate::HardeningTest => hardening_test(ctx, run_id).await, | |
| 77 | 79 | // migration_dry_run's psql restore + sqlx migrate could wedge; bound the | |
| 78 | 80 | // whole gate here. Its bash restore sets kill_on_drop, so a timeout-drop | |
| 79 | 81 | // doesn't orphan it. | |
| @@ -195,7 +197,7 @@ async fn cargo_test(ctx: &GateCtx, run_id: GateRunId) -> Result<GateOutcome> { | |||
| 195 | 197 | // `#[cfg(test)]`-only binary like `load`) otherwise compiles fine under | |
| 196 | 198 | // the build step + `--test integration` and only blows up here, after a | |
| 197 | 199 | // full build, as an opaque mass test failure. | |
| 198 | - | let mut pre = match cargo_test_command(ctx, &server_dir, &["--no-run"]).spawn() { | |
| 200 | + | let mut pre = match cargo_test_command(ctx, &server_dir, &["fast-tests"], &["--no-run"]).spawn() { | |
| 199 | 201 | Ok(c) => c, | |
| 200 | 202 | Err(e) => { | |
| 201 | 203 | return Ok(GateOutcome::failed(GateFailure::SpawnFailed { | |
| @@ -212,7 +214,7 @@ async fn cargo_test(ctx: &GateCtx, run_id: GateRunId) -> Result<GateOutcome> { | |||
| 212 | 214 | ||
| 213 | 215 | // Full run: the test binaries are already built above, so cargo's | |
| 214 | 216 | // up-to-date check skips compilation and this just runs the tests. | |
| 215 | - | let mut child = match cargo_test_command(ctx, &server_dir, &[]).spawn() { | |
| 217 | + | let mut child = match cargo_test_command(ctx, &server_dir, &["fast-tests"], &[]).spawn() { | |
| 216 | 218 | Ok(c) => c, | |
| 217 | 219 | Err(e) => { | |
| 218 | 220 | return Ok(GateOutcome::failed(GateFailure::SpawnFailed { | |
| @@ -246,18 +248,138 @@ async fn cargo_test(ctx: &GateCtx, run_id: GateRunId) -> Result<GateOutcome> { | |||
| 246 | 248 | } | |
| 247 | 249 | } | |
| 248 | 250 | ||
| 249 | - | /// Configure (but don't spawn) `cargo test --release --features fast-tests | |
| 251 | + | /// The tests `cargo_test` cannot reach, run against production constants. | |
| 252 | + | /// | |
| 253 | + | /// `cargo_test` builds with `--features fast-tests`, which relaxes | |
| 254 | + | /// `AUTH_RATE_LIMIT_BURST` 5 → 20, `SANDBOX_RATE_LIMIT_MS` 30s → 10ms, and | |
| 255 | + | /// argon2 from 46 MiB/t=2 to 8 MiB/t=1 — and the rate-limiting suite is | |
| 256 | + | /// `#[cfg_attr(feature = "fast-tests", ignore)]`d on top of that, because a | |
| 257 | + | /// bucket refilling at 100/sec never depletes under parallel test threads. The | |
| 258 | + | /// net effect was that Sando's only code gate silently skipped every test of | |
| 259 | + | /// the auth hardening it most needs to protect. | |
| 260 | + | /// | |
| 261 | + | /// So: no features, `--test-threads=1` (these tests key on a shared per-IP | |
| 262 | + | /// bucket and must not interleave), and a name filter rather than the whole | |
| 263 | + | /// suite — the rest of the suite is tuned for `fast-tests` and would only go | |
| 264 | + | /// slow and flaky here. The filter is a substring match, so it catches | |
| 265 | + | /// `..._rate_limit_...` and `..._rate_limited` alike. | |
| 266 | + | /// | |
| 267 | + | /// This costs a second compile of the lib + integration binary (a different | |
| 268 | + | /// feature set is a different cfg, so no artifact sharing with `cargo_test`). | |
| 269 | + | /// That is the price of the coverage; the filter keeps the *run* to seconds. | |
| 270 | + | async fn hardening_test(ctx: &GateCtx, run_id: GateRunId) -> Result<GateOutcome> { | |
| 271 | + | let server_dir = ctx.worktree.join("server"); | |
| 272 | + | let log_path = gate_log_path(ctx, GateKind::HardeningTest); | |
| 273 | + | let log_ref = LogRef::new(&ctx.version, GateKind::HardeningTest); | |
| 274 | + | ||
| 275 | + | if let Some(scratch_url) = ctx.cfg.scratch_db_url.as_deref() { | |
| 276 | + | clean_stale_test_dbs(scratch_url).await; | |
| 277 | + | } | |
| 278 | + | ||
| 279 | + | let started = std::time::Instant::now(); | |
| 280 | + | ||
| 281 | + | // Same two-step shape as cargo_test: compile first so a test-target break | |
| 282 | + | // reports as a compile error rather than an opaque mass test failure. | |
| 283 | + | let mut pre = match cargo_test_command(ctx, &server_dir, &[], &["--no-run", "--test", "integration"]).spawn() { | |
| 284 | + | Ok(c) => c, | |
| 285 | + | Err(e) => { | |
| 286 | + | return Ok(GateOutcome::failed(GateFailure::SpawnFailed { | |
| 287 | + | message: e.to_string(), | |
| 288 | + | }).with_log_ref(log_ref)); | |
| 289 | + | } | |
| 290 | + | }; | |
| 291 | + | let (pre_out, pre_err, pre_status) = | |
| 292 | + | stream_child_to_live_log(&mut pre, ctx.events.clone(), run_id, log_path.clone()).await?; | |
| 293 | + | if !pre_status.success() { | |
| 294 | + | let failure = classify::classify_compile_error(&pre_out, &pre_err); | |
| 295 | + | return Ok(GateOutcome::failed(failure).with_log_ref(log_ref)); | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | let mut child = match cargo_test_command( | |
| 299 | + | ctx, | |
| 300 | + | &server_dir, | |
| 301 | + | &[], | |
| 302 | + | &["--test", "integration", "--", "--test-threads=1", "rate_limit"], | |
| 303 | + | ) | |
| 304 | + | .spawn() | |
| 305 | + | { | |
| 306 | + | Ok(c) => c, | |
| 307 | + | Err(e) => { | |
| 308 | + | return Ok(GateOutcome::failed(GateFailure::SpawnFailed { | |
| 309 | + | message: e.to_string(), | |
| 310 | + | }).with_log_ref(log_ref)); | |
| 311 | + | } | |
| 312 | + | }; | |
| 313 | + | let ceiling = std::time::Duration::from_secs(ctx.cfg.gate_timeout_secs); | |
| 314 | + | let stream = stream_child_to_live_log(&mut child, ctx.events.clone(), run_id, log_path); | |
| 315 | + | let (stdout_buf, stderr_buf, status) = match tokio::time::timeout(ceiling, stream).await { | |
| 316 | + | Ok(res) => res?, | |
| 317 | + | Err(_elapsed) => { | |
| 318 | + | child.start_kill().ok(); | |
| 319 | + | let _ = child.wait().await; | |
| 320 | + | return Ok(GateOutcome::failed(GateFailure::Timeout { | |
| 321 | + | gate: GateKind::HardeningTest, | |
| 322 | + | after_s: started.elapsed().as_secs() as u32, | |
| 323 | + | }) | |
| 324 | + | .with_log_ref(log_ref)); | |
| 325 | + | } | |
| 326 | + | }; | |
| 327 | + | let duration_s = started.elapsed().as_secs() as u32; | |
| 328 | + | if status.success() { | |
| 329 | + | // A filter that matches nothing exits 0, which would make this gate a | |
| 330 | + | // green no-op the day someone renames the tests. Fail closed instead. | |
| 331 | + | if tests_run(&stdout_buf) == 0 { | |
| 332 | + | return Ok(GateOutcome::failed(GateFailure::Unclassified { | |
| 333 | + | legacy_detail: Some( | |
| 334 | + | "hardening_test ran 0 tests: the `rate_limit` filter matched nothing. \ | |
| 335 | + | The suite was renamed or moved — this gate is proving nothing." | |
| 336 | + | .into(), | |
| 337 | + | ), | |
| 338 | + | }) | |
| 339 | + | .with_log_ref(log_ref)); | |
| 340 | + | } | |
| 341 | + | Ok(GateOutcome::passed(PassNote::TestsPassed { duration_s }).with_log_ref(log_ref)) | |
| 342 | + | } else { | |
| 343 | + | let failure = classify::classify_cargo_test(&stdout_buf, &stderr_buf); | |
| 344 | + | Ok(GateOutcome::failed(failure).with_log_ref(log_ref)) | |
| 345 | + | } | |
| 346 | + | } | |
| 347 | + | ||
| 348 | + | /// Count the tests libtest reports as run, from its `test result: ok. N passed` | |
| 349 | + | /// summary line. Returns 0 when no summary is present, which is itself the | |
| 350 | + | /// "nothing ran" case the caller fails on. | |
| 351 | + | fn tests_run(stdout: &[u8]) -> u32 { | |
| 352 | + | String::from_utf8_lossy(stdout) | |
| 353 | + | .lines() | |
| 354 | + | .filter_map(|l| l.trim().strip_prefix("test result:")) | |
| 355 | + | .filter_map(|rest| rest.split_once(" passed")) | |
| 356 | + | .filter_map(|(head, _)| head.rsplit(' ').find(|t| !t.is_empty())?.parse::<u32>().ok()) | |
| 357 | + | .sum() | |
| 358 | + | } | |
| 359 | + | ||
| 360 | + | /// Configure (but don't spawn) `cargo test --release [--features <features>] | |
| 250 | 361 | /// <extra>` in `dir`, wired to the scratch DB. Shared by the `--no-run` | |
| 251 | - | /// pre-gate compile and the full test run so both go through one env setup. | |
| 362 | + | /// pre-gate compile and the full test run so both go through one env setup, | |
| 363 | + | /// and by both test gates so they can't drift apart on env. | |
| 252 | 364 | /// | |
| 253 | - | /// `--features fast-tests` matches CI (`server/deploy/run-ci.sh`): it relaxes | |
| 254 | - | /// the auth rate-limit burst (5 → 20) and argon2 cost so signup-heavy + | |
| 255 | - | /// lockout workflow tests complete without hitting Governor before the | |
| 256 | - | /// hand-rolled lockout check (documented at `server/src/constants.rs:87`). | |
| 257 | - | fn cargo_test_command(ctx: &GateCtx, dir: &std::path::Path, extra: &[&str]) -> Command { | |
| 365 | + | /// `cargo_test` passes `fast-tests`, matching CI (`server/deploy/run-ci.sh`): | |
| 366 | + | /// it relaxes the auth rate-limit burst (5 → 20) and argon2 cost so | |
| 367 | + | /// signup-heavy + lockout workflow tests complete without hitting Governor | |
| 368 | + | /// before the hand-rolled lockout check (documented at | |
| 369 | + | /// `server/src/constants.rs:87`). `hardening_test` passes no features, which is | |
| 370 | + | /// the whole point of that gate — see `GateKind::HardeningTest`. | |
| 371 | + | fn cargo_test_command( | |
| 372 | + | ctx: &GateCtx, | |
| 373 | + | dir: &std::path::Path, | |
| 374 | + | features: &[&str], | |
| 375 | + | extra: &[&str], | |
| 376 | + | ) -> Command { | |
| 258 | 377 | let mut cmd = Command::new("cargo"); | |
| 259 | - | cmd.args(["test", "--release", "--features", "fast-tests"]) | |
| 260 | - | .args(extra) | |
| 378 | + | cmd.args(["test", "--release"]); | |
| 379 | + | if !features.is_empty() { | |
| 380 | + | cmd.args(["--features", &features.join(",")]); | |
| 381 | + | } | |
| 382 | + | cmd.args(extra) | |
| 261 | 383 | .current_dir(dir) | |
| 262 | 384 | .stdout(std::process::Stdio::piped()) | |
| 263 | 385 | .stderr(std::process::Stdio::piped()) | |
| @@ -1330,6 +1452,56 @@ mod tests { | |||
| 1330 | 1452 | use crate::events; | |
| 1331 | 1453 | use sqlx::sqlite::SqlitePoolOptions; | |
| 1332 | 1454 | ||
| 1455 | + | #[test] | |
| 1456 | + | fn tests_run_reads_the_libtest_summary() { | |
| 1457 | + | let out = b"running 6 tests\ntest auth_rate_limit_triggers_on_burst ... ok\n\n\ | |
| 1458 | + | test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 118 filtered out\n"; | |
| 1459 | + | assert_eq!(tests_run(out), 6); | |
| 1460 | + | } | |
| 1461 | + | ||
| 1462 | + | #[test] | |
| 1463 | + | fn tests_run_sums_across_test_binaries() { | |
| 1464 | + | let out = b"test result: ok. 6 passed; 0 failed\ntest result: ok. 2 passed; 0 failed\n"; | |
| 1465 | + | assert_eq!(tests_run(out), 8); | |
| 1466 | + | } | |
| 1467 | + | ||
| 1468 | + | #[test] | |
| 1469 | + | fn tests_run_is_zero_when_the_filter_matched_nothing() { | |
| 1470 | + | // The case hardening_test fails closed on: a filter matching no tests | |
| 1471 | + | // exits 0, so a rename would otherwise make the gate a green no-op. | |
| 1472 | + | let out = b"running 0 tests\n\n\ | |
| 1473 | + | test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 124 filtered out\n"; | |
| 1474 | + | assert_eq!(tests_run(out), 0); | |
| 1475 | + | } | |
| 1476 | + | ||
| 1477 | + | #[test] | |
| 1478 | + | fn tests_run_is_zero_without_a_summary_line() { | |
| 1479 | + | assert_eq!(tests_run(b"error: could not compile `makenotwork`\n"), 0); | |
| 1480 | + | } | |
| 1481 | + | ||
| 1482 | + | #[tokio::test] | |
| 1483 | + | async fn hardening_test_command_carries_no_features() { | |
| 1484 | + | // The entire point of the gate: production constants, which means no | |
| 1485 | + | // `fast-tests`. A stray feature here silently restores the blind spot. | |
| 1486 | + | let ctx = GateCtx { | |
| 1487 | + | pool: SqlitePoolOptions::new().max_connections(1).connect_lazy("sqlite::memory:").unwrap(), | |
| 1488 | + | cfg: std::sync::Arc::new(crate::config::Config::for_tests()), | |
| 1489 | + | tier: TierId::new("host"), | |
| 1490 | + | version: "0.1.0".parse().unwrap(), | |
| 1491 | + | worktree: std::path::PathBuf::from("/tmp/wt"), | |
| 1492 | + | events: events::channel(), | |
| 1493 | + | nodes: Vec::new(), | |
| 1494 | + | }; | |
| 1495 | + | let cmd = cargo_test_command(&ctx, std::path::Path::new("/tmp/wt/server"), &[], &["--test", "integration"]); | |
| 1496 | + | let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_string_lossy().into_owned()).collect(); | |
| 1497 | + | assert!(!args.iter().any(|a| a == "--features"), "hardening_test must pass no features: {args:?}"); | |
| 1498 | + | assert!(!args.iter().any(|a| a.contains("fast-tests")), "got: {args:?}"); | |
| 1499 | + | ||
| 1500 | + | let fast = cargo_test_command(&ctx, std::path::Path::new("/tmp/wt/server"), &["fast-tests"], &[]); | |
| 1501 | + | let fast_args: Vec<_> = fast.as_std().get_args().map(|a| a.to_string_lossy().into_owned()).collect(); | |
| 1502 | + | assert!(fast_args.windows(2).any(|w| w == ["--features", "fast-tests"]), "got: {fast_args:?}"); | |
| 1503 | + | } | |
| 1504 | + | ||
| 1333 | 1505 | /// Spawn a one-shot loopback server that answers the first connection with | |
| 1334 | 1506 | /// `status_line` + a tiny body, then closes. Returns the bound port. | |
| 1335 | 1507 | async fn oneshot_http(status_line: &'static str) -> u16 { |
| @@ -479,7 +479,8 @@ pub(super) async fn unsatisfied_gates( | |||
| 479 | 479 | // compile error here until its promotion semantics are decided — | |
| 480 | 480 | // a transient-`blocked` kind silently falling into "needs a passed | |
| 481 | 481 | // row" would be permanently unsatisfiable. | |
| 482 | - | Gate::CargoTest | Gate::MigrationDryRun | Gate::CodeSmoke | Gate::BootSmoke | Gate::NodeHealth => { | |
| 482 | + | Gate::CargoTest | Gate::HardeningTest | Gate::MigrationDryRun | Gate::CodeSmoke | |
| 483 | + | | Gate::BootSmoke | Gate::NodeHealth => { | |
| 483 | 484 | // Latest row for this configured gate kind; NULL/missing/any | |
| 484 | 485 | // non-'passed' status all count as unsatisfied (fail closed). | |
| 485 | 486 | let status: Option<String> = sqlx::query_scalar( |
| @@ -129,6 +129,7 @@ impl CanaryPolicy { | |||
| 129 | 129 | #[serde(tag = "kind", rename_all = "snake_case")] | |
| 130 | 130 | pub enum Gate { | |
| 131 | 131 | CargoTest, | |
| 132 | + | HardeningTest, | |
| 132 | 133 | MigrationDryRun, | |
| 133 | 134 | CodeSmoke, | |
| 134 | 135 | BootSmoke, | |
| @@ -144,6 +145,7 @@ impl Gate { | |||
| 144 | 145 | pub fn kind(&self) -> GateKind { | |
| 145 | 146 | match self { | |
| 146 | 147 | Gate::CargoTest => GateKind::CargoTest, | |
| 148 | + | Gate::HardeningTest => GateKind::HardeningTest, | |
| 147 | 149 | Gate::MigrationDryRun => GateKind::MigrationDryRun, | |
| 148 | 150 | Gate::CodeSmoke => GateKind::CodeSmoke, | |
| 149 | 151 | Gate::BootSmoke => GateKind::BootSmoke, |
| @@ -38,9 +38,18 @@ canary = "sequential" | |||
| 38 | 38 | # /health. Fast + infra-light (no prod-dump restore), so a green here proves the | |
| 39 | 39 | # code is sound and isolates a later cargo_test / migration_dry_run red as an | |
| 40 | 40 | # environment problem rather than a code one. | |
| 41 | + | # | |
| 42 | + | # hardening_test is cargo_test's blind spot: cargo_test builds with | |
| 43 | + | # --features fast-tests, which relaxes the auth/sandbox rate limits and argon2 | |
| 44 | + | # cost AND #[ignore]s the whole rate-limiting suite, so the gate never touched | |
| 45 | + | # the auth hardening it exists to protect. hardening_test re-runs that suite | |
| 46 | + | # with no features, single-threaded, against production constants. It pays for | |
| 47 | + | # a second compile of the server's test binary; that is the cost of the | |
| 48 | + | # coverage. | |
| 41 | 49 | gates = [ | |
| 42 | 50 | { kind = "code_smoke" }, | |
| 43 | 51 | { kind = "cargo_test" }, | |
| 52 | + | { kind = "hardening_test" }, | |
| 44 | 53 | { kind = "migration_dry_run" }, | |
| 45 | 54 | { kind = "boot_smoke" }, | |
| 46 | 55 | ] |
| @@ -483,7 +483,7 @@ dependencies = [ | |||
| 483 | 483 | "async-stripe-shared", | |
| 484 | 484 | "bytes", | |
| 485 | 485 | "http-body-util", | |
| 486 | - | "hyper 1.10.1", | |
| 486 | + | "hyper", | |
| 487 | 487 | "hyper-tls", | |
| 488 | 488 | "hyper-util", | |
| 489 | 489 | "miniserde", | |
| @@ -998,23 +998,17 @@ dependencies = [ | |||
| 998 | 998 | "aws-smithy-async", | |
| 999 | 999 | "aws-smithy-runtime-api", | |
| 1000 | 1000 | "aws-smithy-types", | |
| 1001 | - | "h2 0.3.27", | |
| 1002 | - | "h2 0.4.15", | |
| 1003 | - | "http 0.2.12", | |
| 1001 | + | "h2", | |
| 1004 | 1002 | "http 1.4.2", | |
| 1005 | - | "http-body 0.4.6", | |
| 1006 | - | "hyper 0.14.32", | |
| 1007 | - | "hyper 1.10.1", | |
| 1008 | - | "hyper-rustls 0.24.2", | |
| 1009 | - | "hyper-rustls 0.27.9", | |
| 1003 | + | "hyper", | |
| 1004 | + | "hyper-rustls", | |
| 1010 | 1005 | "hyper-util", | |
| 1011 | 1006 | "pin-project-lite", | |
| 1012 | - | "rustls 0.21.12", | |
| 1013 | - | "rustls 0.23.41", | |
| 1007 | + | "rustls", | |
| 1014 | 1008 | "rustls-native-certs 0.8.4", | |
| 1015 | 1009 | "rustls-pki-types", | |
| 1016 | 1010 | "tokio", | |
| 1017 | - | "tokio-rustls 0.26.4", | |
| 1011 | + | "tokio-rustls", | |
| 1018 | 1012 | "tower", | |
| 1019 | 1013 | "tracing", | |
| 1020 | 1014 | ] | |
| @@ -1179,7 +1173,7 @@ dependencies = [ | |||
| 1179 | 1173 | "http 1.4.2", | |
| 1180 | 1174 | "http-body 1.0.1", | |
| 1181 | 1175 | "http-body-util", | |
| 1182 | - | "hyper 1.10.1", | |
| 1176 | + | "hyper", | |
| 1183 | 1177 | "hyper-util", | |
| 1184 | 1178 | "itoa", | |
| 1185 | 1179 | "matchit", | |
| @@ -3283,25 +3277,6 @@ dependencies = [ | |||
| 3283 | 3277 | ||
| 3284 | 3278 | [[package]] | |
| 3285 | 3279 | name = "h2" | |
| 3286 | - | version = "0.3.27" | |
| 3287 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3288 | - | checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" | |
| 3289 | - | dependencies = [ | |
| 3290 | - | "bytes", | |
| 3291 | - | "fnv", | |
| 3292 | - | "futures-core", | |
| 3293 | - | "futures-sink", | |
| 3294 | - | "futures-util", | |
| 3295 | - | "http 0.2.12", | |
| 3296 | - | "indexmap", | |
| 3297 | - | "slab", | |
| 3298 | - | "tokio", | |
| 3299 | - | "tokio-util", | |
| 3300 | - | "tracing", | |
| 3301 | - | ] | |
| 3302 | - | ||
| 3303 | - | [[package]] | |
| 3304 | - | name = "h2" | |
| 3305 | 3280 | version = "0.4.15" | |
| 3306 | 3281 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3307 | 3282 | checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" | |
| @@ -3556,30 +3531,6 @@ dependencies = [ | |||
| 3556 | 3531 | ||
| 3557 | 3532 | [[package]] | |
| 3558 | 3533 | name = "hyper" | |
| 3559 | - | version = "0.14.32" | |
| 3560 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3561 | - | checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" | |
| 3562 | - | dependencies = [ | |
| 3563 | - | "bytes", | |
| 3564 | - | "futures-channel", | |
| 3565 | - | "futures-core", | |
| 3566 | - | "futures-util", | |
| 3567 | - | "h2 0.3.27", | |
| 3568 | - | "http 0.2.12", | |
| 3569 | - | "http-body 0.4.6", | |
| 3570 | - | "httparse", | |
| 3571 | - | "httpdate", | |
| 3572 | - | "itoa", | |
| 3573 | - | "pin-project-lite", | |
| 3574 | - | "socket2 0.5.10", | |
| 3575 | - | "tokio", | |
| 3576 | - | "tower-service", | |
| 3577 | - | "tracing", | |
| 3578 | - | "want", | |
| 3579 | - | ] | |
| 3580 | - | ||
| 3581 | - | [[package]] | |
| 3582 | - | name = "hyper" | |
| 3583 | 3534 | version = "1.10.1" | |
| 3584 | 3535 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3585 | 3536 | checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" | |
| @@ -3588,7 +3539,7 @@ dependencies = [ | |||
| 3588 | 3539 | "bytes", | |
| 3589 | 3540 | "futures-channel", | |
| 3590 | 3541 | "futures-core", | |
| 3591 | - | "h2 0.4.15", | |
| 3542 | + | "h2", | |
| 3592 | 3543 | "http 1.4.2", | |
| 3593 | 3544 | "http-body 1.0.1", | |
| 3594 | 3545 | "httparse", | |
| @@ -3602,32 +3553,17 @@ dependencies = [ | |||
| 3602 | 3553 | ||
| 3603 | 3554 | [[package]] | |
| 3604 | 3555 | name = "hyper-rustls" | |
| 3605 | - | version = "0.24.2" | |
| 3606 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3607 | - | checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" | |
| 3608 | - | dependencies = [ | |
| 3609 | - | "futures-util", | |
| 3610 | - | "http 0.2.12", | |
| 3611 | - | "hyper 0.14.32", | |
| 3612 | - | "log", | |
| 3613 | - | "rustls 0.21.12", | |
| 3614 | - | "tokio", | |
| 3615 | - | "tokio-rustls 0.24.1", | |
| 3616 | - | ] | |
| 3617 | - | ||
| 3618 | - | [[package]] | |
| 3619 | - | name = "hyper-rustls" | |
| 3620 | 3556 | version = "0.27.9" | |
| 3621 | 3557 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3622 | 3558 | checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" | |
| 3623 | 3559 | dependencies = [ | |
| 3624 | 3560 | "http 1.4.2", | |
| 3625 | - | "hyper 1.10.1", | |
| 3561 | + | "hyper", | |
| 3626 | 3562 | "hyper-util", | |
| 3627 | - | "rustls 0.23.41", | |
| 3563 | + | "rustls", | |
| 3628 | 3564 | "rustls-native-certs 0.8.4", | |
| 3629 | 3565 | "tokio", | |
| 3630 | - | "tokio-rustls 0.26.4", | |
| 3566 | + | "tokio-rustls", | |
| 3631 | 3567 | "tower-service", | |
| 3632 | 3568 | "webpki-roots", | |
| 3633 | 3569 | ] | |
| @@ -3640,7 +3576,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" | |||
| 3640 | 3576 | dependencies = [ | |
| 3641 | 3577 | "bytes", | |
| 3642 | 3578 | "http-body-util", | |
| 3643 | - | "hyper 1.10.1", | |
| 3579 | + | "hyper", | |
| 3644 | 3580 | "hyper-util", | |
| 3645 | 3581 | "native-tls", | |
| 3646 | 3582 | "tokio", | |
| @@ -3660,12 +3596,12 @@ dependencies = [ | |||
| 3660 | 3596 | "futures-util", | |
| 3661 | 3597 | "http 1.4.2", | |
| 3662 | 3598 | "http-body 1.0.1", | |
| 3663 | - | "hyper 1.10.1", | |
| 3599 | + | "hyper", | |
| 3664 | 3600 | "ipnet", | |
| 3665 | 3601 | "libc", | |
| 3666 | 3602 | "percent-encoding", | |
| 3667 | 3603 | "pin-project-lite", | |
| 3668 | - | "socket2 0.6.4", | |
| 3604 | + | "socket2", | |
| 3669 | 3605 | "system-configuration", | |
| 3670 | 3606 | "tokio", | |
| 3671 | 3607 | "tower-service", | |
| @@ -5686,8 +5622,8 @@ dependencies = [ | |||
| 5686 | 5622 | "quinn-proto", | |
| 5687 | 5623 | "quinn-udp", | |
| 5688 | 5624 | "rustc-hash 2.1.2", | |
| 5689 | - | "rustls 0.23.41", | |
| 5690 | - | "socket2 0.6.4", | |
| 5625 | + | "rustls", | |
| 5626 | + | "socket2", | |
| 5691 | 5627 | "thiserror 2.0.18", | |
| 5692 | 5628 | "tokio", | |
| 5693 | 5629 | "tracing", | |
| @@ -5707,7 +5643,7 @@ dependencies = [ | |||
| 5707 | 5643 | "rand 0.9.4", | |
| 5708 | 5644 | "ring", | |
| 5709 | 5645 | "rustc-hash 2.1.2", | |
| 5710 | - | "rustls 0.23.41", | |
| 5646 | + | "rustls", | |
| 5711 | 5647 | "rustls-pki-types", | |
| 5712 | 5648 | "slab", | |
| 5713 | 5649 | "thiserror 2.0.18", | |
| @@ -5725,7 +5661,7 @@ dependencies = [ | |||
| 5725 | 5661 | "cfg_aliases", | |
| 5726 | 5662 | "libc", | |
| 5727 | 5663 | "once_cell", | |
| 5728 | - | "socket2 0.6.4", | |
| 5664 | + | "socket2", | |
| 5729 | 5665 | "tracing", | |
| 5730 | 5666 | "windows-sys 0.60.2", | |
| 5731 | 5667 | ] | |
| @@ -6026,19 +5962,19 @@ dependencies = [ | |||
| 6026 | 5962 | "futures-channel", | |
| 6027 | 5963 | "futures-core", | |
| 6028 | 5964 | "futures-util", | |
| 6029 | - | "h2 0.4.15", | |
| 5965 | + | "h2", | |
| 6030 | 5966 | "http 1.4.2", | |
| 6031 | 5967 | "http-body 1.0.1", | |
| 6032 | 5968 | "http-body-util", | |
| 6033 | - | "hyper 1.10.1", | |
| 6034 | - | "hyper-rustls 0.27.9", | |
| 5969 | + | "hyper", | |
| 5970 | + | "hyper-rustls", | |
| 6035 | 5971 | "hyper-util", | |
| 6036 | 5972 | "js-sys", | |
| 6037 | 5973 | "log", | |
| 6038 | 5974 | "percent-encoding", | |
| 6039 | 5975 | "pin-project-lite", | |
| 6040 | 5976 | "quinn", | |
| 6041 | - | "rustls 0.23.41", | |
| 5977 | + | "rustls", | |
| 6042 | 5978 | "rustls-native-certs 0.8.4", | |
| 6043 | 5979 | "rustls-pki-types", | |
| 6044 | 5980 | "serde", | |
| @@ -6046,7 +5982,7 @@ dependencies = [ | |||
| 6046 | 5982 | "serde_urlencoded", | |
| 6047 | 5983 | "sync_wrapper", | |
| 6048 | 5984 | "tokio", | |
| 6049 | - | "tokio-rustls 0.26.4", | |
| 5985 | + | "tokio-rustls", | |
| 6050 | 5986 | "tower", | |
| 6051 | 5987 | "tower-http", | |
| 6052 | 5988 | "tower-service", | |
| @@ -6069,12 +6005,12 @@ dependencies = [ | |||
| 6069 | 6005 | "cookie_store", | |
| 6070 | 6006 | "encoding_rs", | |
| 6071 | 6007 | "futures-core", | |
| 6072 | - | "h2 0.4.15", | |
| 6008 | + | "h2", | |
| 6073 | 6009 | "http 1.4.2", | |
| 6074 | 6010 | "http-body 1.0.1", | |
| 6075 | 6011 | "http-body-util", | |
| 6076 | - | "hyper 1.10.1", | |
| 6077 | - | "hyper-rustls 0.27.9", | |
| 6012 | + | "hyper", | |
| 6013 | + | "hyper-rustls", | |
| 6078 | 6014 | "hyper-util", | |
| 6079 | 6015 | "js-sys", | |
| 6080 | 6016 | "log", | |
| @@ -6082,7 +6018,7 @@ dependencies = [ | |||
| 6082 | 6018 | "percent-encoding", | |
| 6083 | 6019 | "pin-project-lite", | |
| 6084 | 6020 | "quinn", | |
| 6085 | - | "rustls 0.23.41", | |
| 6021 | + | "rustls", | |
| 6086 | 6022 | "rustls-pki-types", | |
| 6087 | 6023 | "rustls-platform-verifier", | |
| 6088 | 6024 | "serde", | |
| @@ -6090,7 +6026,7 @@ dependencies = [ | |||
| 6090 | 6026 | "serde_urlencoded", | |
| 6091 | 6027 | "sync_wrapper", | |
| 6092 | 6028 | "tokio", | |
| 6093 | - | "tokio-rustls 0.26.4", | |
| 6029 | + | "tokio-rustls", | |
| 6094 | 6030 | "tower", | |
| 6095 | 6031 | "tower-http", | |
| 6096 | 6032 | "tower-service", | |
| @@ -6277,18 +6213,6 @@ dependencies = [ | |||
| 6277 | 6213 | ||
| 6278 | 6214 | [[package]] | |
| 6279 | 6215 | name = "rustls" | |
| 6280 | - | version = "0.21.12" | |
| 6281 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6282 | - | checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" | |
| 6283 | - | dependencies = [ | |
| 6284 | - | "log", | |
| 6285 | - | "ring", | |
| 6286 | - | "rustls-webpki 0.101.7", | |
| 6287 | - | "sct", | |
| 6288 | - | ] | |
| 6289 | - | ||
| 6290 | - | [[package]] | |
| 6291 | - | name = "rustls" | |
| 6292 | 6216 | version = "0.23.41" | |
| 6293 | 6217 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6294 | 6218 | checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" | |
| @@ -6297,7 +6221,7 @@ dependencies = [ | |||
| 6297 | 6221 | "once_cell", | |
| 6298 | 6222 | "ring", | |
| 6299 | 6223 | "rustls-pki-types", | |
| 6300 | - | "rustls-webpki 0.103.13", | |
| 6224 | + | "rustls-webpki", | |
| 6301 | 6225 | "subtle", | |
| 6302 | 6226 | "zeroize", | |
| 6303 | 6227 | ] | |
| @@ -6357,10 +6281,10 @@ dependencies = [ | |||
| 6357 | 6281 | "jni", | |
| 6358 | 6282 | "log", | |
| 6359 | 6283 | "once_cell", | |
| 6360 | - | "rustls 0.23.41", | |
| 6284 | + | "rustls", | |
| 6361 | 6285 | "rustls-native-certs 0.8.4", | |
| 6362 | 6286 | "rustls-platform-verifier-android", | |
| 6363 | - | "rustls-webpki 0.103.13", | |
| 6287 | + | "rustls-webpki", | |
| 6364 | 6288 | "security-framework 3.7.0", | |
| 6365 | 6289 | "security-framework-sys", | |
| 6366 | 6290 | "webpki-root-certs", | |
| @@ -6375,16 +6299,6 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" | |||
| 6375 | 6299 | ||
| 6376 | 6300 | [[package]] | |
| 6377 | 6301 | name = "rustls-webpki" | |
| 6378 | - | version = "0.101.7" | |
| 6379 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6380 | - | checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" | |
| 6381 | - | dependencies = [ | |
| 6382 | - | "ring", | |
| 6383 | - | "untrusted", | |
| 6384 | - | ] | |
| 6385 | - | ||
| 6386 | - | [[package]] | |
| 6387 | - | name = "rustls-webpki" | |
| 6388 | 6302 | version = "0.103.13" | |
| 6389 | 6303 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6390 | 6304 | checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" | |
| @@ -6519,16 +6433,6 @@ dependencies = [ | |||
| 6519 | 6433 | ] | |
| 6520 | 6434 | ||
| 6521 | 6435 | [[package]] | |
| 6522 | - | name = "sct" | |
| 6523 | - | version = "0.7.1" | |
| 6524 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6525 | - | checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" | |
| 6526 | - | dependencies = [ | |
| 6527 | - | "ring", | |
| 6528 | - | "untrusted", | |
| 6529 | - | ] | |
| 6530 | - | ||
| 6531 | - | [[package]] | |
| 6532 | 6436 | name = "sec1" | |
| 6533 | 6437 | version = "0.7.3" | |
| 6534 | 6438 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -6924,16 +6828,6 @@ dependencies = [ | |||
| 6924 | 6828 | ||
| 6925 | 6829 | [[package]] | |
| 6926 | 6830 | name = "socket2" | |
| 6927 | - | version = "0.5.10" | |
| 6928 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6929 | - | checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" | |
| 6930 | - | dependencies = [ | |
| 6931 | - | "libc", | |
| 6932 | - | "windows-sys 0.52.0", | |
| 6933 | - | ] | |
| 6934 | - | ||
| 6935 | - | [[package]] | |
| 6936 | - | name = "socket2" | |
| 6937 | 6831 | version = "0.6.4" | |
| 6938 | 6832 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6939 | 6833 | checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" | |
| @@ -7537,7 +7431,7 @@ dependencies = [ | |||
| 7537 | 7431 | "mio", | |
| 7538 | 7432 | "pin-project-lite", | |
| 7539 | 7433 | "signal-hook-registry", | |
| 7540 | - | "socket2 0.6.4", | |
| 7434 | + | "socket2", | |
| 7541 | 7435 | "tokio-macros", | |
| 7542 | 7436 | "windows-sys 0.61.2", | |
| 7543 | 7437 | ] | |
| @@ -7565,21 +7459,11 @@ dependencies = [ | |||
| 7565 | 7459 | ||
| 7566 | 7460 | [[package]] | |
| 7567 | 7461 | name = "tokio-rustls" | |
| 7568 | - | version = "0.24.1" | |
| 7569 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 7570 | - | checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" | |
| 7571 | - | dependencies = [ | |
| 7572 | - | "rustls 0.21.12", | |
| 7573 | - | "tokio", | |
| 7574 | - | ] | |
| 7575 | - | ||
| 7576 | - | [[package]] | |
| 7577 | - | name = "tokio-rustls" | |
| 7578 | 7462 | version = "0.26.4" | |
| 7579 | 7463 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 7580 | 7464 | checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" | |
| 7581 | 7465 | dependencies = [ | |
| 7582 | - | "rustls 0.23.41", | |
| 7466 | + | "rustls", | |
| 7583 | 7467 | "tokio", | |
| 7584 | 7468 | ] | |
| 7585 | 7469 | ||
| @@ -7952,7 +7836,7 @@ dependencies = [ | |||
| 7952 | 7836 | "httparse", | |
| 7953 | 7837 | "log", | |
| 7954 | 7838 | "rand 0.8.6", | |
| 7955 | - | "rustls 0.23.41", | |
| 7839 | + | "rustls", | |
| 7956 | 7840 | "rustls-native-certs 0.7.3", | |
| 7957 | 7841 | "rustls-pki-types", | |
| 7958 | 7842 | "sha1 0.10.6", | |
| @@ -9093,7 +8977,7 @@ dependencies = [ | |||
| 9093 | 8977 | "futures", | |
| 9094 | 8978 | "http 1.4.2", | |
| 9095 | 8979 | "http-body-util", | |
| 9096 | - | "hyper 1.10.1", | |
| 8980 | + | "hyper", | |
| 9097 | 8981 | "hyper-util", | |
| 9098 | 8982 | "log", | |
| 9099 | 8983 | "once_cell", |