Approval-based snapshot testing library for Go (mirror)
1package transform
2
3import (
4 "regexp"
5 "strings"
6 "testing"
7)
8
9// Mock implementations for testing
10
11type mockScrubber struct {
12 fn func(string) string
13}
14
15func (m *mockScrubber) Scrub(content string) string {
16 return m.fn(content)
17}
18
19type mockIgnorePattern struct {
20 fn func(string, string) bool
21}
22
23func (m *mockIgnorePattern) ShouldIgnore(key, value string) bool {
24 return m.fn(key, value)
25}
26
27// Tests for ApplyScrubbers
28
29func TestApplyScrubbers_NoScrubbers(t *testing.T) {
30 input := "hello world"
31 result := ApplyScrubbers(input, nil)
32
33 if result != input {
34 t.Errorf("expected %q, got %q", input, result)
35 }
36}
37
38func TestApplyScrubbers_SingleScrubber(t *testing.T) {
39 scrubber := &mockScrubber{
40 fn: func(s string) string {
41 return strings.ReplaceAll(s, "secret", "<REDACTED>")
42 },
43 }
44
45 input := "my secret password"
46 expected := "my <REDACTED> password"
47 result := ApplyScrubbers(input, []Scrubber{scrubber})
48
49 if result != expected {
50 t.Errorf("expected %q, got %q", expected, result)
51 }
52}
53
54func TestApplyScrubbers_MultipleScrubbers(t *testing.T) {
55 scrubber1 := &mockScrubber{
56 fn: func(s string) string {
57 return strings.ReplaceAll(s, "foo", "FOO")
58 },
59 }
60 scrubber2 := &mockScrubber{
61 fn: func(s string) string {
62 return strings.ReplaceAll(s, "bar", "BAR")
63 },
64 }
65
66 input := "foo and bar"
67 expected := "FOO and BAR"
68 result := ApplyScrubbers(input, []Scrubber{scrubber1, scrubber2})
69
70 if result != expected {
71 t.Errorf("expected %q, got %q", expected, result)
72 }
73}
74
75func TestApplyScrubbers_OrderMatters(t *testing.T) {
76 // Test that scrubbers are applied in order
77 scrubber1 := &mockScrubber{
78 fn: func(s string) string {
79 return strings.ReplaceAll(s, "test", "TEST")
80 },
81 }
82 scrubber2 := &mockScrubber{
83 fn: func(s string) string {
84 return strings.ReplaceAll(s, "TEST", "FINAL")
85 },
86 }
87
88 input := "test value"
89 expected := "FINAL value"
90 result := ApplyScrubbers(input, []Scrubber{scrubber1, scrubber2})
91
92 if result != expected {
93 t.Errorf("expected %q, got %q", expected, result)
94 }
95}
96
97// Tests for TransformJSON
98
99func TestTransformJSON_InvalidJSON(t *testing.T) {
100 config := &Config{}
101 _, err := TransformJSON("not valid json", config)
102
103 if err == nil {
104 t.Error("expected error for invalid JSON")
105 }
106 if err != nil && !strings.Contains(err.Error(), "failed to unmarshal JSON") {
107 t.Errorf("unexpected error message: %v", err)
108 }
109}
110
111func TestTransformJSON_EmptyConfig(t *testing.T) {
112 config := &Config{}
113 input := `{"name":"John","age":30}`
114
115 result, err := TransformJSON(input, config)
116 if err != nil {
117 t.Fatalf("unexpected error: %v", err)
118 }
119
120 // Should return pretty-printed JSON
121 expected := "{\n \"age\": 30,\n \"name\": \"John\"\n}"
122 if result != expected {
123 t.Errorf("expected:\n%s\ngot:\n%s", expected, result)
124 }
125}
126
127func TestTransformJSON_WithScrubbers(t *testing.T) {
128 scrubber := &mockScrubber{
129 fn: func(s string) string {
130 return strings.ReplaceAll(s, "John", "<NAME>")
131 },
132 }
133
134 config := &Config{
135 Scrubbers: []Scrubber{scrubber},
136 }
137
138 input := `{"name":"John","age":30}`
139 result, err := TransformJSON(input, config)
140 if err != nil {
141 t.Fatalf("unexpected error: %v", err)
142 }
143
144 if !strings.Contains(result, "<NAME>") {
145 t.Errorf("expected scrubber to be applied, got: %s", result)
146 }
147 if strings.Contains(result, "John") {
148 t.Errorf("scrubber failed to replace 'John', got: %s", result)
149 }
150}
151
152func TestTransformJSON_WithIgnorePatterns(t *testing.T) {
153 ignorePattern := &mockIgnorePattern{
154 fn: func(key, value string) bool {
155 return key == "password"
156 },
157 }
158
159 config := &Config{
160 Ignore: []IgnorePattern{ignorePattern},
161 }
162
163 input := `{"username":"john","password":"secret"}`
164 result, err := TransformJSON(input, config)
165 if err != nil {
166 t.Fatalf("unexpected error: %v", err)
167 }
168
169 if strings.Contains(result, "password") {
170 t.Errorf("expected 'password' to be ignored, got: %s", result)
171 }
172 if !strings.Contains(result, "username") {
173 t.Errorf("expected 'username' to be present, got: %s", result)
174 }
175}
176
177func TestTransformJSON_ComplexNested(t *testing.T) {
178 ignorePattern := &mockIgnorePattern{
179 fn: func(key, value string) bool {
180 return key == "secret"
181 },
182 }
183
184 config := &Config{
185 Ignore: []IgnorePattern{ignorePattern},
186 }
187
188 input := `{
189 "user": {
190 "name": "John",
191 "secret": "hidden",
192 "nested": {
193 "secret": "also_hidden",
194 "public": "visible"
195 }
196 }
197 }`
198
199 result, err := TransformJSON(input, config)
200 if err != nil {
201 t.Fatalf("unexpected error: %v", err)
202 }
203
204 if strings.Contains(result, "hidden") || strings.Contains(result, "also_hidden") {
205 t.Errorf("expected nested secrets to be ignored, got: %s", result)
206 }
207 if !strings.Contains(result, "visible") {
208 t.Errorf("expected 'visible' to be present, got: %s", result)
209 }
210}
211
212func TestTransformJSON_WithArrays(t *testing.T) {
213 ignorePattern := &mockIgnorePattern{
214 fn: func(key, value string) bool {
215 return key == "id"
216 },
217 }
218
219 config := &Config{
220 Ignore: []IgnorePattern{ignorePattern},
221 }
222
223 input := `{
224 "users": [
225 {"id": 1, "name": "Alice"},
226 {"id": 2, "name": "Bob"}
227 ]
228 }`
229
230 result, err := TransformJSON(input, config)
231 if err != nil {
232 t.Fatalf("unexpected error: %v", err)
233 }
234
235 // Should remove id fields from all array elements
236 if strings.Contains(result, "\"id\"") {
237 t.Errorf("expected 'id' fields to be ignored in arrays, got: %s", result)
238 }
239 if !strings.Contains(result, "Alice") || !strings.Contains(result, "Bob") {
240 t.Errorf("expected names to be present, got: %s", result)
241 }
242}
243
244func TestTransformJSON_ScrubbersAndIgnoreCombined(t *testing.T) {
245 scrubber := &mockScrubber{
246 fn: func(s string) string {
247 re := regexp.MustCompile(`\d{3}-\d{3}-\d{4}`)
248 return re.ReplaceAllString(s, "<PHONE>")
249 },
250 }
251
252 ignorePattern := &mockIgnorePattern{
253 fn: func(key, value string) bool {
254 return key == "ssn"
255 },
256 }
257
258 config := &Config{
259 Scrubbers: []Scrubber{scrubber},
260 Ignore: []IgnorePattern{ignorePattern},
261 }
262
263 input := `{"name":"John","phone":"555-123-4567","ssn":"123-45-6789"}`
264 result, err := TransformJSON(input, config)
265 if err != nil {
266 t.Fatalf("unexpected error: %v", err)
267 }
268
269 // SSN field should be completely removed
270 if strings.Contains(result, "ssn") {
271 t.Errorf("expected 'ssn' field to be ignored, got: %s", result)
272 }
273
274 // Phone should be scrubbed
275 if !strings.Contains(result, "<PHONE>") {
276 t.Errorf("expected phone to be scrubbed, got: %s", result)
277 }
278 if strings.Contains(result, "555-123-4567") {
279 t.Errorf("expected phone number to be replaced, got: %s", result)
280 }
281}
282
283// Tests for helper functions
284
285func TestValueToString_String(t *testing.T) {
286 result := valueToString("hello")
287 if result != "hello" {
288 t.Errorf("expected 'hello', got %q", result)
289 }
290}
291
292func TestValueToString_Nil(t *testing.T) {
293 result := valueToString(nil)
294 if result != "null" {
295 t.Errorf("expected 'null', got %q", result)
296 }
297}
298
299func TestValueToString_BoolTrue(t *testing.T) {
300 result := valueToString(true)
301 if result != "true" {
302 t.Errorf("expected 'true', got %q", result)
303 }
304}
305
306func TestValueToString_BoolFalse(t *testing.T) {
307 result := valueToString(false)
308 if result != "false" {
309 t.Errorf("expected 'false', got %q", result)
310 }
311}
312
313func TestValueToString_Float64(t *testing.T) {
314 result := valueToString(42.5)
315 if result != "42.5" {
316 t.Errorf("expected '42.5', got %q", result)
317 }
318}
319
320func TestValueToString_Int(t *testing.T) {
321 result := valueToString(42)
322 if result != "42" {
323 t.Errorf("expected '42', got %q", result)
324 }
325}
326
327func TestValueToString_ComplexType(t *testing.T) {
328 // Map should be marshalled to JSON
329 m := map[string]any{"key": "value"}
330 result := valueToString(m)
331 if result != `{"key":"value"}` {
332 t.Errorf("expected JSON string, got %q", result)
333 }
334}
335
336func TestFilterMap_RemovesMatchingKeys(t *testing.T) {
337 ignorePattern := &mockIgnorePattern{
338 fn: func(key, value string) bool {
339 return key == "remove_me"
340 },
341 }
342
343 input := map[string]any{
344 "keep": "value1",
345 "remove_me": "value2",
346 "also_keep": "value3",
347 }
348
349 result := filterMap(input, []IgnorePattern{ignorePattern})
350
351 if _, exists := result["remove_me"]; exists {
352 t.Error("expected 'remove_me' to be filtered out")
353 }
354 if result["keep"] != "value1" {
355 t.Error("expected 'keep' to remain")
356 }
357 if result["also_keep"] != "value3" {
358 t.Error("expected 'also_keep' to remain")
359 }
360}
361
362func TestFilterMap_NestedStructures(t *testing.T) {
363 ignorePattern := &mockIgnorePattern{
364 fn: func(key, value string) bool {
365 return key == "secret"
366 },
367 }
368
369 input := map[string]any{
370 "public": "data",
371 "nested": map[string]any{
372 "secret": "hidden",
373 "public": "visible",
374 },
375 }
376
377 result := filterMap(input, []IgnorePattern{ignorePattern})
378
379 nested, ok := result["nested"].(map[string]any)
380 if !ok {
381 t.Fatal("expected nested map")
382 }
383
384 if _, exists := nested["secret"]; exists {
385 t.Error("expected nested 'secret' to be filtered out")
386 }
387 if nested["public"] != "visible" {
388 t.Error("expected nested 'public' to remain")
389 }
390}
391
392func TestFilterSlice_ProcessesAllElements(t *testing.T) {
393 ignorePattern := &mockIgnorePattern{
394 fn: func(key, value string) bool {
395 return key == "id"
396 },
397 }
398
399 input := []any{
400 map[string]any{"id": "1", "name": "Alice"},
401 map[string]any{"id": "2", "name": "Bob"},
402 }
403
404 result := filterSlice(input, []IgnorePattern{ignorePattern})
405
406 if len(result) != 2 {
407 t.Fatalf("expected 2 elements, got %d", len(result))
408 }
409
410 for i, item := range result {
411 m, ok := item.(map[string]any)
412 if !ok {
413 t.Fatalf("expected map at index %d", i)
414 }
415 if _, exists := m["id"]; exists {
416 t.Errorf("expected 'id' to be filtered at index %d", i)
417 }
418 if m["name"] == "" {
419 t.Errorf("expected 'name' to remain at index %d", i)
420 }
421 }
422}
423
424func TestWalkAndFilter_HandlesAllTypes(t *testing.T) {
425 ignorePattern := &mockIgnorePattern{
426 fn: func(key, value string) bool {
427 return false // Don't ignore anything
428 },
429 }
430
431 tests := []struct {
432 name string
433 input any
434 expected any
435 }{
436 {
437 name: "string passthrough",
438 input: "hello",
439 expected: "hello",
440 },
441 {
442 name: "number passthrough",
443 input: 42,
444 expected: 42,
445 },
446 {
447 name: "bool passthrough",
448 input: true,
449 expected: true,
450 },
451 {
452 name: "nil passthrough",
453 input: nil,
454 expected: nil,
455 },
456 }
457
458 for _, tt := range tests {
459 t.Run(tt.name, func(t *testing.T) {
460 result := walkAndFilter(tt.input, []IgnorePattern{ignorePattern})
461 if result != tt.expected {
462 t.Errorf("expected %v, got %v", tt.expected, result)
463 }
464 })
465 }
466}
467
468func TestTransformJSON_EmptyObject(t *testing.T) {
469 config := &Config{}
470 input := `{}`
471
472 result, err := TransformJSON(input, config)
473 if err != nil {
474 t.Fatalf("unexpected error: %v", err)
475 }
476
477 expected := "{}"
478 if result != expected {
479 t.Errorf("expected %q, got %q", expected, result)
480 }
481}
482
483func TestTransformJSON_EmptyArray(t *testing.T) {
484 config := &Config{}
485 input := `[]`
486
487 result, err := TransformJSON(input, config)
488 if err != nil {
489 t.Fatalf("unexpected error: %v", err)
490 }
491
492 expected := "[]"
493 if result != expected {
494 t.Errorf("expected %q, got %q", expected, result)
495 }
496}
497
498func TestTransformJSON_IgnoreByValue(t *testing.T) {
499 ignorePattern := &mockIgnorePattern{
500 fn: func(key, value string) bool {
501 return value == "ignore_this"
502 },
503 }
504
505 config := &Config{
506 Ignore: []IgnorePattern{ignorePattern},
507 }
508
509 input := `{"field1":"keep","field2":"ignore_this","field3":"also_keep"}`
510 result, err := TransformJSON(input, config)
511 if err != nil {
512 t.Fatalf("unexpected error: %v", err)
513 }
514
515 if strings.Contains(result, "field2") {
516 t.Errorf("expected field with ignored value to be removed, got: %s", result)
517 }
518 if !strings.Contains(result, "field1") || !strings.Contains(result, "field3") {
519 t.Errorf("expected other fields to remain, got: %s", result)
520 }
521}