Approval-based snapshot testing library for Go (mirror)
at main 521 lines 12 kB view raw
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}