blob: df930a3e9af407e0ea7345aaddf521057e9bf234 [file] [log] [blame]
Akron49ceeb42025-05-23 17:46:01 +02001package main
2
3import (
4 "bytes"
5 "encoding/json"
6 "io"
7 "net/http"
8 "net/http/httptest"
Akrone1cff7c2025-06-04 18:43:32 +02009 "os"
Akron14678dc2025-06-05 13:01:38 +020010 "path/filepath"
11 "sort"
Akron49ceeb42025-05-23 17:46:01 +020012 "testing"
13
Akrona00d4752025-05-26 17:34:36 +020014 tmconfig "github.com/KorAP/KoralPipe-TermMapper/config"
Akronfa55bb22025-05-26 15:10:42 +020015 "github.com/KorAP/KoralPipe-TermMapper/mapper"
Akron49ceeb42025-05-23 17:46:01 +020016 "github.com/gofiber/fiber/v2"
17 "github.com/stretchr/testify/assert"
18 "github.com/stretchr/testify/require"
19)
20
21func TestTransformEndpoint(t *testing.T) {
Akrona00d4752025-05-26 17:34:36 +020022 // Create test mapping list
23 mappingList := tmconfig.MappingList{
24 ID: "test-mapper",
25 FoundryA: "opennlp",
26 LayerA: "p",
27 FoundryB: "upos",
28 LayerB: "p",
29 Mappings: []tmconfig.MappingRule{
30 "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]",
31 "[DET] <> [opennlp/p=DET]",
32 },
33 }
Akron49ceeb42025-05-23 17:46:01 +020034
35 // Create mapper
Akrona00d4752025-05-26 17:34:36 +020036 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
Akron49ceeb42025-05-23 17:46:01 +020037 require.NoError(t, err)
38
Akron40aaa632025-06-03 17:57:52 +020039 // Create mock config for testing
Akron06d21f02025-06-04 14:36:07 +020040 mockConfig := &tmconfig.MappingConfig{
Akron40aaa632025-06-03 17:57:52 +020041 Lists: []tmconfig.MappingList{mappingList},
42 }
43
Akron49ceeb42025-05-23 17:46:01 +020044 // Create fiber app
45 app := fiber.New()
Akron40aaa632025-06-03 17:57:52 +020046 setupRoutes(app, m, mockConfig)
Akron49ceeb42025-05-23 17:46:01 +020047
48 tests := []struct {
49 name string
50 mapID string
51 direction string
52 foundryA string
53 foundryB string
54 layerA string
55 layerB string
56 input string
57 expectedCode int
58 expectedBody string
59 expectedError string
60 }{
61 {
62 name: "Simple A to B mapping",
63 mapID: "test-mapper",
64 direction: "atob",
65 input: `{
66 "@type": "koral:token",
67 "wrap": {
68 "@type": "koral:term",
69 "foundry": "opennlp",
70 "key": "PIDAT",
71 "layer": "p",
72 "match": "match:eq"
73 }
74 }`,
75 expectedCode: http.StatusOK,
76 expectedBody: `{
77 "@type": "koral:token",
78 "wrap": {
79 "@type": "koral:termGroup",
80 "operands": [
81 {
82 "@type": "koral:term",
83 "foundry": "opennlp",
84 "key": "PIDAT",
85 "layer": "p",
86 "match": "match:eq"
87 },
88 {
89 "@type": "koral:term",
90 "foundry": "opennlp",
91 "key": "AdjType",
92 "layer": "p",
93 "match": "match:eq",
94 "value": "Pdt"
95 }
96 ],
97 "relation": "relation:and"
98 }
99 }`,
100 },
101 {
102 name: "B to A mapping",
103 mapID: "test-mapper",
104 direction: "btoa",
105 input: `{
106 "@type": "koral:token",
107 "wrap": {
108 "@type": "koral:termGroup",
109 "operands": [
110 {
111 "@type": "koral:term",
112 "foundry": "opennlp",
113 "key": "PIDAT",
114 "layer": "p",
115 "match": "match:eq"
116 },
117 {
118 "@type": "koral:term",
119 "foundry": "opennlp",
120 "key": "AdjType",
121 "layer": "p",
122 "match": "match:eq",
123 "value": "Pdt"
124 }
125 ],
126 "relation": "relation:and"
127 }
128 }`,
129 expectedCode: http.StatusOK,
130 expectedBody: `{
131 "@type": "koral:token",
132 "wrap": {
133 "@type": "koral:term",
134 "foundry": "opennlp",
135 "key": "PIDAT",
136 "layer": "p",
137 "match": "match:eq"
138 }
139 }`,
140 },
141 {
142 name: "Mapping with foundry override",
143 mapID: "test-mapper",
144 direction: "atob",
145 foundryB: "custom",
146 input: `{
147 "@type": "koral:token",
148 "wrap": {
149 "@type": "koral:term",
150 "foundry": "opennlp",
151 "key": "PIDAT",
152 "layer": "p",
153 "match": "match:eq"
154 }
155 }`,
156 expectedCode: http.StatusOK,
157 expectedBody: `{
158 "@type": "koral:token",
159 "wrap": {
160 "@type": "koral:termGroup",
161 "operands": [
162 {
163 "@type": "koral:term",
164 "foundry": "custom",
165 "key": "PIDAT",
166 "layer": "p",
167 "match": "match:eq"
168 },
169 {
170 "@type": "koral:term",
171 "foundry": "custom",
172 "key": "AdjType",
173 "layer": "p",
174 "match": "match:eq",
175 "value": "Pdt"
176 }
177 ],
178 "relation": "relation:and"
179 }
180 }`,
181 },
182 {
183 name: "Invalid mapping ID",
184 mapID: "nonexistent",
185 direction: "atob",
186 input: `{"@type": "koral:token"}`,
187 expectedCode: http.StatusInternalServerError,
188 expectedError: "mapping list with ID nonexistent not found",
189 },
190 {
191 name: "Invalid direction",
192 mapID: "test-mapper",
193 direction: "invalid",
194 input: `{"@type": "koral:token"}`,
195 expectedCode: http.StatusBadRequest,
196 expectedError: "invalid direction, must be 'atob' or 'btoa'",
197 },
198 {
199 name: "Invalid JSON",
200 mapID: "test-mapper",
201 direction: "atob",
202 input: `invalid json`,
203 expectedCode: http.StatusBadRequest,
204 expectedError: "invalid JSON in request body",
205 },
206 }
207
208 for _, tt := range tests {
209 t.Run(tt.name, func(t *testing.T) {
210 // Build URL with query parameters
211 url := "/" + tt.mapID + "/query"
212 if tt.direction != "" {
213 url += "?dir=" + tt.direction
214 }
215 if tt.foundryA != "" {
216 url += "&foundryA=" + tt.foundryA
217 }
218 if tt.foundryB != "" {
219 url += "&foundryB=" + tt.foundryB
220 }
221 if tt.layerA != "" {
222 url += "&layerA=" + tt.layerA
223 }
224 if tt.layerB != "" {
225 url += "&layerB=" + tt.layerB
226 }
227
228 // Make request
229 req := httptest.NewRequest(http.MethodPost, url, bytes.NewBufferString(tt.input))
230 req.Header.Set("Content-Type", "application/json")
231 resp, err := app.Test(req)
232 require.NoError(t, err)
233 defer resp.Body.Close()
234
235 // Check status code
236 assert.Equal(t, tt.expectedCode, resp.StatusCode)
237
238 // Read response body
239 body, err := io.ReadAll(resp.Body)
240 require.NoError(t, err)
241
242 if tt.expectedError != "" {
243 // Check error message
244 var errResp fiber.Map
245 err = json.Unmarshal(body, &errResp)
246 require.NoError(t, err)
247 assert.Equal(t, tt.expectedError, errResp["error"])
248 } else {
249 // Compare JSON responses
Akron121c66e2025-06-02 16:34:05 +0200250 var expected, actual any
Akron49ceeb42025-05-23 17:46:01 +0200251 err = json.Unmarshal([]byte(tt.expectedBody), &expected)
252 require.NoError(t, err)
253 err = json.Unmarshal(body, &actual)
254 require.NoError(t, err)
255 assert.Equal(t, expected, actual)
256 }
257 })
258 }
259}
260
261func TestHealthEndpoint(t *testing.T) {
Akrona00d4752025-05-26 17:34:36 +0200262 // Create test mapping list
263 mappingList := tmconfig.MappingList{
264 ID: "test-mapper",
265 Mappings: []tmconfig.MappingRule{
266 "[A] <> [B]",
267 },
268 }
Akron49ceeb42025-05-23 17:46:01 +0200269
Akrona00d4752025-05-26 17:34:36 +0200270 // Create mapper
271 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
Akron49ceeb42025-05-23 17:46:01 +0200272 require.NoError(t, err)
273
Akron40aaa632025-06-03 17:57:52 +0200274 // Create mock config for testing
Akron06d21f02025-06-04 14:36:07 +0200275 mockConfig := &tmconfig.MappingConfig{
Akron40aaa632025-06-03 17:57:52 +0200276 Lists: []tmconfig.MappingList{mappingList},
277 }
278
Akron49ceeb42025-05-23 17:46:01 +0200279 // Create fiber app
280 app := fiber.New()
Akron40aaa632025-06-03 17:57:52 +0200281 setupRoutes(app, m, mockConfig)
Akron49ceeb42025-05-23 17:46:01 +0200282
283 // Test health endpoint
284 req := httptest.NewRequest(http.MethodGet, "/health", nil)
285 resp, err := app.Test(req)
286 require.NoError(t, err)
287 defer resp.Body.Close()
288
289 assert.Equal(t, http.StatusOK, resp.StatusCode)
290 body, err := io.ReadAll(resp.Body)
291 require.NoError(t, err)
292 assert.Equal(t, "OK", string(body))
Akron40aaa632025-06-03 17:57:52 +0200293
Akronc471c0a2025-06-04 11:56:22 +0200294 req = httptest.NewRequest(http.MethodGet, "/", nil)
Akron40aaa632025-06-03 17:57:52 +0200295 resp, err = app.Test(req)
296 require.NoError(t, err)
297 defer resp.Body.Close()
298
299 assert.Equal(t, http.StatusOK, resp.StatusCode)
300 body, err = io.ReadAll(resp.Body)
301 require.NoError(t, err)
Akronfc77b5e2025-06-04 11:44:43 +0200302 assert.Contains(t, string(body), "KoralPipe-TermMapper")
Akron40aaa632025-06-03 17:57:52 +0200303
Akron49ceeb42025-05-23 17:46:01 +0200304}
Akron06d21f02025-06-04 14:36:07 +0200305
306func TestKalamarPluginWithCustomSdkAndServer(t *testing.T) {
307 // Create test mapping list
308 mappingList := tmconfig.MappingList{
309 ID: "test-mapper",
310 Mappings: []tmconfig.MappingRule{
311 "[A] <> [B]",
312 },
313 }
314
315 // Create mapper
316 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
317 require.NoError(t, err)
318
319 tests := []struct {
320 name string
321 customSDK string
322 customServer string
323 expectedSDK string
324 expectedServer string
325 }{
326 {
327 name: "Custom SDK and Server values",
328 customSDK: "https://custom.example.com/custom-sdk.js",
329 customServer: "https://custom.example.com/",
330 expectedSDK: "https://custom.example.com/custom-sdk.js",
331 expectedServer: "https://custom.example.com/",
332 },
333 {
334 name: "Only custom SDK value",
335 customSDK: "https://custom.example.com/custom-sdk.js",
336 customServer: "https://korap.ids-mannheim.de/", // defaults applied during parsing
337 expectedSDK: "https://custom.example.com/custom-sdk.js",
338 expectedServer: "https://korap.ids-mannheim.de/",
339 },
340 {
341 name: "Only custom Server value",
342 customSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // defaults applied during parsing
343 customServer: "https://custom.example.com/",
344 expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js",
345 expectedServer: "https://custom.example.com/",
346 },
347 {
348 name: "Defaults applied during parsing",
349 customSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // defaults applied during parsing
350 customServer: "https://korap.ids-mannheim.de/", // defaults applied during parsing
351 expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js",
352 expectedServer: "https://korap.ids-mannheim.de/",
353 },
354 }
355
356 for _, tt := range tests {
357 t.Run(tt.name, func(t *testing.T) {
358 // Create mock config with custom values
359 mockConfig := &tmconfig.MappingConfig{
360 SDK: tt.customSDK,
361 Server: tt.customServer,
362 Lists: []tmconfig.MappingList{mappingList},
363 }
364
365 // Create fiber app
366 app := fiber.New()
367 setupRoutes(app, m, mockConfig)
368
369 // Test Kalamar plugin endpoint
370 req := httptest.NewRequest(http.MethodGet, "/", nil)
371 resp, err := app.Test(req)
372 require.NoError(t, err)
373 defer resp.Body.Close()
374
375 assert.Equal(t, http.StatusOK, resp.StatusCode)
376 body, err := io.ReadAll(resp.Body)
377 require.NoError(t, err)
378
379 htmlContent := string(body)
380
381 // Check that the HTML contains the expected SDK and Server values
382 assert.Contains(t, htmlContent, `src="`+tt.expectedSDK+`"`)
383 assert.Contains(t, htmlContent, `data-server="`+tt.expectedServer+`"`)
384
385 // Ensure it's still a valid HTML page
386 assert.Contains(t, htmlContent, "KoralPipe-TermMapper")
387 assert.Contains(t, htmlContent, "<!DOCTYPE html>")
388 })
389 }
390}
Akrone1cff7c2025-06-04 18:43:32 +0200391
392func TestMultipleMappingFiles(t *testing.T) {
393 // Create test mapping files
394 mappingFile1Content := `
395id: test-mapper-1
396foundryA: opennlp
397layerA: p
398foundryB: upos
399layerB: p
400mappings:
401 - "[PIDAT] <> [DET & AdjType=Pdt]"
402 - "[PAV] <> [ADV & PronType=Dem]"
403`
404 mappingFile1, err := os.CreateTemp("", "mapping1-*.yaml")
405 require.NoError(t, err)
406 defer os.Remove(mappingFile1.Name())
407
408 _, err = mappingFile1.WriteString(mappingFile1Content)
409 require.NoError(t, err)
410 err = mappingFile1.Close()
411 require.NoError(t, err)
412
413 mappingFile2Content := `
414id: test-mapper-2
415foundryA: stts
416layerA: p
417foundryB: upos
418layerB: p
419mappings:
420 - "[DET] <> [PRON]"
421 - "[ADJ] <> [NOUN]"
422`
423 mappingFile2, err := os.CreateTemp("", "mapping2-*.yaml")
424 require.NoError(t, err)
425 defer os.Remove(mappingFile2.Name())
426
427 _, err = mappingFile2.WriteString(mappingFile2Content)
428 require.NoError(t, err)
429 err = mappingFile2.Close()
430 require.NoError(t, err)
431
432 // Load configuration using multiple mapping files
433 config, err := tmconfig.LoadFromSources("", []string{mappingFile1.Name(), mappingFile2.Name()})
434 require.NoError(t, err)
435
436 // Create mapper
437 m, err := mapper.NewMapper(config.Lists)
438 require.NoError(t, err)
439
440 // Create fiber app
441 app := fiber.New()
442 setupRoutes(app, m, config)
443
444 // Test that both mappers work
445 testCases := []struct {
446 name string
447 mapID string
448 input string
449 expectGroup bool
450 expectedKey string
451 }{
452 {
453 name: "test-mapper-1 with complex mapping",
454 mapID: "test-mapper-1",
455 input: `{
456 "@type": "koral:token",
457 "wrap": {
458 "@type": "koral:term",
459 "foundry": "opennlp",
460 "key": "PIDAT",
461 "layer": "p",
462 "match": "match:eq"
463 }
464 }`,
465 expectGroup: true, // This mapping creates a termGroup because of "&"
466 expectedKey: "DET", // The first operand should be DET
467 },
468 {
469 name: "test-mapper-2 with simple mapping",
470 mapID: "test-mapper-2",
471 input: `{
472 "@type": "koral:token",
473 "wrap": {
474 "@type": "koral:term",
475 "foundry": "stts",
476 "key": "DET",
477 "layer": "p",
478 "match": "match:eq"
479 }
480 }`,
481 expectGroup: false, // This mapping creates a simple term
482 expectedKey: "PRON",
483 },
484 }
485
486 for _, tc := range testCases {
487 t.Run(tc.name, func(t *testing.T) {
488 req := httptest.NewRequest(http.MethodPost, "/"+tc.mapID+"/query?dir=atob", bytes.NewBufferString(tc.input))
489 req.Header.Set("Content-Type", "application/json")
490
491 resp, err := app.Test(req)
492 require.NoError(t, err)
493 defer resp.Body.Close()
494
495 assert.Equal(t, http.StatusOK, resp.StatusCode)
496
497 var result map[string]interface{}
498 err = json.NewDecoder(resp.Body).Decode(&result)
499 require.NoError(t, err)
500
501 // Check that the mapping was applied
502 wrap := result["wrap"].(map[string]interface{})
503 if tc.expectGroup {
504 // For complex mappings, check the first operand
505 assert.Equal(t, "koral:termGroup", wrap["@type"])
506 operands := wrap["operands"].([]interface{})
507 require.Greater(t, len(operands), 0)
508 firstOperand := operands[0].(map[string]interface{})
509 assert.Equal(t, tc.expectedKey, firstOperand["key"])
510 } else {
511 // For simple mappings, check the key directly
512 assert.Equal(t, "koral:term", wrap["@type"])
513 assert.Equal(t, tc.expectedKey, wrap["key"])
514 }
515 })
516 }
517}
518
519func TestCombinedConfigAndMappingFiles(t *testing.T) {
520 // Create main config file
521 mainConfigContent := `
522sdk: "https://custom.example.com/sdk.js"
523server: "https://custom.example.com/"
524lists:
525- id: main-mapper
526 foundryA: opennlp
527 layerA: p
528 mappings:
529 - "[A] <> [B]"
530`
531 mainConfigFile, err := os.CreateTemp("", "main-config-*.yaml")
532 require.NoError(t, err)
533 defer os.Remove(mainConfigFile.Name())
534
535 _, err = mainConfigFile.WriteString(mainConfigContent)
536 require.NoError(t, err)
537 err = mainConfigFile.Close()
538 require.NoError(t, err)
539
540 // Create individual mapping file
541 mappingFileContent := `
542id: additional-mapper
543foundryA: stts
544layerA: p
545mappings:
546 - "[C] <> [D]"
547`
548 mappingFile, err := os.CreateTemp("", "mapping-*.yaml")
549 require.NoError(t, err)
550 defer os.Remove(mappingFile.Name())
551
552 _, err = mappingFile.WriteString(mappingFileContent)
553 require.NoError(t, err)
554 err = mappingFile.Close()
555 require.NoError(t, err)
556
557 // Load configuration from both sources
558 config, err := tmconfig.LoadFromSources(mainConfigFile.Name(), []string{mappingFile.Name()})
559 require.NoError(t, err)
560
561 // Verify that both mappers are loaded
562 require.Len(t, config.Lists, 2)
563
564 ids := make([]string, len(config.Lists))
565 for i, list := range config.Lists {
566 ids[i] = list.ID
567 }
568 assert.Contains(t, ids, "main-mapper")
569 assert.Contains(t, ids, "additional-mapper")
570
571 // Verify custom SDK and server are preserved from main config
572 assert.Equal(t, "https://custom.example.com/sdk.js", config.SDK)
573 assert.Equal(t, "https://custom.example.com/", config.Server)
574
575 // Create mapper and test it works
576 m, err := mapper.NewMapper(config.Lists)
577 require.NoError(t, err)
578 require.NotNil(t, m)
579}
Akron14678dc2025-06-05 13:01:38 +0200580
581func TestExpandGlobs(t *testing.T) {
582 // Create a temporary directory for test files
583 tempDir, err := os.MkdirTemp("", "glob_test_*")
584 require.NoError(t, err)
585 defer os.RemoveAll(tempDir)
586
587 // Create test files with .yaml and .yml extensions
588 testFiles := []struct {
589 name string
590 content string
591 }{
592 {
593 name: "mapper1.yaml",
594 content: `
595id: test-mapper-1
596mappings:
597 - "[A] <> [B]"
598`,
599 },
600 {
601 name: "mapper2.yml",
602 content: `
603id: test-mapper-2
604mappings:
605 - "[C] <> [D]"
606`,
607 },
608 {
609 name: "mapper3.yaml",
610 content: `
611id: test-mapper-3
612mappings:
613 - "[E] <> [F]"
614`,
615 },
616 {
617 name: "other.txt",
618 content: "not a yaml file",
619 },
620 }
621
622 for _, file := range testFiles {
623 filePath := filepath.Join(tempDir, file.name)
624 err := os.WriteFile(filePath, []byte(file.content), 0644)
625 require.NoError(t, err)
626 }
627
628 tests := []struct {
629 name string
630 patterns []string
631 expected []string
632 expectErr bool
633 }{
634 {
635 name: "Single literal file",
636 patterns: []string{filepath.Join(tempDir, "mapper1.yaml")},
637 expected: []string{filepath.Join(tempDir, "mapper1.yaml")},
638 },
639 {
640 name: "Multiple literal files",
641 patterns: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper2.yml")},
642 expected: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper2.yml")},
643 },
644 {
645 name: "Glob pattern for yaml files",
646 patterns: []string{filepath.Join(tempDir, "*.yaml")},
647 expected: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper3.yaml")},
648 },
649 {
650 name: "Glob pattern for yml files",
651 patterns: []string{filepath.Join(tempDir, "*.yml")},
652 expected: []string{filepath.Join(tempDir, "mapper2.yml")},
653 },
654 {
655 name: "Glob pattern for all yaml/yml files",
656 patterns: []string{filepath.Join(tempDir, "*.y*ml")},
657 expected: []string{
658 filepath.Join(tempDir, "mapper1.yaml"),
659 filepath.Join(tempDir, "mapper2.yml"),
660 filepath.Join(tempDir, "mapper3.yaml"),
661 },
662 },
663 {
664 name: "Mixed literal and glob",
665 patterns: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "*.yml")},
666 expected: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper2.yml")},
667 },
668 {
669 name: "No matches - treats as literal",
670 patterns: []string{filepath.Join(tempDir, "nonexistent*.yaml")},
671 expected: []string{filepath.Join(tempDir, "nonexistent*.yaml")},
672 },
673 {
674 name: "Invalid glob pattern",
675 patterns: []string{filepath.Join(tempDir, "[")},
676 expectErr: true,
677 },
678 }
679
680 for _, tt := range tests {
681 t.Run(tt.name, func(t *testing.T) {
682 result, err := expandGlobs(tt.patterns)
683
684 if tt.expectErr {
685 assert.Error(t, err)
686 return
687 }
688
689 require.NoError(t, err)
690
691 // Sort both slices for comparison since glob results may not be in consistent order
692 sort.Strings(result)
693 sort.Strings(tt.expected)
694
695 assert.Equal(t, tt.expected, result)
696 })
697 }
698}
699
700func TestGlobMappingFileLoading(t *testing.T) {
701 // Create a temporary directory for test files
702 tempDir, err := os.MkdirTemp("", "glob_mapping_test_*")
703 require.NoError(t, err)
704 defer os.RemoveAll(tempDir)
705
706 // Create test mapping files
707 testFiles := []struct {
708 name string
709 content string
710 }{
711 {
712 name: "pos-mapper.yaml",
713 content: `
714id: pos-mapper
715foundryA: opennlp
716layerA: p
717foundryB: upos
718layerB: p
719mappings:
720 - "[PIDAT] <> [DET]"
721 - "[ADJA] <> [ADJ]"
722`,
723 },
724 {
725 name: "ner-mapper.yml",
726 content: `
727id: ner-mapper
728foundryA: opennlp
729layerA: ner
730foundryB: upos
731layerB: ner
732mappings:
733 - "[PER] <> [PERSON]"
734 - "[LOC] <> [LOCATION]"
735`,
736 },
737 {
738 name: "special-mapper.yaml",
739 content: `
740id: special-mapper
741mappings:
742 - "[X] <> [Y]"
743`,
744 },
745 }
746
747 for _, file := range testFiles {
748 filePath := filepath.Join(tempDir, file.name)
749 err := os.WriteFile(filePath, []byte(file.content), 0644)
750 require.NoError(t, err)
751 }
752
753 tests := []struct {
754 name string
755 configFile string
756 mappingPattern string
757 expectedIDs []string
758 }{
759 {
760 name: "Load all yaml files",
761 mappingPattern: filepath.Join(tempDir, "*.yaml"),
762 expectedIDs: []string{"pos-mapper", "special-mapper"},
763 },
764 {
765 name: "Load all yml files",
766 mappingPattern: filepath.Join(tempDir, "*.yml"),
767 expectedIDs: []string{"ner-mapper"},
768 },
769 {
770 name: "Load all yaml/yml files",
771 mappingPattern: filepath.Join(tempDir, "*-mapper.y*ml"),
772 expectedIDs: []string{"pos-mapper", "ner-mapper", "special-mapper"},
773 },
774 }
775
776 for _, tt := range tests {
777 t.Run(tt.name, func(t *testing.T) {
778 // Expand the glob pattern
779 expanded, err := expandGlobs([]string{tt.mappingPattern})
780 require.NoError(t, err)
781
782 // Load configuration using the expanded file list
783 config, err := tmconfig.LoadFromSources(tt.configFile, expanded)
784 require.NoError(t, err)
785
786 // Verify that the expected mappers are loaded
787 require.Len(t, config.Lists, len(tt.expectedIDs))
788
789 actualIDs := make([]string, len(config.Lists))
790 for i, list := range config.Lists {
791 actualIDs[i] = list.ID
792 }
793
794 // Sort both slices for comparison
795 sort.Strings(actualIDs)
796 sort.Strings(tt.expectedIDs)
797 assert.Equal(t, tt.expectedIDs, actualIDs)
798
799 // Create mapper to ensure all loaded configs are valid
800 m, err := mapper.NewMapper(config.Lists)
801 require.NoError(t, err)
802 require.NotNil(t, m)
803 })
804 }
805}
806
807func TestGlobErrorHandling(t *testing.T) {
808 tests := []struct {
809 name string
810 patterns []string
811 expectErr bool
812 }{
813 {
814 name: "Empty patterns",
815 patterns: []string{},
816 expectErr: false, // Should return empty slice, no error
817 },
818 {
819 name: "Invalid glob pattern",
820 patterns: []string{"["},
821 expectErr: true,
822 },
823 {
824 name: "Valid and invalid mixed",
825 patterns: []string{"valid.yaml", "["},
826 expectErr: true,
827 },
828 }
829
830 for _, tt := range tests {
831 t.Run(tt.name, func(t *testing.T) {
832 result, err := expandGlobs(tt.patterns)
833
834 if tt.expectErr {
835 assert.Error(t, err)
836 assert.Nil(t, result)
837 } else {
838 assert.NoError(t, err)
839 if len(tt.patterns) == 0 {
840 assert.Empty(t, result)
841 }
842 }
843 })
844 }
845}
846
847func TestGlobIntegrationWithTestData(t *testing.T) {
848 // Test that our glob functionality works with the actual testdata files
849 // This ensures the feature works end-to-end in a realistic scenario
850
851 // Expand glob pattern for the example mapper files
852 expanded, err := expandGlobs([]string{"../../testdata/example-mapper*.yaml"})
853 require.NoError(t, err)
854
855 // Should match exactly the two mapper files
856 sort.Strings(expanded)
857 assert.Len(t, expanded, 2)
858 assert.Contains(t, expanded[0], "example-mapper1.yaml")
859 assert.Contains(t, expanded[1], "example-mapper2.yaml")
860
861 // Load configuration using the expanded files
862 config, err := tmconfig.LoadFromSources("", expanded)
863 require.NoError(t, err)
864
865 // Verify that both mappers are loaded correctly
866 require.Len(t, config.Lists, 2)
867
868 // Get the IDs to verify they match the expected ones
869 actualIDs := make([]string, len(config.Lists))
870 for i, list := range config.Lists {
871 actualIDs[i] = list.ID
872 }
873 sort.Strings(actualIDs)
874
875 expectedIDs := []string{"example-mapper-1", "example-mapper-2"}
876 assert.Equal(t, expectedIDs, actualIDs)
877
878 // Create mapper to ensure everything works
879 m, err := mapper.NewMapper(config.Lists)
880 require.NoError(t, err)
881 require.NotNil(t, m)
882
883 // Test that the mapper actually works with a real transformation
884 app := fiber.New()
885 setupRoutes(app, m, config)
886
887 // Test a transformation from example-mapper-1
888 testInput := `{
889 "@type": "koral:token",
890 "wrap": {
891 "@type": "koral:term",
892 "foundry": "opennlp",
893 "key": "PIDAT",
894 "layer": "p",
895 "match": "match:eq"
896 }
897 }`
898
899 req := httptest.NewRequest(http.MethodPost, "/example-mapper-1/query?dir=atob", bytes.NewBufferString(testInput))
900 req.Header.Set("Content-Type", "application/json")
901
902 resp, err := app.Test(req)
903 require.NoError(t, err)
904 defer resp.Body.Close()
905
906 assert.Equal(t, http.StatusOK, resp.StatusCode)
907
908 var result map[string]interface{}
909 err = json.NewDecoder(resp.Body).Decode(&result)
910 require.NoError(t, err)
911
912 // Verify the transformation was applied
913 wrap := result["wrap"].(map[string]interface{})
914 assert.Equal(t, "koral:termGroup", wrap["@type"])
915 operands := wrap["operands"].([]interface{})
916 require.Greater(t, len(operands), 0)
917 firstOperand := operands[0].(map[string]interface{})
918 assert.Equal(t, "DET", firstOperand["key"])
919}