use crate::ExpectationFailure;
pub const MASK: &str = "***";
const SHORTEST_PIECE: usize = 3;
impl ExpectationFailure {
#[must_use]
pub fn without_secret(mut self, secret: &str) -> Self {
if secret.is_empty() {
return self;
}
self.message = mask(&self.message, secret);
self.hint = self.hint.map(|h| mask(&h, secret));
for s in &mut self.suggestions {
*s = mask(s, secret);
}
for line in &mut self.device_log {
*line = mask(line, secret);
}
for e in &mut self.visible_elements {
e.text = e.text.as_deref().map(|t| mask_field(t, secret));
e.name = e.name.as_deref().map(|n| mask_field(n, secret));
}
self
}
}
pub fn mask(text: &str, secret: &str) -> String {
let whole = text.replace(secret, MASK);
let mut out = String::with_capacity(whole.len());
let mut rest = whole.as_str();
while let Some(open) = rest.find('"') {
out.push_str(&rest[..=open]);
rest = &rest[open + 1..];
let Some(close) = rest.find('"') else {
break;
};
let quoted = &rest[..close];
if is_piece(quoted, secret) {
out.push_str(MASK);
} else {
out.push_str(quoted);
}
out.push('"');
rest = &rest[close + 1..];
}
out.push_str(rest);
out
}
fn mask_field(text: &str, secret: &str) -> String {
if is_piece(text, secret) {
MASK.to_string()
} else {
text.replace(secret, MASK)
}
}
fn is_piece(s: &str, secret: &str) -> bool {
s.chars().count() >= SHORTEST_PIECE.min(secret.chars().count())
&& !s.is_empty()
&& secret.contains(s)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{FailureCode, FailureInit};
#[test]
fn the_secret_and_quoted_pieces_of_it_are_masked() {
let said = r#"input-text: typed "run1-first" into f in 1 chunk(s); the field held "" and holds "un1-first"."#;
assert_eq!(
mask(said, "run1-first"),
r#"input-text: typed "***" into f in 1 chunk(s); the field held "" and holds "***"."#
);
assert_eq!(
mask(r#"id "input-password""#, "hunter2"),
r#"id "input-password""#
);
assert_eq!(mask(r#"held "hu""#, "hunter2"), r#"held "hu""#);
assert_eq!(mask("typed ab here", "ab"), "typed *** here");
}
#[test]
fn every_part_of_a_failure_loses_the_secret() {
let mut e = ExpectationFailure::new(FailureInit {
code: Some(FailureCode::DriverError),
message: "fill sent hunter2".into(),
hint: Some("the field holds \"unter2\"".into()),
..Default::default()
});
e.suggestions = vec!["type hunter2 again".into()];
e.device_log = vec!["log: hunter2".into()];
let e = e.without_secret("hunter2");
let all = format!(
"{} {:?} {:?} {:?}",
e.message, e.hint, e.suggestions, e.device_log
);
assert!(!all.contains("hunter2") && !all.contains("unter2"), "{all}");
assert_eq!(e.message, "fill sent ***");
}
}