1use crate::ExpectationFailure;
4
5pub const MASK: &str = "***";
7
8const SHORTEST_PIECE: usize = 3;
12
13impl ExpectationFailure {
14 #[must_use]
20 pub fn without_secret(mut self, secret: &str) -> Self {
21 if secret.is_empty() {
22 return self;
23 }
24 self.message = mask(&self.message, secret);
25 self.hint = self.hint.map(|h| mask(&h, secret));
26 for s in &mut self.suggestions {
27 *s = mask(s, secret);
28 }
29 for line in &mut self.device_log {
30 *line = mask(line, secret);
31 }
32 for e in &mut self.visible_elements {
33 e.text = e.text.as_deref().map(|t| mask_field(t, secret));
34 e.name = e.name.as_deref().map(|n| mask_field(n, secret));
35 }
36 self
37 }
38}
39
40pub fn mask(text: &str, secret: &str) -> String {
42 let whole = text.replace(secret, MASK);
43 let mut out = String::with_capacity(whole.len());
44 let mut rest = whole.as_str();
45 while let Some(open) = rest.find('"') {
46 out.push_str(&rest[..=open]);
47 rest = &rest[open + 1..];
48 let Some(close) = rest.find('"') else {
49 break;
50 };
51 let quoted = &rest[..close];
52 if is_piece(quoted, secret) {
53 out.push_str(MASK);
54 } else {
55 out.push_str(quoted);
56 }
57 out.push('"');
58 rest = &rest[close + 1..];
59 }
60 out.push_str(rest);
61 out
62}
63
64fn mask_field(text: &str, secret: &str) -> String {
67 if is_piece(text, secret) {
68 MASK.to_string()
69 } else {
70 text.replace(secret, MASK)
71 }
72}
73
74fn is_piece(s: &str, secret: &str) -> bool {
75 s.chars().count() >= SHORTEST_PIECE.min(secret.chars().count())
76 && !s.is_empty()
77 && secret.contains(s)
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use crate::{FailureCode, FailureInit};
84
85 #[test]
86 fn the_secret_and_quoted_pieces_of_it_are_masked() {
87 let said = r#"input-text: typed "run1-first" into f in 1 chunk(s); the field held "" and holds "un1-first"."#;
88 assert_eq!(
89 mask(said, "run1-first"),
90 r#"input-text: typed "***" into f in 1 chunk(s); the field held "" and holds "***"."#
91 );
92 assert_eq!(
94 mask(r#"id "input-password""#, "hunter2"),
95 r#"id "input-password""#
96 );
97 assert_eq!(mask(r#"held "hu""#, "hunter2"), r#"held "hu""#);
99 assert_eq!(mask("typed ab here", "ab"), "typed *** here");
101 }
102
103 #[test]
104 fn every_part_of_a_failure_loses_the_secret() {
105 let mut e = ExpectationFailure::new(FailureInit {
106 code: Some(FailureCode::DriverError),
107 message: "fill sent hunter2".into(),
108 hint: Some("the field holds \"unter2\"".into()),
109 ..Default::default()
110 });
111 e.suggestions = vec!["type hunter2 again".into()];
112 e.device_log = vec!["log: hunter2".into()];
113 let e = e.without_secret("hunter2");
114 let all = format!(
115 "{} {:?} {:?} {:?}",
116 e.message, e.hint, e.suggestions, e.device_log
117 );
118 assert!(!all.contains("hunter2") && !all.contains("unter2"), "{all}");
119 assert_eq!(e.message, "fill sent ***");
120 }
121}