Skip to content

Commit 7de4930

Browse files
max-sixtyclaude
andauthored
Apply redactions to snapshot metadata (#813)
## Summary This PR implements consistent redaction behavior for both snapshot content and metadata, addressing a long-standing inconsistency where `Settings.add_redaction()` worked for snapshot content but not for metadata set via `Settings.set_info()`. ## Problem Previously, redactions were only applied to snapshot content during serialization, not to metadata. This created surprising behavior for users of libraries like `insta-cmd` that capture sensitive data (API keys, credentials, etc.) in snapshot metadata. **Before this change:** ```yaml --- info: env: API_KEY: sk_live_abc123def456 # ⚠️ Secret exposed! --- content here ``` **After this change:** ```yaml --- info: env: API_KEY: "[REDACTED]" # ✅ Redacted --- content here ``` ## Implementation ### Core Changes 1. **Created shared redaction helper** (`Redactions::apply_to_content()`) - Eliminates code duplication between metadata and content redaction - Both paths now use identical redaction logic 2. **Modified `ActualSettings::info()`** to apply redactions to metadata - Metadata is redacted eagerly when set (prevents sensitive data in memory) - Content is still redacted lazily during serialization 3. **Updated documentation** - `set_info()`: Documents that redactions are automatically applied - `set_raw_info()`: Documents that it does NOT apply redactions (low-level API) ### Test Coverage Added two tests documenting the behavior: - `test_metadata_redaction`: Validates `set_info()` applies redactions - `test_metadata_raw_info_no_redaction`: Validates `set_raw_info()` does not Snapshots clearly show the difference: ```diff --- set_info() snapshot +++ set_raw_info() snapshot info: - secret: "[REDACTED]" + secret: sensitive_value ``` ## Benefits 1. **Security**: Easier to prevent secrets in snapshots 2. **Consistency**: Redactions work the same everywhere 3. **User expectations**: `add_redaction()` now applies to all serialized data 4. **Minimal change**: ~30 lines of implementation code ## Breaking Changes **Low impact**: Existing snapshots with sensitive data in metadata will change (values will become redacted). This is the intended behavior and improves security. **Migration**: Users can either: - Update snapshots (recommended - secrets should be redacted) - Remove specific redactions if certain metadata values must be preserved - Use `set_raw_info()` for the low-level API that bypasses redactions ## Testing - ✅ All 164 tests pass (added 2 new tests) - ✅ All lints pass (pre-commit) - ✅ No compiler warnings - ✅ Snapshot tests validate both redaction and non-redaction behavior ## Related Addresses the use case described in the proposal where libraries like `insta-cmd` need to redact environment variables captured in metadata. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 90f6ad8 commit 7de4930

File tree

5 files changed

+93
-6
lines changed

5 files changed

+93
-6
lines changed

insta/src/serialization.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@ pub fn serialize_content(mut content: Content, format: SerializationFormat) -> S
2929
}
3030
#[cfg(feature = "redactions")]
3131
{
32-
for (selector, redaction) in settings.iter_redactions() {
33-
content = selector.redact(content, redaction);
34-
}
32+
content = settings.apply_redactions(content);
3533
}
3634
content
3735
});

insta/src/settings.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ impl<'a> From<Vec<(&'a str, Redaction)>> for Redactions {
5656
}
5757
}
5858

