use crate::util::regex::UUID_REGEX;
use insta::Settings;
use insta::internals::SettingsBindDropGuard;
use itertools::Itertools;
use regex::Regex;
use std::str::FromStr;
use std::sync::LazyLock;
use std::thread::current;
const BEARER_TOKEN_REGEX: &str = r"Bearer [\w\.-]+";
const POSTGRES_URI_REGEX: &str = r"postgres://(\w|\d|@|:|\/|\.)+";
const MYSQL_URI_REGEX: &str = r"mysql://(\w|\d|@|:|\/|\.)+";
const REDIS_URI_REGEX: &str = r"redis://(\w|\d|@|:|\/|\.)+";
const SMTP_URI_REGEX: &str = r"smtp://(\w|\d|@|:|\/|\.)+";
const TIMESTAMP_REGEX: &str = r"(\d{4}-[01]\d-[0-3]\d\s?T?\s?[0-2]\d:[0-5]\d:[0-5]\d\.\d+\s?([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\d\s?T?\s?[0-2]\d:[0-5]\d:[0-5]\d\s?([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\d\s?T?\s?[0-2]\d:[0-5]\d\s?([+-][0-2]\d:[0-5]\d|Z))";
#[derive(bon::Builder)]
#[builder(finish_fn(vis = "", name = build_internal))]
#[non_exhaustive]
pub struct TestCaseConfig {
pub settings: Option<Settings>,
#[builder(into)]
pub description: Option<String>,
#[builder(default = true)]
pub set_suffix: bool,
#[builder(default = true)]
pub redact_uuid: bool,
#[builder(default = true)]
pub redact_auth_tokens: bool,
#[builder(default = true)]
pub redact_postgres_uri: bool,
#[builder(default = true)]
pub redact_mysql_uri: bool,
#[builder(default = true)]
pub redact_redis_uri: bool,
#[builder(default = true)]
pub redact_smtp_uri: bool,
#[builder(default = true)]
pub redact_timestamp: bool,
#[builder(default = true)]
pub bind_scope: bool,
}
impl<S: test_case_config_builder::IsComplete> TestCaseConfigBuilder<S> {
pub fn build(self) -> TestCase {
self.build_internal().into()
}
}
#[non_exhaustive]
pub struct TestCase {
pub description: String,
pub settings: Settings,
_settings_guard: Option<SettingsBindDropGuard>,
}
impl TestCase {
pub fn new() -> Self {
TestCaseConfig::builder().build()
}
}
impl Default for TestCase {
fn default() -> Self {
TestCase::new()
}
}
impl From<TestCaseConfig> for TestCase {
fn from(value: TestCaseConfig) -> Self {
let mut settings = value.settings.unwrap_or(Settings::clone_current());
let description = value
.description
.unwrap_or(description_from_current_thread());
if value.set_suffix {
snapshot_set_suffix(&mut settings, &description);
}
if value.redact_uuid {
snapshot_redact_uuid(&mut settings);
}
if value.redact_auth_tokens {
snapshot_redact_bearer_tokens(&mut settings);
}
if value.redact_postgres_uri {
snapshot_redact_postgres_uri(&mut settings);
}
if value.redact_mysql_uri {
snapshot_redact_mysql_uri(&mut settings);
}
if value.redact_redis_uri {
snapshot_redact_redis_uri(&mut settings);
}
if value.redact_smtp_uri {
snapshot_redact_smtp_uri(&mut settings);
}
if value.redact_timestamp {
snapshot_redact_timestamp(&mut settings);
}
let _settings_guard = if value.bind_scope {
Some(settings.bind_to_scope())
} else {
None
};
Self {
description,
settings,
_settings_guard,
}
}
}
pub fn snapshot_set_suffix<'a>(settings: &'a mut Settings, suffix: &str) -> &'a mut Settings {
settings.set_snapshot_suffix(suffix);
settings
}
pub fn snapshot_redact_uuid(settings: &mut Settings) -> &mut Settings {
settings.add_filter(UUID_REGEX, "[uuid]");
settings
}
pub fn snapshot_redact_bearer_tokens(settings: &mut Settings) -> &mut Settings {
settings.add_filter(BEARER_TOKEN_REGEX, "Sensitive");
settings
}
pub fn snapshot_redact_postgres_uri(settings: &mut Settings) -> &mut Settings {
settings.add_filter(POSTGRES_URI_REGEX, "postgres://[Sensitive]");
settings
}
pub fn snapshot_redact_mysql_uri(settings: &mut Settings) -> &mut Settings {
settings.add_filter(MYSQL_URI_REGEX, "mysql://[Sensitive]");
settings
}
pub fn snapshot_redact_redis_uri(settings: &mut Settings) -> &mut Settings {
settings.add_filter(REDIS_URI_REGEX, "redis://[Sensitive]");
settings
}
pub fn snapshot_redact_smtp_uri(settings: &mut Settings) -> &mut Settings {
settings.add_filter(SMTP_URI_REGEX, "smtp://[Sensitive]");
settings
}
pub fn snapshot_redact_timestamp(settings: &mut Settings) -> &mut Settings {
settings.add_filter(TIMESTAMP_REGEX, "[timestamp]");
settings
}
pub(crate) fn description_from_current_thread() -> String {
let thread_name = current().name().unwrap_or("").to_string();
description_from_thread_name(&thread_name)
}
fn description_from_thread_name(name: &str) -> String {
name.split("::")
.map(|item| {
if item.starts_with("case_") {
item.split('_').skip(2).join("_")
} else {
item.to_string()
}
})
.last()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| fallback_description(name))
}
const CASE_PREFIX: &str = "case_";
static CASE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
#[allow(clippy::expect_used)]
Regex::from_str(&format!(r"{CASE_PREFIX}(\d+)")).expect("Unable to parse regex")
});
fn fallback_description(name: &str) -> String {
#[allow(clippy::expect_used)]
let last = name
.split("::")
.last()
.expect("No string segments after splitting by `::`")
.to_string();
CASE_REGEX
.captures(&last)
.and_then(|captures| captures.get(1))
.map(|m| format!("{CASE_PREFIX}{:0>2}", m.as_str()))
.unwrap_or(last)
}
#[cfg(test)]
mod tests {
use super::*;
use insta::assert_snapshot;
use rstest::{fixture, rstest};
use uuid::Uuid;
#[fixture]
#[cfg_attr(coverage_nightly, coverage(off))]
fn case() -> TestCase {
Default::default()
}
#[rstest]
#[case(0, false)]
#[case::rstest_description(1, false)]
#[case(2, true)]
#[cfg_attr(coverage_nightly, coverage(off))]
fn description(#[case] num: u32, #[case] manual_description: bool) {
let _case = if manual_description {
TestCaseConfig::builder()
.description("manual_description")
.build()
} else {
TestCase::new()
};
assert_snapshot!(num);
}
#[rstest]
#[case(0, false, false)]
#[case(1, true, false)]
#[case(2, false, true)]
#[cfg_attr(coverage_nightly, coverage(off))]
fn bind_scope(
#[case] num: u32,
#[case] auto_bind_scope: bool,
#[case] manual_bind_scope: bool,
) {
assert!(!(auto_bind_scope && manual_bind_scope));
let case = TestCaseConfig::builder()
.bind_scope(auto_bind_scope)
.build();
if manual_bind_scope {
case.settings.bind(|| {
assert_snapshot!(num);
})
} else {
assert_snapshot!(num);
}
}
#[test]
#[cfg_attr(coverage_nightly, coverage(off))]
fn uuid() {
let _case = TestCase::new();
let uuid = Uuid::new_v4();
assert_snapshot!(format!("Foo '{uuid}' bar"));
}
#[rstest]
#[case("Bearer 1234")]
#[case("Bearer access-token")]
#[case("Bearer some.jwt.token")]
#[case("Bearer foo-bar.baz-1234")]
#[case("Bearer token;")]
#[cfg_attr(coverage_nightly, coverage(off))]
fn bearer_token(_case: TestCase, #[case] token: &str) {
assert_snapshot!(format!("Foo {token} bar"));
}
#[rstest]
#[case("postgres://example:example@example.com:1234/example")]
#[case("postgres://example:1234")]
#[case("postgres://localhost")]
#[case("postgres://example.com")]
#[case("postgres://192.168.1.1:3000")]
#[case("postgres://192.168.1.1:3000/example")]
#[cfg_attr(coverage_nightly, coverage(off))]
fn postgres_uri(_case: TestCase, #[case] uri: &str) {
assert_snapshot!(format!("uri = {uri}"));
}
#[rstest]
#[case("mysql://example:example@example.com:1234/example")]
#[case("mysql://example:1234")]
#[case("mysql://localhost")]
#[case("mysql://example.com")]
#[case("mysql://192.168.1.1:3000")]
#[case("mysql://192.168.1.1:3000/example")]
#[cfg_attr(coverage_nightly, coverage(off))]
fn mysql_uri(_case: TestCase, #[case] uri: &str) {
assert_snapshot!(format!("uri = {uri}"));
}
#[rstest]
#[case("redis://example:example@example.com:1234/example")]
#[case("redis://example:1234")]
#[case("redis://localhost")]
#[case("redis://example.com")]
#[case("redis://192.168.1.1:3000")]
#[case("redis://192.168.1.1:3000/example")]
#[cfg_attr(coverage_nightly, coverage(off))]
fn redis_uri(_case: TestCase, #[case] uri: &str) {
assert_snapshot!(format!("uri = {uri}"));
}
#[rstest]
#[case("smtp://example:example@example.com:1234/example")]
#[case("smtp://example:1234")]
#[case("smtp://localhost")]
#[case("smtp://example.com")]
#[case("smtp://192.168.1.1:3000")]
#[case("smtp://192.168.1.1:3000/example")]
#[cfg_attr(coverage_nightly, coverage(off))]
fn smtp_uri(_case: TestCase, #[case] uri: &str) {
assert_snapshot!(format!("uri = {uri}"));
}
#[rstest]
#[case("")]
#[case("foo")]
#[case("foo::bar")]
#[case("foo::bar::x_y_z_1_2_3")]
#[case("foo::bar::case_1_x_y_z_1_2_3")]
#[case("foo::bar::case_1")]
#[case("foo::bar::case_11")]
#[cfg_attr(coverage_nightly, coverage(off))]
fn description_from_thread_name(_case: TestCase, #[case] name: &str) {
let description = super::description_from_thread_name(name);
assert_snapshot!(description);
}
#[rstest]
#[case("case_1")]
#[case("foo::bar::case_1")]
#[case("foo::bar::case_11")]
#[cfg_attr(coverage_nightly, coverage(off))]
fn fallback_description(_case: TestCase, #[case] name: &str) {
let description = super::description_from_thread_name(name);
assert_snapshot!(description);
}
}