//! CLI-driver tests (todo 29) against the `fake-d-oauth.sh` fixture. //! //! The fixture is copied into a per-test tempdir and made executable with //! `std` permissions (no shell `chmod`); every driver run gets a fresh //! `FAKE_STATE_DIR`. The minted `IATTEST`-prefixed tokens are synthetic test //! values, never real credentials. use std::{fs, path::PathBuf}; use provision::cli_driver::{ DEMO_POST_LOGOUT_URI, DEMO_REDIRECT_URI, DEMO_SCOPE, Driver, SetupInputs, load_env_file, run_setup, }; /// Process-env fixture state is global: serialize the CLI-driver tests so two /// tests never point `FAKE_STATE_DIR` at different tempdirs at once. static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(()); /// Install the fixture as an executable in a fresh tempdir; return the /// tempdir (kept alive by the caller), the CLI path, and the state dir. fn install_fake_cli() -> (tempfile::TempDir, PathBuf, PathBuf) { let dir = tempfile::tempdir().expect("tempdir"); let cli = dir.path().join("fake-d-oauth.sh"); let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fake-d-oauth.sh"); let script = fs::read_to_string(&source).expect("read fixture"); fs::write(&cli, script).expect("write fake cli"); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt as _; fs::set_permissions(&cli, fs::Permissions::from_mode(0o755)).expect("chmod fake cli"); } let state = dir.path().join("state"); fs::create_dir(&state).expect("state dir"); unsafe { std::env::set_var("FAKE_STATE_DIR", &state); } (dir, cli, state) } /// Minimal `.env` file: comments, blanks, and quoted values included. fn write_env_file(dir: &tempfile::TempDir) -> PathBuf { let path = dir.path().join("d-oauth.env"); fs::write( &path, "# fake operator env\nD_OAUTH_ISSUER=https://localhost:8080\nQUOTED=\"hello world\"\nEMPTY=\n", ) .expect("write env file"); path } fn write_password_file(dir: &tempfile::TempDir) -> PathBuf { let path = dir.path().join("demo-password.txt"); fs::write(&path, "correct-horse-battery-staple").expect("write password file"); path } fn run_driver(cli: &std::path::Path, env_file: &std::path::Path, pw: &std::path::Path) -> String { let outcome = run_setup(&SetupInputs { cli: cli.to_owned(), env_file: env_file.to_owned(), email: "demo@example.com".to_owned(), password_file: pw.to_owned(), post_logout_uri: DEMO_POST_LOGOUT_URI.to_owned(), }) .expect("setup succeeds"); outcome.initial_access_token } #[test] fn cli_driver_runs_full_sequence_and_captures_iat() { let _guard = SERIAL.lock().expect("serial guard"); let (dir, cli, state) = install_fake_cli(); let env_file = write_env_file(&dir); let pw = write_password_file(&dir); let iat = run_driver(&cli, &env_file, &pw); assert_eq!(iat, format!("IATTEST{:037}", 1)); let log = fs::read_to_string(state.join("call.log")).expect("call log"); let mut calls = log.lines(); assert_eq!( calls.next(), Some(format!("scope create {DEMO_SCOPE}").as_str()) ); assert_eq!(calls.next(), Some("user scopes --email demo@example.com")); let create = calls.next().expect("user create call"); assert!(create.starts_with("user create --email demo@example.com --password-file ")); assert_eq!( calls.next(), Some(format!("user grant-scope --email demo@example.com --scope {DEMO_SCOPE}").as_str()) ); assert_eq!( calls.next(), Some(format!("redirect-uri create {DEMO_REDIRECT_URI}").as_str()) ); assert_eq!( calls.next(), Some(format!("redirect-uri create {DEMO_POST_LOGOUT_URI} --kind post_logout").as_str()) ); assert_eq!( calls.next(), Some(format!("redirect-uri enable {DEMO_POST_LOGOUT_URI} --kind post_logout").as_str()) ); assert_eq!( calls.next(), Some("initial-access-token issue --uses 1 --ttl 600") ); assert_eq!(calls.next(), None); // The IAT travels on stdout into the DCR step only: it must not appear in // the argv log or in any persisted fixture state. assert!(!log.contains("IATTEST")); let _ = dir; } #[test] fn cli_driver_second_run_skips_existing_user() { let _guard = SERIAL.lock().expect("serial guard"); let (dir, cli, state) = install_fake_cli(); let env_file = write_env_file(&dir); let pw = write_password_file(&dir); assert_eq!( run_driver(&cli, &env_file, &pw), format!("IATTEST{:037}", 1) ); // Second run: `user create` hits the already-exists path and must not fail. assert_eq!( run_driver(&cli, &env_file, &pw), format!("IATTEST{:037}", 2) ); let log = fs::read_to_string(state.join("call.log")).expect("call log"); assert_eq!( log .lines() .filter(|line| line.starts_with("user create ")) .count(), 2 ); assert!(!log.contains("IATTEST")); let _ = dir; } #[test] fn cli_driver_surfaces_missing_scope_as_typed_error() { let _guard = SERIAL.lock().expect("serial guard"); let (dir, cli, _state) = install_fake_cli(); let env_file = write_env_file(&dir); let pw = write_password_file(&dir); unsafe { std::env::set_var("FAKE_FAIL_STEP", "grant"); } let result = run_setup(&SetupInputs { cli: cli.to_owned(), env_file: env_file.to_owned(), email: "demo@example.com".to_owned(), password_file: pw.to_owned(), post_logout_uri: DEMO_POST_LOGOUT_URI.to_owned(), }); unsafe { std::env::remove_var("FAKE_FAIL_STEP"); } match result { Err(provision::cli_driver::CliDriverError::Step { step, stderr, .. }) => { assert_eq!(step, "user grant-scope"); assert!( stderr.contains("not found: scope demo.read"), "stderr: {stderr}" ); } other => panic!("expected a typed grant-scope failure, got {other:?}"), } let _ = dir; } #[test] fn cli_driver_parses_env_file_without_touching_process_env() { let _guard = SERIAL.lock().expect("serial guard"); let (dir, _cli, _state) = install_fake_cli(); let env_file = write_env_file(&dir); let env = load_env_file(&env_file).expect("parse env file"); assert_eq!( env.get("D_OAUTH_ISSUER").map(String::as_str), Some("https://localhost:8080") ); assert_eq!(env.get("QUOTED").map(String::as_str), Some("hello world")); assert_eq!(env.get("EMPTY").map(String::as_str), Some("")); // Parsing must not export into our own process environment. assert!(std::env::var("D_OAUTH_ISSUER").is_err()); let _ = dir; } #[test] fn cli_driver_driver_struct_is_usable_directly() { let _guard = SERIAL.lock().expect("serial guard"); let (dir, cli, state) = install_fake_cli(); let env_file = write_env_file(&dir); let pw = write_password_file(&dir); let env = load_env_file(&env_file).expect("parse env file"); let driver = Driver { cli: cli.clone(), env, }; let outcome = driver .run("demo@example.com", &pw, DEMO_POST_LOGOUT_URI) .expect("driver run"); assert_eq!(outcome.initial_access_token, format!("IATTEST{:037}", 1)); assert!(state.join("scopes.txt").exists()); let _ = dir; }