59+
#[cfg(feature = "redactions")]
60+
impl Redactions {
61+
/// Applies all redactions to the given content.
62+
pub(crate) fn apply_to_content(&self, mut content: Content) -> Content {
63+
for (selector, redaction) in self.0.iter() {
64+
content = selector.redact(content, redaction);
65+
}
66+
content
67+
}
68+
}
69+
5970
#[derive(Clone)]
6071
#[doc(hidden)]
6172
pub struct ActualSettings {
@@ -100,6 +111,15 @@ impl ActualSettings {
100111
pub fn info<S: Serialize>(&mut self, s: &S) {
101112
let serializer = ContentSerializer::<ValueError>::new();
102113
let content = Serialize::serialize(s, serializer).unwrap();
114+
115+
// Apply redactions to metadata immediately when set. Unlike snapshot
116+
// content (which is redacted lazily during serialization), metadata is
117+
// redacted eagerly to ensure sensitive data never reaches the stored
118+
// settings. The redacted content is then written to the snapshot file
119+
// as-is without further redaction.
120+
#[cfg(feature = "redactions")]
121+
let content = self.redactions.apply_to_content(content);
122+
103123
self.info = Some(content);
104124
}
105125

@@ -319,6 +339,9 @@ impl Settings {
319339
/// As an example the input parameters to the function that creates the snapshot
320340
/// can be persisted here.
321341
///
342+
/// **Note:** Redactions configured via [`Self::add_redaction`] are automatically
343+
/// applied to the info metadata when it is set.
344+
///
322345
/// Alternatively you can use [`Self::set_raw_info`] instead.
323346
#[cfg(feature = "serde")]
324347
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
@@ -329,6 +352,10 @@ impl Settings {
329352
/// Sets the info from a content object.
330353
///
331354
/// This works like [`Self::set_info`] but does not require [`serde`].
355+
///
356+
/// **Note:** Unlike [`Self::set_info`], this method does NOT automatically apply
357+
/// redactions. If you need redactions applied to metadata, use [`Self::set_info`]
358+
/// instead (which requires the `serde` feature).
332359
pub fn set_raw_info(&mut self, content: &Content) {
333360
self._private_inner_mut().raw_info(content);
334361
}
@@ -421,11 +448,11 @@ impl Settings {
421448
self._private_inner_mut().redactions.0.clear();
422449
}
423450

424-
/// Iterate over the redactions.
451+
/// Apply redactions to content.
425452
#[cfg(feature = "redactions")]
426453
#[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
427-
pub(crate) fn iter_redactions(&self) -> impl Iterator<Item = (&Selector<'_>, &Redaction)> {
428-
self.inner.redactions.0.iter().map(|(a, b)| (a, &**b))
454+
pub(crate) fn apply_redactions(&self, content: Content) -> Content {
455+
self.inner.redactions.apply_to_content(content)
429456
}
430457

431458
/// Adds a new filter.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
source: insta/tests/test_redaction.rs
3+
expression: "&vec![1, 2, 3]"
4+
info:
5+
secret: sensitive_value
6+
public: visible
7+
---
8+
- 1
9+
- 2
10+
- 3
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
source: insta/tests/test_redaction.rs
3+
expression: "&vec![1, 2, 3]"
4+
info:
5+
secret: "[REDACTED]"
6+
public: visible
7+
---
8+
- 1
9+
- 2
10+
- 3

insta/tests/test_redaction.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,3 +608,45 @@ fn test_named_redacted_supported_form() {
608608
}
609609
);
610610
}
611+
612+
#[cfg(all(feature = "yaml", feature = "redactions"))]
613+
#[test]
614+
fn test_metadata_redaction() {
615+
#[derive(Serialize)]
616+
struct Info {
617+
secret: String,
618+
public: String,
619+
}
620+
621+
let mut settings = insta::Settings::new();
622+
settings.add_redaction(".secret", "[REDACTED]");
623+
settings.set_info(&Info {
624+
secret: "sensitive_value".into(),
625+
public: "visible".into(),
626+
});
627+
628+
settings.bind(|| {
629+
assert_yaml_snapshot!("metadata_redaction_test", &vec![1, 2, 3]);
630+
});
631+
}
632+
633+
#[cfg(all(feature = "yaml", feature = "redactions"))]
634+
#[test]
635+
fn test_metadata_raw_info_no_redaction() {
636+
use insta::internals::Content;
637+
638+
let mut settings = insta::Settings::new();
639+
settings.add_redaction(".secret", "[REDACTED]");
640+
641+
// Create content that would be redacted if redactions were applied
642+
let content = Content::Map(vec![
643+
(Content::from("secret"), Content::from("sensitive_value")),
644+
(Content::from("public"), Content::from("visible")),
645+
]);
646+
647+
settings.set_raw_info(&content);
648+
649+
settings.bind(|| {
650+
assert_yaml_snapshot!("metadata_raw_info_no_redaction", &vec![1, 2, 3]);
651+
});
652+
}

0 commit comments

Comments
 (0)