Approval-based snapshot testing library for Go (mirror)
1package shutter
2
3import (
4 "regexp"
5 "strings"
6)
7
8// regexScrubber replaces all matches of a regex pattern with a replacement string.
9type regexScrubber struct {
10 pattern *regexp.Regexp
11 replacement string
12}
13
14func (r *regexScrubber) isOption() {}
15
16func (r *regexScrubber) Scrub(content string) string {
17 return r.pattern.ReplaceAllString(content, r.replacement)
18}
19
20// ScrubRegex creates a scrubber that replaces all matches of the given
21// regex pattern with the replacement string.
22//
23// Example:
24//
25// shutter.ScrubRegex(`user-\d+`, "<USER_ID>")
26func ScrubRegex(pattern string, replacement string) Scrubber {
27 re := regexp.MustCompile(pattern)
28 return ®exScrubber{
29 pattern: re,
30 replacement: replacement,
31 }
32}
33
34// exactMatchScrubber replaces exact string matches with a replacement.
35type exactMatchScrubber struct {
36 match string
37 replacement string
38}
39
40func (e *exactMatchScrubber) isOption() {}
41
42func (e *exactMatchScrubber) Scrub(content string) string {
43 return strings.ReplaceAll(content, e.match, e.replacement)
44}
45
46// ScrubExact creates a scrubber that replaces exact string matches.
47//
48// Example:
49//
50// shutter.ScrubExact("secret_value", "<REDACTED>")
51func ScrubExact(match string, replacement string) Scrubber {
52 return &exactMatchScrubber{
53 match: match,
54 replacement: replacement,
55 }
56}
57
58// Common regex patterns for scrubbing
59var (
60 uuidPattern = regexp.MustCompile(`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`)
61 iso8601Pattern = regexp.MustCompile(`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?`)
62 emailPattern = regexp.MustCompile(`[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}`)
63 // Unix timestamp pattern - matches 10-13 digit numbers (Unix timestamps in seconds or milliseconds)
64 // Note: This is aggressive and may match other numbers. Use with caution or customize.
65 unixTsPattern = regexp.MustCompile(`\b\d{10,13}\b`)
66 // IPv4 pattern with basic range validation
67 ipv4Pattern = regexp.MustCompile(`\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b`)
68 // Credit card pattern - matches 16 digit numbers with optional separators
69 creditCardPattern = regexp.MustCompile(`\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b`)
70 jwtPattern = regexp.MustCompile(`eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*`)
71 // Date patterns
72 datePattern = regexp.MustCompile(`\b\d{4}[-/]\d{2}[-/]\d{2}\b|\b\d{2}[-/]\d{2}[-/]\d{4}\b`)
73 // API key pattern - matches patterns like: sk_live_..., pk_test_..., api_key_...
74 apiKeyPattern = regexp.MustCompile(`\b(sk|pk|api[_-]?key)[_-](live|test|prod|dev)[_-][a-zA-Z0-9]+\b`)
75)
76
77// ScrubUUID replaces all UUIDs with "<UUID>".
78//
79// Example:
80//
81// shutter.Snap(t, "user", user, shutter.ScrubUUID())
82func ScrubUUID() Scrubber {
83 return ®exScrubber{
84 pattern: uuidPattern,
85 replacement: "<UUID>",
86 }
87}
88
89// ScrubTimestamp replaces ISO8601 timestamps with "<TIMESTAMP>".
90//
91// Example:
92//
93// shutter.Snap(t, "event", event, shutter.ScrubTimestamp())
94func ScrubTimestamp() Scrubber {
95 return ®exScrubber{
96 pattern: iso8601Pattern,
97 replacement: "<TIMESTAMP>",
98 }
99}
100
101// ScrubEmail replaces email addresses with "<EMAIL>".
102//
103// Example:
104//
105// shutter.Snap(t, "user", user, shutter.ScrubEmail())
106func ScrubEmail() Scrubber {
107 return ®exScrubber{
108 pattern: emailPattern,
109 replacement: "<EMAIL>",
110 }
111}
112
113// ScrubUnixTimestamp replaces Unix timestamps (10-13 digits) with "<UNIX_TS>".
114// Note: This is aggressive and may match other long numbers. For more conservative
115// scrubbing with context keywords, use ScrubRegex with a custom pattern.
116//
117// Example:
118//
119// shutter.Snap(t, "data", data, shutter.ScrubUnixTimestamp())
120func ScrubUnixTimestamp() Scrubber {
121 return ®exScrubber{
122 pattern: unixTsPattern,
123 replacement: "<UNIX_TS>",
124 }
125}
126
127// ScrubIP replaces IPv4 addresses with "<IP>".
128//
129// Example:
130//
131// shutter.Snap(t, "request", request, shutter.ScrubIP())
132func ScrubIP() Scrubber {
133 return ®exScrubber{
134 pattern: ipv4Pattern,
135 replacement: "<IP>",
136 }
137}
138
139// ScrubCreditCard replaces credit card numbers with "<CREDIT_CARD>".
140//
141// Example:
142//
143// shutter.Snap(t, "payment", payment, shutter.ScrubCreditCard())
144func ScrubCreditCard() Scrubber {
145 return ®exScrubber{
146 pattern: creditCardPattern,
147 replacement: "<CREDIT_CARD>",
148 }
149}
150
151// ScrubJWT replaces JWT tokens with "<JWT>".
152//
153// Example:
154//
155// shutter.Snap(t, "auth", authData, shutter.ScrubJWT())
156func ScrubJWT() Scrubber {
157 return ®exScrubber{
158 pattern: jwtPattern,
159 replacement: "<JWT>",
160 }
161}
162
163// ScrubDate replaces various date formats with "<DATE>".
164//
165// Example:
166//
167// shutter.Snap(t, "data", data, shutter.ScrubDate())
168func ScrubDate() Scrubber {
169 return ®exScrubber{
170 pattern: datePattern,
171 replacement: "<DATE>",
172 }
173}
174
175// ScrubAPIKey replaces common API key patterns with "<API_KEY>".
176// Matches patterns like: sk_live_..., pk_test_..., api_key_...
177//
178// Example:
179//
180// shutter.Snap(t, "config", config, shutter.ScrubAPIKey())
181func ScrubAPIKey() Scrubber {
182 return ®exScrubber{
183 pattern: apiKeyPattern,
184 replacement: "<API_KEY>",
185 }
186}
187
188// customScrubber allows users to provide a custom scrubbing function.
189type customScrubber struct {
190 scrubFunc func(string) string
191}
192
193func (c *customScrubber) isOption() {}
194
195func (c *customScrubber) Scrub(content string) string {
196 return c.scrubFunc(content)
197}
198
199// ScrubWith creates a scrubber using a custom function.
200// The function receives the snapshot content and should return the scrubbed content.
201//
202// Example:
203//
204// shutter.Snap(t, "data", data,
205// shutter.ScrubWith(func(content string) string {
206// return strings.ReplaceAll(content, "localhost", "<HOST>")
207// }),
208// )
209func ScrubWith(scrubFunc func(string) string) Scrubber {
210 return &customScrubber{
211 scrubFunc: scrubFunc,
212 }
213}