bootstrap.rs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. //! `test(bootstrap)` — real `serve` bootstrap over the shared test services
  2. //! (plan todo 41).
  3. //!
  4. //! Both tests boot the actual `d-oauth-server` binary (never an in-process
  5. //! router): `config check` first (fail-fast env smoke), then `migrate` (the
  6. //! only path that applies migrations — `serve` never does), then `serve`
  7. //! with a TLS public listener (PKI client-CA, the mTLS reachability
  8. //! contract) plus the plaintext pod-IP health listener. Postgres and Redis
  9. //! come from `compose.test.yaml`
  10. //! (`docker compose -f compose.test.yaml up -d`); each test uses a freshly
  11. //! created database and unique Redis key prefixes, so parallel tests never
  12. //! share state (and never FLUSH shared state).
  13. //!
  14. //! * `bootstrap_serve_tls_mtls_registered_completes_unregistered_rejected`:
  15. //! `/health/ready` is 200 beforehand, mutual-TLS traffic flows through
  16. //! the production accept loop, the presented leaf completes
  17. //! `mtls::authenticate_by_id` for the registered `tls_client_auth`
  18. //! client (the injected peer chain reaches `mtls.rs`), and an
  19. //! unregistered self-signed cert fails the handshake and the matcher.
  20. //! * `bootstrap_serve_sigterm_drains_ready_and_exits_zero`: after SIGTERM,
  21. //! `/health/ready` flips to 503 inside the propagation grace and the
  22. //! process exits 0 with the listeners closed.
  23. //!
  24. //! Test names contain `bootstrap` so
  25. //! `cargo nextest run -E 'test(bootstrap)'` selects exactly them. Bodies use
  26. //! `Result` + `?` (`unwrap`/`expect`/`panic` are denied workspace-wide).
  27. #[path = "mtls/certs.rs"]
  28. // The fixture module is shared per-target with `test(mtls)`: this target
  29. // uses a subset (`der_b64` belongs to the proxy-header suite), so the
  30. // include carries a scoped, documented allow instead of restructuring a
  31. // peer-owned file.
  32. #[allow(dead_code)]
  33. mod certs;
  34. mod common;
  35. use std::{
  36. net::SocketAddr,
  37. path::PathBuf,
  38. process::Stdio,
  39. sync::Arc,
  40. time::{Duration, Instant},
  41. };
  42. use common::{pg_server_url, redis_url, unique_prefixes};
  43. use d_oauth::protocol::mtls;
  44. use rustls::pki_types::{CertificateDer, PrivateKeyDer};
  45. use sqlx::{Executor as _, postgres::PgPoolOptions};
  46. /// Registered `tls_client_auth` client for the mTLS test.
  47. const CLIENT_ID: &str = "bootstrap-tls-client";
  48. /// Subject DN of the CA-signed fixture, in [`mtls::subject_dn`] rendering.
  49. const PKI_DN: &str = "CN=mtls-pki-client, O=Example";
  50. /// `x5t#S256` of the CA-signed fixture (locked by `test(mtls)` too;
  51. /// re-minted 2026-10-06, valid to 2036-10-03).
  52. const PKI_THUMBPRINT: &str = "VSFBS4-7neqMcHVsx-dcc28rEEemzFvmeqtZSE3xbK8";
  53. /// Test-body error.
  54. #[derive(Debug, thiserror::Error)]
  55. enum TestError {
  56. /// The shared test service is unreachable (usually: compose not started).
  57. #[error("test setup: {0}")]
  58. Setup(String),
  59. /// A sqlx operation failed.
  60. #[error("database error: {0}")]
  61. Database(#[from] sqlx::Error),
  62. /// An mTLS operation failed.
  63. #[error("mtls error: {0}")]
  64. Mtls(#[from] mtls::MtlsError),
  65. /// A test key ring was rejected.
  66. #[error("key error: {0}")]
  67. Keys(#[from] d_oauth::keys::KeyRingError),
  68. /// A fixture did not decode.
  69. #[error("fixture error: {0}")]
  70. Fixture(#[from] certs::FixtureError),
  71. /// A rustls operation failed.
  72. #[error("tls error: {0}")]
  73. Tls(#[from] rustls::Error),
  74. /// A DNS name was invalid.
  75. #[error("dns error: {0}")]
  76. Dns(#[from] rustls::pki_types::InvalidDnsNameError),
  77. /// A response was not UTF-8.
  78. #[error("encoding error: {0}")]
  79. Encoding(#[from] std::string::FromUtf8Error),
  80. /// An I/O operation failed.
  81. #[error("io error: {0}")]
  82. Io(#[from] std::io::Error),
  83. /// An async deadline elapsed.
  84. #[error("timeout: {0}")]
  85. Timeout(String),
  86. /// A socket address did not parse.
  87. #[error("address error: {0}")]
  88. Addr(#[from] std::net::AddrParseError),
  89. /// A test step failed.
  90. #[error("failed: {0}")]
  91. Failed(String),
  92. }
  93. type TestResult<T = ()> = Result<T, TestError>;
  94. /// Live world for one test: shared-service URLs, ports, fixture files, and
  95. /// the config file the child `serve` process boots from.
  96. struct World {
  97. pg_url: String,
  98. tls_port: u16,
  99. health_port: u16,
  100. config_path: String,
  101. }
  102. /// Locate the compiled server binary (cargo sets one of these spellings for
  103. /// integration targets; otherwise resolve next to the test executable).
  104. fn server_binary() -> Result<PathBuf, TestError> {
  105. for var in [
  106. "CARGO_BIN_EXE_d-oauth-server",
  107. "CARGO_BIN_EXE_d_oauth_server",
  108. ] {
  109. if let Ok(path) = std::env::var(var) {
  110. return Ok(PathBuf::from(path));
  111. }
  112. }
  113. let mut path = std::env::current_exe()?;
  114. if !path.pop() {
  115. return Err(TestError::Failed(
  116. "test executable has no parent".to_owned(),
  117. ));
  118. }
  119. if path.ends_with("deps") && !path.pop() {
  120. return Err(TestError::Failed("cannot leave deps dir".to_owned()));
  121. }
  122. path.push("d-oauth-server");
  123. Ok(path)
  124. }
  125. /// An unused loopback port (TOCTOU-tolerated: the child binds it right away).
  126. async fn free_port() -> Result<u16, TestError> {
  127. let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
  128. Ok(listener.local_addr()?.port())
  129. }
  130. /// Hint shown when the shared test services are unreachable.
  131. const COMPOSE_HINT: &str = "run `docker compose -f compose.test.yaml up -d`";
  132. /// Create a uniquely named database on the shared Postgres server and return
  133. /// a URL pointing at it (the child `serve` process migrates it via its own
  134. /// `migrate` subcommand, like production).
  135. async fn fresh_db_url() -> Result<String, TestError> {
  136. let server = pg_server_url();
  137. let admin = PgPoolOptions::new()
  138. .max_connections(1)
  139. .connect(&server)
  140. .await
  141. .map_err(|error| {
  142. TestError::Setup(format!(
  143. "cannot reach shared test Postgres at {server}: {error}; {COMPOSE_HINT}"
  144. ))
  145. })?;
  146. let name = format!("bootstrap_{}", uuid::Uuid::new_v4().simple());
  147. // `CREATE DATABASE` cannot take a bind parameter, and `Pool`'s `Executor`
  148. // requires a `'static` query, so leak this one tiny test-only string.
  149. let ddl: &'static str = Box::leak(format!("CREATE DATABASE \"{name}\"").into_boxed_str());
  150. let created = admin
  151. .execute(ddl)
  152. .await
  153. .map_err(|error| TestError::Setup(format!("failed to create test database {name}: {error}")));
  154. admin.close().await;
  155. created?;
  156. Ok(server.replacen("/postgres?", &format!("/{name}?"), 1))
  157. }
  158. /// Build one test world: shared-service URLs, ports, PEM fixture files,
  159. /// config file.
  160. async fn build_world() -> Result<World, TestError> {
  161. let pg_url = fresh_db_url().await?;
  162. let redis_url = redis_url();
  163. let (token_prefix, ratelimit_prefix) = unique_prefixes();
  164. let tls_port = free_port().await?;
  165. let health_port = free_port().await?;
  166. let dir = std::env::temp_dir().join(format!("bootstrap-test-{}", uuid::Uuid::new_v4()));
  167. std::fs::create_dir_all(&dir)?;
  168. let cert_path = write_file(&dir, "server-cert.pem", certs::SERVER_CERT_PEM)?;
  169. let key_path = write_file(&dir, "server-key.pem", certs::SERVER_KEY_PEM)?;
  170. let ca_path = write_file(&dir, "client-ca.pem", certs::CA_CERT_PEM)?;
  171. // Layered env (`D_OAUTH_TLS__CERT_PATH`, …) does not reach nested tables,
  172. // so the child boots from a JSON file via `D_OAUTH_CONFIG` (the extension
  173. // is omitted: `File::with_name` probes supported formats).
  174. let config = serde_json::json!({
  175. "issuer": "https://auth.example.test",
  176. "environment": "development",
  177. "bind_addr": format!("127.0.0.1:{tls_port}"),
  178. "health_addr": format!("0.0.0.0:{health_port}"),
  179. "tls": {
  180. "cert_path": path_string(&cert_path)?,
  181. "key_path": path_string(&key_path)?,
  182. "client_ca_path": path_string(&ca_path)?,
  183. },
  184. "database": {"url": pg_url.clone()},
  185. "redis": {
  186. "url": redis_url,
  187. "durability": "waitaof",
  188. "token_key_prefix": token_prefix,
  189. "ratelimit_key_prefix": ratelimit_prefix,
  190. },
  191. "token_pepper": "bootstrap-test-pepper-0123456789abcdef",
  192. "totp_encryption_key": "bootstrap-test-totp-key-0123456789abcdef",
  193. "pii_encryption_key": "bootstrap-test-pii-key-0123456789abcdef",
  194. // Dual-write window: the mTLS fixture seeds a legacy-plaintext client
  195. // row via raw SQL, which the encrypt-only startup guard would (correctly)
  196. // refuse to serve over. Dual-write keeps the seed legal without touching
  197. // the guard or the handshake assertions.
  198. "pii_dual_write": true,
  199. });
  200. let config_file = dir.join("d-oauth.json");
  201. std::fs::write(
  202. &config_file,
  203. serde_json::to_string_pretty(&config)
  204. .map_err(|error| TestError::Failed(format!("config JSON failed: {error}")))?,
  205. )?;
  206. let stem = config_file.with_extension("");
  207. Ok(World {
  208. pg_url,
  209. tls_port,
  210. health_port,
  211. config_path: path_string(&stem)?,
  212. })
  213. }
  214. /// Write one fixture file, returning its path.
  215. fn write_file(dir: &std::path::Path, name: &str, contents: &str) -> Result<PathBuf, TestError> {
  216. let path = dir.join(name);
  217. std::fs::write(&path, contents)?;
  218. Ok(path)
  219. }
  220. /// A filesystem path as UTF-8 (temp dirs always are here).
  221. fn path_string(path: &std::path::Path) -> Result<String, TestError> {
  222. path
  223. .to_str()
  224. .map(str::to_owned)
  225. .ok_or_else(|| TestError::Failed(format!("non-UTF-8 temp path: {}", path.display())))
  226. }
  227. /// A child command for the server binary with the world config applied.
  228. fn child_command(world: &World, args: &[&str]) -> Result<tokio::process::Command, TestError> {
  229. let mut command = tokio::process::Command::new(server_binary()?);
  230. command
  231. .args(args)
  232. .env("D_OAUTH_CONFIG", &world.config_path)
  233. .stdin(Stdio::null());
  234. Ok(command)
  235. }
  236. /// Run a one-shot subcommand (`config check`, `migrate`) to completion.
  237. async fn run_once(world: &World, args: &[&str]) -> Result<(), TestError> {
  238. let output = child_command(world, args)?
  239. .stdout(Stdio::piped())
  240. .stderr(Stdio::piped())
  241. .output()
  242. .await?;
  243. if output.status.success() {
  244. return Ok(());
  245. }
  246. Err(TestError::Failed(format!(
  247. "{} failed: {} {}",
  248. args.join(" "),
  249. output.status,
  250. String::from_utf8_lossy(&output.stderr)
  251. )))
  252. }
  253. /// Spawn the long-lived `serve` child (killed with the test on drop).
  254. fn spawn_serve(world: &World) -> Result<tokio::process::Child, TestError> {
  255. let mut child = child_command(world, &["serve"])?;
  256. child
  257. .stdout(Stdio::null())
  258. .stderr(Stdio::piped())
  259. .kill_on_drop(true)
  260. .spawn()
  261. .map_err(TestError::from)
  262. }
  263. /// Raw plaintext `GET` over TCP, returning `(status, body)`.
  264. async fn http_get_plain(addr: SocketAddr, path: &str) -> Result<(u16, String), TestError> {
  265. use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
  266. let mut stream =
  267. tokio::time::timeout(Duration::from_secs(2), tokio::net::TcpStream::connect(addr))
  268. .await
  269. .map_err(|_| TestError::Timeout("health connect timed out".to_owned()))??;
  270. let request = format!("GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n");
  271. stream.write_all(request.as_bytes()).await?;
  272. let mut raw = Vec::new();
  273. stream.read_to_end(&mut raw).await?;
  274. parse_response(&raw)
  275. }
  276. /// Split a raw HTTP/1.1 response into `(status, body)`.
  277. fn parse_response(raw: &[u8]) -> Result<(u16, String), TestError> {
  278. let text = String::from_utf8(raw.to_vec())?;
  279. let (head, body) = text
  280. .split_once("\r\n\r\n")
  281. .map_or((text.as_str(), ""), |(head, body)| (head, body));
  282. let line = head
  283. .lines()
  284. .next()
  285. .ok_or_else(|| TestError::Failed("empty HTTP response".to_owned()))?;
  286. let code = line
  287. .split_whitespace()
  288. .nth(1)
  289. .ok_or_else(|| TestError::Failed(format!("bad status line: {line}")))?;
  290. let status: u16 = code
  291. .parse()
  292. .map_err(|_| TestError::Failed(format!("bad status code: {code}")))?;
  293. Ok((status, body.to_owned()))
  294. }
  295. /// Health listener address (reachable via loopback; bound to the pod-IP wildcard).
  296. fn health_addr(world: &World) -> Result<SocketAddr, TestError> {
  297. Ok(SocketAddr::from(([127, 0, 0, 1], world.health_port)))
  298. }
  299. /// Poll `/health/ready` until it answers 200 (fails fast if the child dies).
  300. async fn poll_ready(world: &World, child: &mut tokio::process::Child) -> Result<(), TestError> {
  301. let addr = health_addr(world)?;
  302. let deadline = Instant::now() + Duration::from_secs(90);
  303. loop {
  304. if let Some(status) = child.try_wait()? {
  305. return Err(TestError::Failed(format!("serve exited early: {status}")));
  306. }
  307. match http_get_plain(addr, "/health/ready").await {
  308. Ok((200, _)) => return Ok(()),
  309. Ok(_) | Err(_) if Instant::now() < deadline => {}
  310. Ok((status, body)) => {
  311. return Err(TestError::Failed(format!(
  312. "ready never 200: {status} {body}"
  313. )));
  314. }
  315. Err(error) => {
  316. return Err(TestError::Failed(format!("ready never reachable: {error}")));
  317. }
  318. }
  319. tokio::time::sleep(Duration::from_millis(250)).await;
  320. }
  321. }
  322. /// Poll `/health/ready` until it flips to 503 after SIGTERM.
  323. async fn poll_not_ready(world: &World) -> Result<(), TestError> {
  324. let addr = health_addr(world)?;
  325. let deadline = Instant::now() + Duration::from_secs(20);
  326. loop {
  327. match http_get_plain(addr, "/health/ready").await {
  328. Ok((503, _)) => return Ok(()),
  329. Ok(_) | Err(_) if Instant::now() < deadline => {}
  330. Ok((status, body)) => {
  331. return Err(TestError::Failed(format!(
  332. "ready never flipped: {status} {body}"
  333. )));
  334. }
  335. Err(error) => {
  336. return Err(TestError::Failed(format!(
  337. "ready unreachable post-TERM: {error}"
  338. )));
  339. }
  340. }
  341. tokio::time::sleep(Duration::from_millis(50)).await;
  342. }
  343. }
  344. /// Raw mutual-TLS `GET` through the production listener, returning `(status, body)`.
  345. async fn tls_get(
  346. world: &World,
  347. client: Option<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>,
  348. path: &str,
  349. ) -> Result<(u16, String), TestError> {
  350. use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
  351. let mut roots = rustls::RootCertStore::empty();
  352. roots.add(certs::cert_der(certs::SERVER_CERT_PEM)?)?;
  353. let builder = rustls::ClientConfig::builder().with_root_certificates(roots);
  354. let config = match client {
  355. Some((chain, key)) => builder.with_client_auth_cert(chain, key)?,
  356. None => builder.with_no_client_auth(),
  357. };
  358. let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
  359. let stream =
  360. tokio::net::TcpStream::connect(SocketAddr::from(([127, 0, 0, 1], world.tls_port))).await?;
  361. let server_name = rustls::pki_types::ServerName::try_from("localhost")?.to_owned();
  362. let mut tls = connector.connect(server_name, stream).await?;
  363. let request = format!("GET {path} HTTP/1.1\r\nhost: localhost\r\nconnection: close\r\n\r\n");
  364. tls.write_all(request.as_bytes()).await?;
  365. let mut raw = Vec::new();
  366. tls.read_to_end(&mut raw).await?;
  367. parse_response(&raw)
  368. }
  369. /// Whether a full mutual-TLS request with the given client identity is
  370. /// rejected: no valid HTTP response is ever served for it.
  371. ///
  372. /// The rejection may surface during the handshake or on the first request
  373. /// (in TLS 1.3 the client can finish its flight before the server's alert
  374. /// arrives), so this drives a complete `GET` and requires "not served".
  375. async fn tls_request_rejected(
  376. world: &World,
  377. cert_pem: &str,
  378. key_pem: &str,
  379. ) -> Result<bool, TestError> {
  380. let attempt = tls_get(
  381. world,
  382. Some((
  383. vec![certs::cert_der(cert_pem)?],
  384. certs::private_key(key_pem)?,
  385. )),
  386. "/health/live",
  387. )
  388. .await;
  389. Ok(!matches!(attempt, Ok((200, _))))
  390. }
  391. /// Test PII key ring matching the world's `pii_encryption_key`.
  392. fn test_keys() -> Result<d_oauth::keys::KeyRing, TestError> {
  393. use d_oauth::keys::{KeyId, KeyRing};
  394. use secrecy::SecretString;
  395. Ok(KeyRing::from_parts(
  396. vec![(
  397. KeyId::new("v1"),
  398. SecretString::from("bootstrap-test-pepper"),
  399. )],
  400. vec![(KeyId::new("v1"), SecretString::from("bootstrap-test-totp"))],
  401. vec![(
  402. KeyId::new("v1"),
  403. SecretString::from("bootstrap-test-pii-key-0123456789abcdef"),
  404. )],
  405. )?)
  406. }
  407. /// Register one `tls_client_auth` client row (runtime query: no `.sqlx` cache).
  408. async fn insert_tls_client(pool: &sqlx::PgPool) -> Result<(), TestError> {
  409. sqlx::query(
  410. "INSERT INTO oauth_clients (id, client_id, client_id_issued_at, client_name,
  411. grant_types, response_types, scope, token_endpoint_auth_method,
  412. tls_client_auth_subject_dn, tls_client_auth_cert_thumbprint,
  413. created_at, updated_at)
  414. VALUES ($1, $2, now(), 'Bootstrap Test',
  415. ARRAY['client_credentials'], ARRAY['token'], 'api:read',
  416. 'tls_client_auth', $3, NULL, now(), now())",
  417. )
  418. .bind(uuid::Uuid::new_v4())
  419. .bind(CLIENT_ID)
  420. .bind(PKI_DN)
  421. .execute(pool)
  422. .await?;
  423. Ok(())
  424. }
  425. /// SIGTERM the child and wait for exit 0 (bounded well under the drain budget).
  426. async fn terminate(mut child: tokio::process::Child) -> Result<(), TestError> {
  427. let pid = child
  428. .id()
  429. .ok_or_else(|| TestError::Failed("serve has no pid".to_owned()))?;
  430. let status = tokio::process::Command::new("kill")
  431. .args(["-TERM", &pid.to_string()])
  432. .status()
  433. .await?;
  434. if !status.success() {
  435. return Err(TestError::Failed("kill -TERM failed".to_owned()));
  436. }
  437. let exit = tokio::time::timeout(Duration::from_secs(45), child.wait())
  438. .await
  439. .map_err(|_| TestError::Timeout("serve did not exit after SIGTERM".to_owned()))??;
  440. if exit.success() {
  441. return Ok(());
  442. }
  443. Err(TestError::Failed(format!("serve exited dirty: {exit}")))
  444. }
  445. /// The real `serve` serves mutual-TLS traffic, the registered client
  446. /// completes mTLS against `mtls.rs`, and the rogue cert is rejected.
  447. #[tokio::test]
  448. async fn bootstrap_serve_tls_mtls_registered_completes_unregistered_rejected() -> TestResult {
  449. let world = build_world().await?;
  450. run_once(&world, &["config", "check"]).await?;
  451. run_once(&world, &["migrate"]).await?;
  452. let pool = sqlx::PgPool::connect(&world.pg_url).await?;
  453. insert_tls_client(&pool).await?;
  454. let mut child = spawn_serve(&world)?;
  455. poll_ready(&world, &mut child).await?;
  456. let registered = Some((
  457. vec![certs::cert_der(certs::CLIENT_PKI_CERT_PEM)?],
  458. certs::private_key(certs::CLIENT_PKI_KEY_PEM)?,
  459. ));
  460. let (status, body) = tls_get(&world, registered, "/health/live").await?;
  461. if status != 200 || !body.contains("live") {
  462. return Err(TestError::Failed(format!(
  463. "mTLS /health/live failed: {status} {body}"
  464. )));
  465. }
  466. let (status, _) = tls_get(
  467. &world,
  468. Some((
  469. vec![certs::cert_der(certs::CLIENT_PKI_CERT_PEM)?],
  470. certs::private_key(certs::CLIENT_PKI_KEY_PEM)?,
  471. )),
  472. "/.well-known/oauth-authorization-server",
  473. )
  474. .await?;
  475. if status != 200 {
  476. return Err(TestError::Failed(format!("mTLS metadata failed: {status}")));
  477. }
  478. let leaf = certs::cert_der(certs::CLIENT_PKI_CERT_PEM)?;
  479. let cnf = mtls::authenticate_by_id(&pool, &test_keys()?, CLIENT_ID, Some(&leaf)).await?;
  480. if cnf.x5t_s256 != PKI_THUMBPRINT {
  481. return Err(TestError::Failed(format!(
  482. "unexpected cnf: {}",
  483. cnf.x5t_s256
  484. )));
  485. }
  486. if !tls_request_rejected(
  487. &world,
  488. certs::CLIENT_SELF_CERT_PEM,
  489. certs::CLIENT_SELF_KEY_PEM,
  490. )
  491. .await?
  492. {
  493. return Err(TestError::Failed(
  494. "unregistered self-signed cert was served".to_owned(),
  495. ));
  496. }
  497. let foreign = certs::cert_der(certs::CLIENT_ROGUE_CERT_PEM)?;
  498. if mtls::authenticate_by_id(&pool, &test_keys()?, CLIENT_ID, Some(&foreign))
  499. .await
  500. .is_ok()
  501. {
  502. return Err(TestError::Failed(
  503. "unregistered cert authenticated".to_owned(),
  504. ));
  505. }
  506. terminate(child).await
  507. }
  508. /// After SIGTERM, `/health/ready` flips to 503 and the process exits 0.
  509. #[tokio::test]
  510. async fn bootstrap_serve_sigterm_drains_ready_and_exits_zero() -> TestResult {
  511. let world = build_world().await?;
  512. run_once(&world, &["config", "check"]).await?;
  513. run_once(&world, &["migrate"]).await?;
  514. let mut child = spawn_serve(&world)?;
  515. poll_ready(&world, &mut child).await?;
  516. let pid = child
  517. .id()
  518. .ok_or_else(|| TestError::Failed("serve has no pid".to_owned()))?;
  519. let status = tokio::process::Command::new("kill")
  520. .args(["-TERM", &pid.to_string()])
  521. .status()
  522. .await?;
  523. if !status.success() {
  524. return Err(TestError::Failed("kill -TERM failed".to_owned()));
  525. }
  526. poll_not_ready(&world).await?;
  527. let exit = tokio::time::timeout(Duration::from_secs(45), child.wait())
  528. .await
  529. .map_err(|_| TestError::Timeout("serve did not exit after SIGTERM".to_owned()))??;
  530. if !exit.success() {
  531. return Err(TestError::Failed(format!("serve exited dirty: {exit}")));
  532. }
  533. // The listeners are closed: fresh connections are refused.
  534. if http_get_plain(health_addr(&world)?, "/health/live")
  535. .await
  536. .is_ok()
  537. {
  538. return Err(TestError::Failed(
  539. "health listener still open after exit".to_owned(),
  540. ));
  541. }
  542. Ok(())
  543. }