cli_driver.rs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. //! CLI-driver tests (todo 29) against the `fake-d-oauth.sh` fixture.
  2. //!
  3. //! The fixture is copied into a per-test tempdir and made executable with
  4. //! `std` permissions (no shell `chmod`); every driver run gets a fresh
  5. //! `FAKE_STATE_DIR`. The minted `IATTEST`-prefixed tokens are synthetic test
  6. //! values, never real credentials.
  7. use std::{fs, path::PathBuf};
  8. use provision::cli_driver::{
  9. DEMO_POST_LOGOUT_URI, DEMO_REDIRECT_URI, DEMO_SCOPE, Driver, SetupInputs, load_env_file,
  10. run_setup,
  11. };
  12. /// Process-env fixture state is global: serialize the CLI-driver tests so two
  13. /// tests never point `FAKE_STATE_DIR` at different tempdirs at once.
  14. static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
  15. /// Install the fixture as an executable in a fresh tempdir; return the
  16. /// tempdir (kept alive by the caller), the CLI path, and the state dir.
  17. fn install_fake_cli() -> (tempfile::TempDir, PathBuf, PathBuf) {
  18. let dir = tempfile::tempdir().expect("tempdir");
  19. let cli = dir.path().join("fake-d-oauth.sh");
  20. let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fake-d-oauth.sh");
  21. let script = fs::read_to_string(&source).expect("read fixture");
  22. fs::write(&cli, script).expect("write fake cli");
  23. #[cfg(unix)]
  24. {
  25. use std::os::unix::fs::PermissionsExt as _;
  26. fs::set_permissions(&cli, fs::Permissions::from_mode(0o755)).expect("chmod fake cli");
  27. }
  28. let state = dir.path().join("state");
  29. fs::create_dir(&state).expect("state dir");
  30. unsafe {
  31. std::env::set_var("FAKE_STATE_DIR", &state);
  32. }
  33. (dir, cli, state)
  34. }
  35. /// Minimal `.env` file: comments, blanks, and quoted values included.
  36. fn write_env_file(dir: &tempfile::TempDir) -> PathBuf {
  37. let path = dir.path().join("d-oauth.env");
  38. fs::write(
  39. &path,
  40. "# fake operator env\nD_OAUTH_ISSUER=https://localhost:8080\nQUOTED=\"hello world\"\nEMPTY=\n",
  41. )
  42. .expect("write env file");
  43. path
  44. }
  45. fn write_password_file(dir: &tempfile::TempDir) -> PathBuf {
  46. let path = dir.path().join("demo-password.txt");
  47. fs::write(&path, "correct-horse-battery-staple").expect("write password file");
  48. path
  49. }
  50. fn run_driver(cli: &std::path::Path, env_file: &std::path::Path, pw: &std::path::Path) -> String {
  51. let outcome = run_setup(&SetupInputs {
  52. cli: cli.to_owned(),
  53. env_file: env_file.to_owned(),
  54. email: "demo@example.com".to_owned(),
  55. password_file: pw.to_owned(),
  56. post_logout_uri: DEMO_POST_LOGOUT_URI.to_owned(),
  57. })
  58. .expect("setup succeeds");
  59. outcome.initial_access_token
  60. }
  61. #[test]
  62. fn cli_driver_runs_full_sequence_and_captures_iat() {
  63. let _guard = SERIAL.lock().expect("serial guard");
  64. let (dir, cli, state) = install_fake_cli();
  65. let env_file = write_env_file(&dir);
  66. let pw = write_password_file(&dir);
  67. let iat = run_driver(&cli, &env_file, &pw);
  68. assert_eq!(iat, format!("IATTEST{:037}", 1));
  69. let log = fs::read_to_string(state.join("call.log")).expect("call log");
  70. let mut calls = log.lines();
  71. assert_eq!(
  72. calls.next(),
  73. Some(format!("scope create {DEMO_SCOPE}").as_str())
  74. );
  75. assert_eq!(calls.next(), Some("user scopes --email demo@example.com"));
  76. let create = calls.next().expect("user create call");
  77. assert!(create.starts_with("user create --email demo@example.com --password-file "));
  78. assert_eq!(
  79. calls.next(),
  80. Some(format!("user grant-scope --email demo@example.com --scope {DEMO_SCOPE}").as_str())
  81. );
  82. assert_eq!(
  83. calls.next(),
  84. Some(format!("redirect-uri create {DEMO_REDIRECT_URI}").as_str())
  85. );
  86. assert_eq!(
  87. calls.next(),
  88. Some(format!("redirect-uri create {DEMO_POST_LOGOUT_URI} --kind post_logout").as_str())
  89. );
  90. assert_eq!(
  91. calls.next(),
  92. Some(format!("redirect-uri enable {DEMO_POST_LOGOUT_URI} --kind post_logout").as_str())
  93. );
  94. assert_eq!(
  95. calls.next(),
  96. Some("initial-access-token issue --uses 1 --ttl 600")
  97. );
  98. assert_eq!(calls.next(), None);
  99. // The IAT travels on stdout into the DCR step only: it must not appear in
  100. // the argv log or in any persisted fixture state.
  101. assert!(!log.contains("IATTEST"));
  102. let _ = dir;
  103. }
  104. #[test]
  105. fn cli_driver_second_run_skips_existing_user() {
  106. let _guard = SERIAL.lock().expect("serial guard");
  107. let (dir, cli, state) = install_fake_cli();
  108. let env_file = write_env_file(&dir);
  109. let pw = write_password_file(&dir);
  110. assert_eq!(
  111. run_driver(&cli, &env_file, &pw),
  112. format!("IATTEST{:037}", 1)
  113. );
  114. // Second run: `user create` hits the already-exists path and must not fail.
  115. assert_eq!(
  116. run_driver(&cli, &env_file, &pw),
  117. format!("IATTEST{:037}", 2)
  118. );
  119. let log = fs::read_to_string(state.join("call.log")).expect("call log");
  120. assert_eq!(
  121. log
  122. .lines()
  123. .filter(|line| line.starts_with("user create "))
  124. .count(),
  125. 2
  126. );
  127. assert!(!log.contains("IATTEST"));
  128. let _ = dir;
  129. }
  130. #[test]
  131. fn cli_driver_surfaces_missing_scope_as_typed_error() {
  132. let _guard = SERIAL.lock().expect("serial guard");
  133. let (dir, cli, _state) = install_fake_cli();
  134. let env_file = write_env_file(&dir);
  135. let pw = write_password_file(&dir);
  136. unsafe {
  137. std::env::set_var("FAKE_FAIL_STEP", "grant");
  138. }
  139. let result = run_setup(&SetupInputs {
  140. cli: cli.to_owned(),
  141. env_file: env_file.to_owned(),
  142. email: "demo@example.com".to_owned(),
  143. password_file: pw.to_owned(),
  144. post_logout_uri: DEMO_POST_LOGOUT_URI.to_owned(),
  145. });
  146. unsafe {
  147. std::env::remove_var("FAKE_FAIL_STEP");
  148. }
  149. match result {
  150. Err(provision::cli_driver::CliDriverError::Step { step, stderr, .. }) => {
  151. assert_eq!(step, "user grant-scope");
  152. assert!(
  153. stderr.contains("not found: scope demo.read"),
  154. "stderr: {stderr}"
  155. );
  156. }
  157. other => panic!("expected a typed grant-scope failure, got {other:?}"),
  158. }
  159. let _ = dir;
  160. }
  161. #[test]
  162. fn cli_driver_parses_env_file_without_touching_process_env() {
  163. let _guard = SERIAL.lock().expect("serial guard");
  164. let (dir, _cli, _state) = install_fake_cli();
  165. let env_file = write_env_file(&dir);
  166. let env = load_env_file(&env_file).expect("parse env file");
  167. assert_eq!(
  168. env.get("D_OAUTH_ISSUER").map(String::as_str),
  169. Some("https://localhost:8080")
  170. );
  171. assert_eq!(env.get("QUOTED").map(String::as_str), Some("hello world"));
  172. assert_eq!(env.get("EMPTY").map(String::as_str), Some(""));
  173. // Parsing must not export into our own process environment.
  174. assert!(std::env::var("D_OAUTH_ISSUER").is_err());
  175. let _ = dir;
  176. }
  177. #[test]
  178. fn cli_driver_driver_struct_is_usable_directly() {
  179. let _guard = SERIAL.lock().expect("serial guard");
  180. let (dir, cli, state) = install_fake_cli();
  181. let env_file = write_env_file(&dir);
  182. let pw = write_password_file(&dir);
  183. let env = load_env_file(&env_file).expect("parse env file");
  184. let driver = Driver {
  185. cli: cli.clone(),
  186. env,
  187. };
  188. let outcome = driver
  189. .run("demo@example.com", &pw, DEMO_POST_LOGOUT_URI)
  190. .expect("driver run");
  191. assert_eq!(outcome.initial_access_token, format!("IATTEST{:037}", 1));
  192. assert!(state.join("scopes.txt").exists());
  193. let _ = dir;
  194. }