e2e.rs 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. //! E2E bootstrap fixture tests (todo 31).
  2. //!
  3. //! All passwords and TOTP secrets below are synthetic test values in temp
  4. //! dirs — never the real `config/` tree, never real credentials.
  5. use std::fs;
  6. use provision::e2e::{
  7. CHANGE_CONFIRM_FIELD, CHANGE_NEW_PASSWORD_FIELD, CONSENT_APPROVE_VALUE, CONSENT_DECISION_FIELD,
  8. CONSENT_REQUEST_ID_FIELD, CSRF_FIELD, E2eError, LOGIN_CODE_FIELD, LOGIN_EMAIL_FIELD,
  9. LOGIN_PASSWORD_FIELD, MISSING_FIXTURE_HINT, WriteOutcome, load, write_initial,
  10. };
  11. #[cfg(unix)]
  12. fn assert_0600(path: &std::path::Path) {
  13. use std::os::unix::fs::PermissionsExt as _;
  14. let mode = fs::metadata(path)
  15. .expect("fixture metadata")
  16. .permissions()
  17. .mode()
  18. & 0o777;
  19. assert_eq!(mode, 0o600, "fixture at `{}` is not 0600", path.display());
  20. }
  21. #[test]
  22. fn writes_email_and_password_0600_only_when_absent() {
  23. let dir = tempfile::tempdir().expect("tempdir");
  24. let out = dir.path().join("e2e.json");
  25. let outcome = write_initial(&out, "demo@example.com", "test-password-01").expect("first write");
  26. assert_eq!(outcome, WriteOutcome::Created);
  27. let raw = fs::read_to_string(&out).expect("fixture readable");
  28. let value: serde_json::Value = serde_json::from_str(&raw).expect("fixture is JSON");
  29. assert_eq!(
  30. value.get("email").and_then(|email| email.as_str()),
  31. Some("demo@example.com")
  32. );
  33. assert_eq!(
  34. value.get("password").and_then(|password| password.as_str()),
  35. Some("test-password-01")
  36. );
  37. assert!(
  38. value.get("totp_secret").is_none(),
  39. "provision must not write the spec-owned totp_secret field"
  40. );
  41. #[cfg(unix)]
  42. assert_0600(&out);
  43. }
  44. #[test]
  45. fn leaves_an_existing_file_byte_for_byte_untouched() {
  46. let dir = tempfile::tempdir().expect("tempdir");
  47. let out = dir.path().join("e2e.json");
  48. // A spec-owned file: rotated password plus a captured TOTP secret.
  49. let spec_owned =
  50. r#"{"email":"demo@example.com","password":"rotated-by-spec","totp_secret":"JBSWY3DPEHPK3PXP"}"#;
  51. fs::write(&out, spec_owned).expect("pre-existing fixture");
  52. let outcome = write_initial(&out, "demo@example.com", "initial-password").expect("second write");
  53. assert_eq!(outcome, WriteOutcome::Kept);
  54. let raw = fs::read_to_string(&out).expect("fixture readable");
  55. assert_eq!(raw, spec_owned, "existing fixture must be untouched");
  56. }
  57. #[test]
  58. fn written_fixture_round_trips_through_load() {
  59. let dir = tempfile::tempdir().expect("tempdir");
  60. let out = dir.path().join("nested/e2e.json");
  61. write_initial(&out, "demo@example.com", "test-password-01").expect("write");
  62. let fixture = load(&out).expect("load");
  63. assert_eq!(fixture.email, "demo@example.com");
  64. assert_eq!(fixture.password, "test-password-01");
  65. assert_eq!(fixture.totp_secret, None);
  66. }
  67. #[test]
  68. fn missing_fixture_load_fails_with_run_provision_first_hint() {
  69. let dir = tempfile::tempdir().expect("tempdir");
  70. let out = dir.path().join("e2e.json");
  71. let result = load(&out);
  72. assert!(
  73. matches!(result, Err(E2eError::Missing { .. })),
  74. "expected Missing, got {result:?}"
  75. );
  76. let message = result.expect_err("missing fixture").to_string();
  77. assert!(
  78. message.contains("provision"),
  79. "hint must name provision: {message}"
  80. );
  81. assert!(
  82. message.contains(MISSING_FIXTURE_HINT),
  83. "hint must match the documented contract: {message}"
  84. );
  85. }
  86. #[test]
  87. fn malformed_fixture_is_a_typed_parse_error() {
  88. let dir = tempfile::tempdir().expect("tempdir");
  89. let out = dir.path().join("e2e.json");
  90. fs::write(&out, "{not json").expect("malformed fixture");
  91. let result = load(&out);
  92. assert!(matches!(result, Err(E2eError::Parse { .. })));
  93. }
  94. #[test]
  95. fn debug_redacts_password_and_totp_secret() {
  96. let dir = tempfile::tempdir().expect("tempdir");
  97. let out = dir.path().join("e2e.json");
  98. write_initial(&out, "demo@example.com", "S3CRET-PW").expect("write");
  99. let mut fixture = load(&out).expect("load");
  100. fixture.totp_secret = Some("S3CRET-TOTP".to_owned());
  101. let rendered = format!("{fixture:?}");
  102. assert!(
  103. !rendered.contains("S3CRET-PW"),
  104. "password leaked: {rendered}"
  105. );
  106. assert!(
  107. !rendered.contains("S3CRET-TOTP"),
  108. "totp secret leaked: {rendered}"
  109. );
  110. assert!(rendered.contains("demo@example.com"));
  111. }
  112. #[test]
  113. fn documented_selectors_match_the_d_oauth_templates() {
  114. // Login form (`src/ui/login.rs` `LoginTemplate`).
  115. assert_eq!(LOGIN_EMAIL_FIELD, "email");
  116. assert_eq!(LOGIN_PASSWORD_FIELD, "password");
  117. assert_eq!(LOGIN_CODE_FIELD, "code");
  118. // Hidden form token on every d-OAuth form.
  119. assert_eq!(CSRF_FIELD, "csrf");
  120. // Password-change form (`PasswordTemplate`).
  121. assert_eq!(CHANGE_NEW_PASSWORD_FIELD, "new_password");
  122. assert_eq!(CHANGE_CONFIRM_FIELD, "confirm");
  123. // Consent form (`src/ui/consent.rs` `ConsentTemplate`/`ConsentForm`).
  124. assert_eq!(CONSENT_REQUEST_ID_FIELD, "request_id");
  125. assert_eq!(CONSENT_DECISION_FIELD, "decision");
  126. assert_eq!(CONSENT_APPROVE_VALUE, "approve");
  127. }