blob: cc14416aefbbb0b3d72f20f038ac0995abf94e27 [file] [log] [blame]
Akron57ee5582025-05-21 15:25:13 +02001package config
2
3import (
Akron7e8da932025-07-01 11:56:46 +02004 "bytes"
Akron57ee5582025-05-21 15:25:13 +02005 "os"
6 "testing"
7
Akron2ef703c2025-07-03 15:57:42 +02008 "github.com/KorAP/Koral-Mapper/ast"
Akron7e8da932025-07-01 11:56:46 +02009 "github.com/rs/zerolog/log"
Akron57ee5582025-05-21 15:25:13 +020010 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12)
13
14func TestLoadConfig(t *testing.T) {
15 // Create a temporary YAML file
16 content := `
17- id: opennlp-mapper
18 foundryA: opennlp
19 layerA: p
20 foundryB: upos
21 layerB: p
22 mappings:
23 - "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]"
24 - "[PAV] <> [ADV & PronType:Dem]"
25
26- id: simple-mapper
27 mappings:
28 - "[A] <> [B]"
29`
30 tmpfile, err := os.CreateTemp("", "config-*.yaml")
31 require.NoError(t, err)
32 defer os.Remove(tmpfile.Name())
33
34 _, err = tmpfile.WriteString(content)
35 require.NoError(t, err)
36 err = tmpfile.Close()
37 require.NoError(t, err)
38
39 // Test loading the configuration
Akron585f50f2025-07-03 13:55:47 +020040 config, err := LoadFromSources(tmpfile.Name(), nil)
Akron57ee5582025-05-21 15:25:13 +020041 require.NoError(t, err)
42
43 // Verify the configuration
44 require.Len(t, config.Lists, 2)
45
46 // Check first mapping list
47 list1 := config.Lists[0]
48 assert.Equal(t, "opennlp-mapper", list1.ID)
49 assert.Equal(t, "opennlp", list1.FoundryA)
50 assert.Equal(t, "p", list1.LayerA)
51 assert.Equal(t, "upos", list1.FoundryB)
52 assert.Equal(t, "p", list1.LayerB)
53 require.Len(t, list1.Mappings, 2)
54 assert.Equal(t, "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]", string(list1.Mappings[0]))
55 assert.Equal(t, "[PAV] <> [ADV & PronType:Dem]", string(list1.Mappings[1]))
56
57 // Check second mapping list
58 list2 := config.Lists[1]
59 assert.Equal(t, "simple-mapper", list2.ID)
60 assert.Empty(t, list2.FoundryA)
61 assert.Empty(t, list2.LayerA)
62 assert.Empty(t, list2.FoundryB)
63 assert.Empty(t, list2.LayerB)
64 require.Len(t, list2.Mappings, 1)
65 assert.Equal(t, "[A] <> [B]", string(list2.Mappings[0]))
66}
67
68func TestParseMappings(t *testing.T) {
69 list := &MappingList{
70 ID: "test-mapper",
71 FoundryA: "opennlp",
72 LayerA: "p",
73 FoundryB: "upos",
74 LayerB: "p",
75 Mappings: []MappingRule{
76 "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]",
77 },
78 }
79
80 results, err := list.ParseMappings()
81 require.NoError(t, err)
82 require.Len(t, results, 1)
83
84 // Check the parsed upper pattern
85 upper := results[0].Upper
86 require.NotNil(t, upper)
87 require.IsType(t, &ast.Token{}, upper)
88 upperTerm := upper.Wrap.(*ast.Term)
89 assert.Equal(t, "opennlp", upperTerm.Foundry)
90 assert.Equal(t, "p", upperTerm.Layer)
91 assert.Equal(t, "PIDAT", upperTerm.Key)
92
93 // Check the parsed lower pattern
94 lower := results[0].Lower
95 require.NotNil(t, lower)
96 require.IsType(t, &ast.Token{}, lower)
97 lowerGroup := lower.Wrap.(*ast.TermGroup)
98 require.Len(t, lowerGroup.Operands, 2)
99 assert.Equal(t, ast.AndRelation, lowerGroup.Relation)
100
101 // Check first operand
102 term1 := lowerGroup.Operands[0].(*ast.Term)
103 assert.Equal(t, "opennlp", term1.Foundry)
104 assert.Equal(t, "p", term1.Layer)
105 assert.Equal(t, "PIDAT", term1.Key)
106
107 // Check second operand
108 term2 := lowerGroup.Operands[1].(*ast.Term)
109 assert.Equal(t, "opennlp", term2.Foundry)
110 assert.Equal(t, "p", term2.Layer)
111 assert.Equal(t, "AdjType", term2.Key)
112 assert.Equal(t, "Pdt", term2.Value)
113}
114
115func TestLoadConfigValidation(t *testing.T) {
116 tests := []struct {
117 name string
118 content string
119 wantErr string
120 }{
121 {
122 name: "Missing ID",
123 content: `
124- foundryA: opennlp
125 mappings:
126 - "[A] <> [B]"
127`,
128 wantErr: "mapping list at index 0 is missing an ID",
129 },
130 {
131 name: "Empty mappings",
132 content: `
133- id: test
134 foundryA: opennlp
135 mappings: []
136`,
137 wantErr: "mapping list 'test' has no mapping rules",
138 },
139 {
140 name: "Empty rule",
141 content: `
142- id: test
143 mappings:
144 - ""
145`,
146 wantErr: "mapping list 'test' rule at index 0 is empty",
147 },
148 }
149
150 for _, tt := range tests {
151 t.Run(tt.name, func(t *testing.T) {
152 tmpfile, err := os.CreateTemp("", "config-*.yaml")
153 require.NoError(t, err)
154 defer os.Remove(tmpfile.Name())
155
156 _, err = tmpfile.WriteString(tt.content)
157 require.NoError(t, err)
158 err = tmpfile.Close()
159 require.NoError(t, err)
160
Akron585f50f2025-07-03 13:55:47 +0200161 _, err = LoadFromSources(tmpfile.Name(), nil)
Akron57ee5582025-05-21 15:25:13 +0200162 require.Error(t, err)
163 assert.Contains(t, err.Error(), tt.wantErr)
164 })
165 }
166}
Akrona5d88142025-05-22 14:42:09 +0200167
168func TestLoadConfigEdgeCases(t *testing.T) {
169 tests := []struct {
170 name string
171 content string
172 wantErr string
173 }{
174 {
175 name: "Duplicate mapping list IDs",
176 content: `
177- id: test
178 mappings:
179 - "[A] <> [B]"
180- id: test
181 mappings:
182 - "[C] <> [D]"`,
183 wantErr: "duplicate mapping list ID found: test",
184 },
185 {
186 name: "Invalid YAML syntax",
187 content: `
188- id: test
189 mappings:
190 - [A] <> [B] # Unquoted special characters
191`,
192 wantErr: "yaml",
193 },
194 {
195 name: "Empty file",
196 content: "",
197 wantErr: "EOF",
198 },
199 {
200 name: "Non-list YAML",
201 content: `
202id: test
203mappings:
204 - "[A] <> [B]"`,
Akron813780f2025-06-05 15:44:28 +0200205 wantErr: "no mapping lists found",
Akrona5d88142025-05-22 14:42:09 +0200206 },
207 {
208 name: "Missing required fields",
209 content: `
210- mappings:
211 - "[A] <> [B]"
212- id: test2
213 foundryA: opennlp`,
214 wantErr: "missing an ID",
215 },
216 {
217 name: "Empty mappings list",
218 content: `
219- id: test
220 foundryA: opennlp
221 mappings: []`,
222 wantErr: "has no mapping rules",
223 },
224 {
225 name: "Null values in optional fields",
226 content: `
227- id: test
228 foundryA: null
229 layerA: null
230 foundryB: null
231 layerB: null
232 mappings:
233 - "[A] <> [B]"`,
234 wantErr: "",
235 },
236 {
237 name: "Special characters in IDs",
238 content: `
239- id: "test/special@chars#1"
240 mappings:
241 - "[A] <> [B]"`,
242 wantErr: "",
243 },
244 {
245 name: "Unicode characters in mappings",
246 content: `
247- id: test
248 mappings:
249 - "[ß] <> [ss]"
250 - "[é] <> [e]"`,
251 wantErr: "",
252 },
253 }
254
255 for _, tt := range tests {
256 t.Run(tt.name, func(t *testing.T) {
257 tmpfile, err := os.CreateTemp("", "config-*.yaml")
258 require.NoError(t, err)
259 defer os.Remove(tmpfile.Name())
260
261 _, err = tmpfile.WriteString(tt.content)
262 require.NoError(t, err)
263 err = tmpfile.Close()
264 require.NoError(t, err)
265
Akron585f50f2025-07-03 13:55:47 +0200266 config, err := LoadFromSources(tmpfile.Name(), nil)
Akrona5d88142025-05-22 14:42:09 +0200267 if tt.wantErr != "" {
268 require.Error(t, err)
269 assert.Contains(t, err.Error(), tt.wantErr)
270 return
271 }
272 require.NoError(t, err)
273 require.NotNil(t, config)
274 })
275 }
276}
277
278func TestParseMappingsEdgeCases(t *testing.T) {
279 tests := []struct {
280 name string
281 list *MappingList
282 wantErr bool
283 errCheck func(t *testing.T, err error)
284 }{
285 {
286 name: "Empty mapping rule",
287 list: &MappingList{
288 ID: "test",
289 Mappings: []MappingRule{""},
290 },
291 wantErr: true,
292 errCheck: func(t *testing.T, err error) {
293 assert.Contains(t, err.Error(), "empty")
294 },
295 },
296 {
297 name: "Invalid mapping syntax",
298 list: &MappingList{
299 ID: "test",
300 Mappings: []MappingRule{"[A] -> [B]"},
301 },
302 wantErr: true,
303 errCheck: func(t *testing.T, err error) {
304 assert.Contains(t, err.Error(), "failed to parse")
305 },
306 },
307 {
308 name: "Missing brackets",
309 list: &MappingList{
310 ID: "test",
311 Mappings: []MappingRule{"A <> B"},
312 },
313 wantErr: true,
314 errCheck: func(t *testing.T, err error) {
315 assert.Contains(t, err.Error(), "failed to parse")
316 },
317 },
318 {
319 name: "Complex nested expressions",
320 list: &MappingList{
321 ID: "test",
322 Mappings: []MappingRule{
323 "[A & (B | C) & (D | (E & F))] <> [X & (Y | Z)]",
324 },
325 },
326 wantErr: false,
327 },
328 {
329 name: "Multiple foundry/layer combinations",
330 list: &MappingList{
331 ID: "test",
332 Mappings: []MappingRule{
333 "[foundry1/layer1=A & foundry2/layer2=B] <> [foundry3/layer3=C]",
334 },
335 },
336 wantErr: false,
337 },
338 {
339 name: "Default foundry/layer override",
340 list: &MappingList{
341 ID: "test",
342 FoundryA: "defaultFoundry",
343 LayerA: "defaultLayer",
344 Mappings: []MappingRule{
345 "[A] <> [B]", // Should use defaults
346 },
347 },
348 wantErr: false,
349 },
350 }
351
352 for _, tt := range tests {
353 t.Run(tt.name, func(t *testing.T) {
354 results, err := tt.list.ParseMappings()
355 if tt.wantErr {
356 require.Error(t, err)
357 if tt.errCheck != nil {
358 tt.errCheck(t, err)
359 }
360 return
361 }
362 require.NoError(t, err)
363 require.NotNil(t, results)
364 })
365 }
366}
Akroncc25e932025-06-02 19:39:43 +0200367
368func TestUserProvidedMappingRules(t *testing.T) {
369 // Test the exact YAML mapping rules provided by the user
370 content := `
371- id: stts-ud
372 foundryA: opennlp
373 layerA: p
374 foundryB: upos
375 layerB: p
376 mappings:
377 - "[$\\(] <> [PUNCT & PunctType=Brck]"
378 - "[$,] <> [PUNCT & PunctType=Comm]"
379 - "[$.] <> [PUNCT & PunctType=Peri]"
380 - "[ADJA] <> [ADJ]"
381 - "[ADJD] <> [ADJ & Variant=Short]"
382 - "[ADV] <> [ADV]"
383`
384 tmpfile, err := os.CreateTemp("", "user-config-*.yaml")
385 require.NoError(t, err)
386 defer os.Remove(tmpfile.Name())
387
388 _, err = tmpfile.WriteString(content)
389 require.NoError(t, err)
390 err = tmpfile.Close()
391 require.NoError(t, err)
392
393 // Test loading the configuration
Akron585f50f2025-07-03 13:55:47 +0200394 config, err := LoadFromSources(tmpfile.Name(), nil)
Akroncc25e932025-06-02 19:39:43 +0200395 require.NoError(t, err)
396
397 // Verify the configuration loaded correctly
398 require.Len(t, config.Lists, 1)
399 list := config.Lists[0]
400 assert.Equal(t, "stts-ud", list.ID)
401 assert.Equal(t, "opennlp", list.FoundryA)
402 assert.Equal(t, "p", list.LayerA)
403 assert.Equal(t, "upos", list.FoundryB)
404 assert.Equal(t, "p", list.LayerB)
405 require.Len(t, list.Mappings, 6)
406
407 // First, test individual mappings to isolate the issue
408 t.Run("parenthesis mapping", func(t *testing.T) {
409 singleRule := &MappingList{
410 ID: "test-paren",
411 FoundryA: "opennlp",
412 LayerA: "p",
413 FoundryB: "upos",
414 LayerB: "p",
415 Mappings: []MappingRule{"[$\\(] <> [PUNCT & PunctType=Brck]"},
416 }
417 results, err := singleRule.ParseMappings()
418 require.NoError(t, err)
419 require.Len(t, results, 1)
420
421 upperTerm := results[0].Upper.Wrap.(*ast.Term)
422 assert.Equal(t, "$(", upperTerm.Key)
423 })
424
425 t.Run("comma mapping", func(t *testing.T) {
426 singleRule := &MappingList{
427 ID: "test-comma",
428 FoundryA: "opennlp",
429 LayerA: "p",
430 FoundryB: "upos",
431 LayerB: "p",
432 Mappings: []MappingRule{"[$,] <> [PUNCT & PunctType=Comm]"},
433 }
434 results, err := singleRule.ParseMappings()
435 require.NoError(t, err)
436 require.Len(t, results, 1)
437
438 upperTerm := results[0].Upper.Wrap.(*ast.Term)
439 assert.Equal(t, "$,", upperTerm.Key)
440 })
441
442 t.Run("period mapping", func(t *testing.T) {
443 singleRule := &MappingList{
444 ID: "test-period",
445 FoundryA: "opennlp",
446 LayerA: "p",
447 FoundryB: "upos",
448 LayerB: "p",
449 Mappings: []MappingRule{"[$.] <> [PUNCT & PunctType=Peri]"},
450 }
451 results, err := singleRule.ParseMappings()
452 require.NoError(t, err)
453 require.Len(t, results, 1)
454
455 upperTerm := results[0].Upper.Wrap.(*ast.Term)
456 assert.Equal(t, "$.", upperTerm.Key)
457 })
458
459 // Test that all mapping rules can be parsed successfully
460 results, err := list.ParseMappings()
461 require.NoError(t, err)
462 require.Len(t, results, 6)
463
464 // Verify specific parsing of the special character mapping
465 // The first mapping "[$\\(] <> [PUNCT & PunctType=Brck]" should parse correctly
466 firstMapping := results[0]
467 require.NotNil(t, firstMapping.Upper)
468 upperTerm := firstMapping.Upper.Wrap.(*ast.Term)
469 assert.Equal(t, "$(", upperTerm.Key) // The actual parsed key should be "$("
470 assert.Equal(t, "opennlp", upperTerm.Foundry)
471 assert.Equal(t, "p", upperTerm.Layer)
472
473 require.NotNil(t, firstMapping.Lower)
474 lowerGroup := firstMapping.Lower.Wrap.(*ast.TermGroup)
475 require.Len(t, lowerGroup.Operands, 2)
476 assert.Equal(t, ast.AndRelation, lowerGroup.Relation)
477
478 // Check the PUNCT term
479 punctTerm := lowerGroup.Operands[0].(*ast.Term)
480 assert.Equal(t, "PUNCT", punctTerm.Key)
481 assert.Equal(t, "upos", punctTerm.Foundry)
482 assert.Equal(t, "p", punctTerm.Layer)
483
484 // Check the PunctType term
485 punctTypeTerm := lowerGroup.Operands[1].(*ast.Term)
486 assert.Equal(t, "PunctType", punctTypeTerm.Layer)
487 assert.Equal(t, "Brck", punctTypeTerm.Key)
488 assert.Equal(t, "upos", punctTypeTerm.Foundry)
489
490 // Verify the comma mapping as well
491 secondMapping := results[1]
492 upperTerm2 := secondMapping.Upper.Wrap.(*ast.Term)
493 assert.Equal(t, "$,", upperTerm2.Key)
494
495 // Verify the period mapping
496 thirdMapping := results[2]
497 upperTerm3 := thirdMapping.Upper.Wrap.(*ast.Term)
498 assert.Equal(t, "$.", upperTerm3.Key)
499
500 // Verify basic mappings without special characters
501 fourthMapping := results[3]
502 upperTerm4 := fourthMapping.Upper.Wrap.(*ast.Term)
503 assert.Equal(t, "ADJA", upperTerm4.Key)
504 lowerTerm4 := fourthMapping.Lower.Wrap.(*ast.Term)
505 assert.Equal(t, "ADJ", lowerTerm4.Key)
506}
507
Akron06d21f02025-06-04 14:36:07 +0200508func TestConfigWithSdkAndServer(t *testing.T) {
509 tests := []struct {
510 name string
511 content string
512 expectedSDK string
513 expectedServer string
514 wantErr bool
515 }{
516 {
517 name: "Configuration with SDK and Server values",
518 content: `
519sdk: "https://custom.example.com/sdk.js"
520server: "https://custom.example.com/"
521lists:
522- id: test-mapper
523 foundryA: opennlp
524 layerA: p
525 foundryB: upos
526 layerB: p
527 mappings:
528 - "[A] <> [B]"
529`,
530 expectedSDK: "https://custom.example.com/sdk.js",
531 expectedServer: "https://custom.example.com/",
532 wantErr: false,
533 },
534 {
535 name: "Configuration with only SDK value",
536 content: `
537sdk: "https://custom.example.com/sdk.js"
538lists:
539- id: test-mapper
540 mappings:
541 - "[A] <> [B]"
542`,
543 expectedSDK: "https://custom.example.com/sdk.js",
544 expectedServer: "https://korap.ids-mannheim.de/", // default applied
545 wantErr: false,
546 },
547 {
548 name: "Configuration with only Server value",
549 content: `
550server: "https://custom.example.com/"
551lists:
552- id: test-mapper
553 mappings:
554 - "[A] <> [B]"
555`,
556 expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // default applied
557 expectedServer: "https://custom.example.com/",
558 wantErr: false,
559 },
560 {
561 name: "Configuration without SDK and Server (old format with defaults applied)",
562 content: `
563- id: test-mapper
564 mappings:
565 - "[A] <> [B]"
566`,
567 expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // default applied
568 expectedServer: "https://korap.ids-mannheim.de/", // default applied
569 wantErr: false,
570 },
571 {
572 name: "Configuration with lists field explicitly",
573 content: `
574sdk: "https://custom.example.com/sdk.js"
575server: "https://custom.example.com/"
576lists:
577- id: test-mapper-1
578 mappings:
579 - "[A] <> [B]"
580- id: test-mapper-2
581 mappings:
582 - "[C] <> [D]"
583`,
584 expectedSDK: "https://custom.example.com/sdk.js",
585 expectedServer: "https://custom.example.com/",
586 wantErr: false,
587 },
588 }
589
590 for _, tt := range tests {
591 t.Run(tt.name, func(t *testing.T) {
592 tmpfile, err := os.CreateTemp("", "config-*.yaml")
593 require.NoError(t, err)
594 defer os.Remove(tmpfile.Name())
595
596 _, err = tmpfile.WriteString(tt.content)
597 require.NoError(t, err)
598 err = tmpfile.Close()
599 require.NoError(t, err)
600
Akron585f50f2025-07-03 13:55:47 +0200601 config, err := LoadFromSources(tmpfile.Name(), nil)
Akron06d21f02025-06-04 14:36:07 +0200602 if tt.wantErr {
603 require.Error(t, err)
604 return
605 }
606
607 require.NoError(t, err)
608 require.NotNil(t, config)
609
610 // Check SDK and Server values
611 assert.Equal(t, tt.expectedSDK, config.SDK)
612 assert.Equal(t, tt.expectedServer, config.Server)
613
614 // Ensure lists are still loaded correctly
615 require.Greater(t, len(config.Lists), 0)
616
617 // Verify first mapping list
618 firstList := config.Lists[0]
619 assert.NotEmpty(t, firstList.ID)
620 assert.Greater(t, len(firstList.Mappings), 0)
621 })
622 }
623}
Akrone1cff7c2025-06-04 18:43:32 +0200624
625func TestLoadFromSources(t *testing.T) {
626 // Create main config file
627 mainConfigContent := `
628sdk: "https://custom.example.com/sdk.js"
629server: "https://custom.example.com/"
630lists:
631- id: main-mapper
632 mappings:
633 - "[A] <> [B]"
634`
635 mainConfigFile, err := os.CreateTemp("", "main-config-*.yaml")
636 require.NoError(t, err)
637 defer os.Remove(mainConfigFile.Name())
638
639 _, err = mainConfigFile.WriteString(mainConfigContent)
640 require.NoError(t, err)
641 err = mainConfigFile.Close()
642 require.NoError(t, err)
643
644 // Create individual mapping files
645 mappingFile1Content := `
646id: mapper-1
647foundryA: opennlp
648layerA: p
649mappings:
650 - "[C] <> [D]"
651`
652 mappingFile1, err := os.CreateTemp("", "mapping1-*.yaml")
653 require.NoError(t, err)
654 defer os.Remove(mappingFile1.Name())
655
656 _, err = mappingFile1.WriteString(mappingFile1Content)
657 require.NoError(t, err)
658 err = mappingFile1.Close()
659 require.NoError(t, err)
660
661 mappingFile2Content := `
662id: mapper-2
663foundryB: upos
664layerB: p
665mappings:
666 - "[E] <> [F]"
667`
668 mappingFile2, err := os.CreateTemp("", "mapping2-*.yaml")
669 require.NoError(t, err)
670 defer os.Remove(mappingFile2.Name())
671
672 _, err = mappingFile2.WriteString(mappingFile2Content)
673 require.NoError(t, err)
674 err = mappingFile2.Close()
675 require.NoError(t, err)
676
677 tests := []struct {
678 name string
679 configFile string
680 mappingFiles []string
681 wantErr bool
682 expectedIDs []string
683 }{
684 {
685 name: "Main config only",
686 configFile: mainConfigFile.Name(),
687 mappingFiles: []string{},
688 wantErr: false,
689 expectedIDs: []string{"main-mapper"},
690 },
691 {
692 name: "Mapping files only",
693 configFile: "",
694 mappingFiles: []string{mappingFile1.Name(), mappingFile2.Name()},
695 wantErr: false,
696 expectedIDs: []string{"mapper-1", "mapper-2"},
697 },
698 {
699 name: "Main config and mapping files",
700 configFile: mainConfigFile.Name(),
701 mappingFiles: []string{mappingFile1.Name(), mappingFile2.Name()},
702 wantErr: false,
703 expectedIDs: []string{"main-mapper", "mapper-1", "mapper-2"},
704 },
705 {
706 name: "No configuration sources",
707 configFile: "",
708 mappingFiles: []string{},
709 wantErr: true,
710 },
711 }
712
713 for _, tt := range tests {
714 t.Run(tt.name, func(t *testing.T) {
715 config, err := LoadFromSources(tt.configFile, tt.mappingFiles)
716 if tt.wantErr {
717 require.Error(t, err)
718 return
719 }
720
721 require.NoError(t, err)
722 require.NotNil(t, config)
723
724 // Check that all expected mapping IDs are present
725 require.Len(t, config.Lists, len(tt.expectedIDs))
726 actualIDs := make([]string, len(config.Lists))
727 for i, list := range config.Lists {
728 actualIDs[i] = list.ID
729 }
730 for _, expectedID := range tt.expectedIDs {
731 assert.Contains(t, actualIDs, expectedID)
732 }
733
734 // Check that SDK and Server are set (either from config or defaults)
735 assert.NotEmpty(t, config.SDK)
736 assert.NotEmpty(t, config.Server)
737 })
738 }
739}
740
741func TestLoadFromSourcesWithDefaults(t *testing.T) {
742 // Test that defaults are applied when loading only mapping files
743 mappingFileContent := `
744id: test-mapper
745mappings:
746 - "[A] <> [B]"
747`
748 mappingFile, err := os.CreateTemp("", "mapping-*.yaml")
749 require.NoError(t, err)
750 defer os.Remove(mappingFile.Name())
751
752 _, err = mappingFile.WriteString(mappingFileContent)
753 require.NoError(t, err)
754 err = mappingFile.Close()
755 require.NoError(t, err)
756
757 config, err := LoadFromSources("", []string{mappingFile.Name()})
758 require.NoError(t, err)
759
760 // Check that defaults are applied
761 assert.Equal(t, defaultSDK, config.SDK)
762 assert.Equal(t, defaultServer, config.Server)
763 require.Len(t, config.Lists, 1)
764 assert.Equal(t, "test-mapper", config.Lists[0].ID)
765}
766
767func TestLoadFromSourcesDuplicateIDs(t *testing.T) {
Akron7e8da932025-07-01 11:56:46 +0200768 // Set up a buffer to capture log output
769 var buf bytes.Buffer
770 originalLogger := log.Logger
771 defer func() {
772 log.Logger = originalLogger
773 }()
774 log.Logger = log.Logger.Output(&buf)
775
Akrone1cff7c2025-06-04 18:43:32 +0200776 // Create config with duplicate IDs across sources
777 configContent := `
778lists:
779- id: duplicate-id
780 mappings:
781 - "[A] <> [B]"
782`
783 configFile, err := os.CreateTemp("", "config-*.yaml")
784 require.NoError(t, err)
785 defer os.Remove(configFile.Name())
786
787 _, err = configFile.WriteString(configContent)
788 require.NoError(t, err)
789 err = configFile.Close()
790 require.NoError(t, err)
791
792 mappingContent := `
793id: duplicate-id
794mappings:
795 - "[C] <> [D]"
796`
797 mappingFile, err := os.CreateTemp("", "mapping-*.yaml")
798 require.NoError(t, err)
799 defer os.Remove(mappingFile.Name())
800
801 _, err = mappingFile.WriteString(mappingContent)
802 require.NoError(t, err)
803 err = mappingFile.Close()
804 require.NoError(t, err)
805
Akron7e8da932025-07-01 11:56:46 +0200806 // The function should now succeed but log the duplicate ID error
807 config, err := LoadFromSources(configFile.Name(), []string{mappingFile.Name()})
808 require.NoError(t, err)
809 require.NotNil(t, config)
810
811 // Check that the duplicate ID error was logged
812 logOutput := buf.String()
813 assert.Contains(t, logOutput, "Duplicate mapping list ID found")
814 assert.Contains(t, logOutput, "duplicate-id")
815
816 // Only the first mapping list (from config file) should be loaded
817 require.Len(t, config.Lists, 1)
818 assert.Equal(t, "duplicate-id", config.Lists[0].ID)
819 // Check that it's the one from the config file (has mapping "[A] <> [B]")
820 assert.Equal(t, "[A] <> [B]", string(config.Lists[0].Mappings[0]))
Akrone1cff7c2025-06-04 18:43:32 +0200821}
Akron813780f2025-06-05 15:44:28 +0200822
823func TestLoadFromSourcesConfigWithOnlyPort(t *testing.T) {
824 // Create config file with only port (no lists)
825 configContent := `
826port: 8080
827loglevel: debug
828`
829 configFile, err := os.CreateTemp("", "config-*.yaml")
830 require.NoError(t, err)
831 defer os.Remove(configFile.Name())
832
833 _, err = configFile.WriteString(configContent)
834 require.NoError(t, err)
835 err = configFile.Close()
836 require.NoError(t, err)
837
838 // Create mapping file
839 mappingContent := `
840id: test-mapper
841mappings:
842 - "[A] <> [B]"
843`
844 mappingFile, err := os.CreateTemp("", "mapping-*.yaml")
845 require.NoError(t, err)
846 defer os.Remove(mappingFile.Name())
847
848 _, err = mappingFile.WriteString(mappingContent)
849 require.NoError(t, err)
850 err = mappingFile.Close()
851 require.NoError(t, err)
852
853 // This should work: config file has only port, mapping file provides the mapping list
854 config, err := LoadFromSources(configFile.Name(), []string{mappingFile.Name()})
855 require.NoError(t, err)
856 require.NotNil(t, config)
857
858 // Check that port and log level from config file are preserved
859 assert.Equal(t, 8080, config.Port)
860 assert.Equal(t, "debug", config.LogLevel)
861
862 // Check that mapping from mapping file is loaded
863 require.Len(t, config.Lists, 1)
864 assert.Equal(t, "test-mapper", config.Lists[0].ID)
865
866 // Check that defaults are applied for other fields
867 assert.Equal(t, defaultSDK, config.SDK)
868 assert.Equal(t, defaultServer, config.Server)
869 assert.Equal(t, defaultServiceURL, config.ServiceURL)
870}