blob: 8030b37adeddace30ef4da5f7cedbab7d9fd62ba [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
Akron4de47a92025-06-27 11:58:11 +0200261func TestResponseTransformEndpoint(t *testing.T) {
262 // Create test mapping list
263 mappingList := tmconfig.MappingList{
264 ID: "test-response-mapper",
265 FoundryA: "marmot",
266 LayerA: "m",
267 FoundryB: "opennlp",
268 LayerB: "p",
269 Mappings: []tmconfig.MappingRule{
270 "[gender=masc] <> [p=M & m=M]",
271 },
272 }
273
274 // Create mapper
275 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
276 require.NoError(t, err)
277
278 // Create mock config for testing
279 mockConfig := &tmconfig.MappingConfig{
280 Lists: []tmconfig.MappingList{mappingList},
281 }
282
283 // Create fiber app
284 app := fiber.New()
285 setupRoutes(app, m, mockConfig)
286
287 tests := []struct {
288 name string
289 mapID string
290 direction string
291 foundryA string
292 foundryB string
293 layerA string
294 layerB string
295 input string
296 expectedCode int
297 expectedBody string
298 expectedError string
299 }{
300 {
301 name: "Simple response mapping with snippet transformation",
302 mapID: "test-response-mapper",
303 direction: "atob",
304 input: `{
305 "snippet": "<span title=\"marmot/m:gender:masc\">Der</span>"
306 }`,
307 expectedCode: http.StatusOK,
308 expectedBody: `{
309 "snippet": "<span title=\"marmot/m:gender:masc\"><span title=\"opennlp/p:M\" class=\"notinindex\"><span title=\"opennlp/m:M\" class=\"notinindex\">Der</span></span></span>"
310 }`,
311 },
312 {
313 name: "Response with no snippet field",
314 mapID: "test-response-mapper",
315 direction: "atob",
316 input: `{
317 "@type": "koral:response",
318 "meta": {
319 "version": "Krill-0.64.1"
320 }
321 }`,
322 expectedCode: http.StatusOK,
323 expectedBody: `{
324 "@type": "koral:response",
325 "meta": {
326 "version": "Krill-0.64.1"
327 }
328 }`,
329 },
330 {
331 name: "Response with null snippet",
332 mapID: "test-response-mapper",
333 direction: "atob",
334 input: `{
335 "snippet": null
336 }`,
337 expectedCode: http.StatusOK,
338 expectedBody: `{
339 "snippet": null
340 }`,
341 },
342 {
343 name: "Response with non-string snippet",
344 mapID: "test-response-mapper",
345 direction: "atob",
346 input: `{
347 "snippet": 123
348 }`,
349 expectedCode: http.StatusOK,
350 expectedBody: `{
351 "snippet": 123
352 }`,
353 },
354 {
355 name: "Response mapping with foundry override",
356 mapID: "test-response-mapper",
357 direction: "atob",
358 foundryB: "custom",
359 input: `{
360 "snippet": "<span title=\"marmot/m:gender:masc\">Der</span>"
361 }`,
362 expectedCode: http.StatusOK,
363 expectedBody: `{
364 "snippet": "<span title=\"marmot/m:gender:masc\"><span title=\"custom/p:M\" class=\"notinindex\"><span title=\"custom/m:M\" class=\"notinindex\">Der</span></span></span>"
365 }`,
366 },
367 {
368 name: "Invalid mapping ID for response",
369 mapID: "nonexistent",
370 direction: "atob",
371 input: `{"snippet": "<span>test</span>"}`,
372 expectedCode: http.StatusInternalServerError,
373 expectedError: "mapping list with ID nonexistent not found",
374 },
375 {
376 name: "Invalid direction for response",
377 mapID: "test-response-mapper",
378 direction: "invalid",
379 input: `{"snippet": "<span>test</span>"}`,
380 expectedCode: http.StatusBadRequest,
381 expectedError: "invalid direction, must be 'atob' or 'btoa'",
382 },
383 {
384 name: "Invalid JSON for response",
385 mapID: "test-response-mapper",
386 direction: "atob",
387 input: `{invalid json}`,
388 expectedCode: http.StatusBadRequest,
389 expectedError: "invalid JSON in request body",
390 },
391 }
392
393 for _, tt := range tests {
394 t.Run(tt.name, func(t *testing.T) {
395 // Build URL with query parameters
396 url := "/" + tt.mapID + "/response"
397 if tt.direction != "" {
398 url += "?dir=" + tt.direction
399 }
400 if tt.foundryA != "" {
401 url += "&foundryA=" + tt.foundryA
402 }
403 if tt.foundryB != "" {
404 url += "&foundryB=" + tt.foundryB
405 }
406 if tt.layerA != "" {
407 url += "&layerA=" + tt.layerA
408 }
409 if tt.layerB != "" {
410 url += "&layerB=" + tt.layerB
411 }
412
413 // Make request
414 req := httptest.NewRequest(http.MethodPost, url, bytes.NewBufferString(tt.input))
415 req.Header.Set("Content-Type", "application/json")
416 resp, err := app.Test(req)
417 require.NoError(t, err)
418 defer resp.Body.Close()
419
420 // Check status code
421 assert.Equal(t, tt.expectedCode, resp.StatusCode)
422
423 // Read response body
424 body, err := io.ReadAll(resp.Body)
425 require.NoError(t, err)
426
427 if tt.expectedError != "" {
428 // Check error message
429 var errResp fiber.Map
430 err = json.Unmarshal(body, &errResp)
431 require.NoError(t, err)
432 assert.Equal(t, tt.expectedError, errResp["error"])
433 } else {
434 // Compare JSON responses
435 var expected, actual any
436 err = json.Unmarshal([]byte(tt.expectedBody), &expected)
437 require.NoError(t, err)
438 err = json.Unmarshal(body, &actual)
439 require.NoError(t, err)
440 assert.Equal(t, expected, actual)
441 }
442 })
443 }
444}
445
Akron49ceeb42025-05-23 17:46:01 +0200446func TestHealthEndpoint(t *testing.T) {
Akrona00d4752025-05-26 17:34:36 +0200447 // Create test mapping list
448 mappingList := tmconfig.MappingList{
449 ID: "test-mapper",
450 Mappings: []tmconfig.MappingRule{
451 "[A] <> [B]",
452 },
453 }
Akron49ceeb42025-05-23 17:46:01 +0200454
Akrona00d4752025-05-26 17:34:36 +0200455 // Create mapper
456 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
Akron49ceeb42025-05-23 17:46:01 +0200457 require.NoError(t, err)
458
Akron40aaa632025-06-03 17:57:52 +0200459 // Create mock config for testing
Akron06d21f02025-06-04 14:36:07 +0200460 mockConfig := &tmconfig.MappingConfig{
Akron40aaa632025-06-03 17:57:52 +0200461 Lists: []tmconfig.MappingList{mappingList},
462 }
463
Akron49ceeb42025-05-23 17:46:01 +0200464 // Create fiber app
465 app := fiber.New()
Akron40aaa632025-06-03 17:57:52 +0200466 setupRoutes(app, m, mockConfig)
Akron49ceeb42025-05-23 17:46:01 +0200467
468 // Test health endpoint
469 req := httptest.NewRequest(http.MethodGet, "/health", nil)
470 resp, err := app.Test(req)
471 require.NoError(t, err)
472 defer resp.Body.Close()
473
474 assert.Equal(t, http.StatusOK, resp.StatusCode)
475 body, err := io.ReadAll(resp.Body)
476 require.NoError(t, err)
477 assert.Equal(t, "OK", string(body))
Akron40aaa632025-06-03 17:57:52 +0200478
Akronc471c0a2025-06-04 11:56:22 +0200479 req = httptest.NewRequest(http.MethodGet, "/", nil)
Akron40aaa632025-06-03 17:57:52 +0200480 resp, err = app.Test(req)
481 require.NoError(t, err)
482 defer resp.Body.Close()
483
484 assert.Equal(t, http.StatusOK, resp.StatusCode)
485 body, err = io.ReadAll(resp.Body)
486 require.NoError(t, err)
Akronfc77b5e2025-06-04 11:44:43 +0200487 assert.Contains(t, string(body), "KoralPipe-TermMapper")
Akron40aaa632025-06-03 17:57:52 +0200488
Akron49ceeb42025-05-23 17:46:01 +0200489}
Akron06d21f02025-06-04 14:36:07 +0200490
491func TestKalamarPluginWithCustomSdkAndServer(t *testing.T) {
492 // Create test mapping list
493 mappingList := tmconfig.MappingList{
494 ID: "test-mapper",
495 Mappings: []tmconfig.MappingRule{
496 "[A] <> [B]",
497 },
498 }
499
500 // Create mapper
501 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
502 require.NoError(t, err)
503
504 tests := []struct {
505 name string
506 customSDK string
507 customServer string
508 expectedSDK string
509 expectedServer string
510 }{
511 {
512 name: "Custom SDK and Server values",
513 customSDK: "https://custom.example.com/custom-sdk.js",
514 customServer: "https://custom.example.com/",
515 expectedSDK: "https://custom.example.com/custom-sdk.js",
516 expectedServer: "https://custom.example.com/",
517 },
518 {
519 name: "Only custom SDK value",
520 customSDK: "https://custom.example.com/custom-sdk.js",
521 customServer: "https://korap.ids-mannheim.de/", // defaults applied during parsing
522 expectedSDK: "https://custom.example.com/custom-sdk.js",
523 expectedServer: "https://korap.ids-mannheim.de/",
524 },
525 {
526 name: "Only custom Server value",
527 customSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // defaults applied during parsing
528 customServer: "https://custom.example.com/",
529 expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js",
530 expectedServer: "https://custom.example.com/",
531 },
532 {
533 name: "Defaults applied during parsing",
534 customSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // defaults applied during parsing
535 customServer: "https://korap.ids-mannheim.de/", // defaults applied during parsing
536 expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js",
537 expectedServer: "https://korap.ids-mannheim.de/",
538 },
539 }
540
541 for _, tt := range tests {
542 t.Run(tt.name, func(t *testing.T) {
543 // Create mock config with custom values
544 mockConfig := &tmconfig.MappingConfig{
545 SDK: tt.customSDK,
546 Server: tt.customServer,
547 Lists: []tmconfig.MappingList{mappingList},
548 }
549
550 // Create fiber app
551 app := fiber.New()
552 setupRoutes(app, m, mockConfig)
553
554 // Test Kalamar plugin endpoint
555 req := httptest.NewRequest(http.MethodGet, "/", nil)
556 resp, err := app.Test(req)
557 require.NoError(t, err)
558 defer resp.Body.Close()
559
560 assert.Equal(t, http.StatusOK, resp.StatusCode)
561 body, err := io.ReadAll(resp.Body)
562 require.NoError(t, err)
563
564 htmlContent := string(body)
565
566 // Check that the HTML contains the expected SDK and Server values
567 assert.Contains(t, htmlContent, `src="`+tt.expectedSDK+`"`)
568 assert.Contains(t, htmlContent, `data-server="`+tt.expectedServer+`"`)
569
570 // Ensure it's still a valid HTML page
571 assert.Contains(t, htmlContent, "KoralPipe-TermMapper")
572 assert.Contains(t, htmlContent, "<!DOCTYPE html>")
573 })
574 }
575}
Akrone1cff7c2025-06-04 18:43:32 +0200576
577func TestMultipleMappingFiles(t *testing.T) {
578 // Create test mapping files
579 mappingFile1Content := `
580id: test-mapper-1
581foundryA: opennlp
582layerA: p
583foundryB: upos
584layerB: p
585mappings:
586 - "[PIDAT] <> [DET & AdjType=Pdt]"
587 - "[PAV] <> [ADV & PronType=Dem]"
588`
589 mappingFile1, err := os.CreateTemp("", "mapping1-*.yaml")
590 require.NoError(t, err)
591 defer os.Remove(mappingFile1.Name())
592
593 _, err = mappingFile1.WriteString(mappingFile1Content)
594 require.NoError(t, err)
595 err = mappingFile1.Close()
596 require.NoError(t, err)
597
598 mappingFile2Content := `
599id: test-mapper-2
600foundryA: stts
601layerA: p
602foundryB: upos
603layerB: p
604mappings:
605 - "[DET] <> [PRON]"
606 - "[ADJ] <> [NOUN]"
607`
608 mappingFile2, err := os.CreateTemp("", "mapping2-*.yaml")
609 require.NoError(t, err)
610 defer os.Remove(mappingFile2.Name())
611
612 _, err = mappingFile2.WriteString(mappingFile2Content)
613 require.NoError(t, err)
614 err = mappingFile2.Close()
615 require.NoError(t, err)
616
617 // Load configuration using multiple mapping files
618 config, err := tmconfig.LoadFromSources("", []string{mappingFile1.Name(), mappingFile2.Name()})
619 require.NoError(t, err)
620
621 // Create mapper
622 m, err := mapper.NewMapper(config.Lists)
623 require.NoError(t, err)
624
625 // Create fiber app
626 app := fiber.New()
627 setupRoutes(app, m, config)
628
629 // Test that both mappers work
630 testCases := []struct {
631 name string
632 mapID string
633 input string
634 expectGroup bool
635 expectedKey string
636 }{
637 {
638 name: "test-mapper-1 with complex mapping",
639 mapID: "test-mapper-1",
640 input: `{
641 "@type": "koral:token",
642 "wrap": {
643 "@type": "koral:term",
644 "foundry": "opennlp",
645 "key": "PIDAT",
646 "layer": "p",
647 "match": "match:eq"
648 }
649 }`,
650 expectGroup: true, // This mapping creates a termGroup because of "&"
651 expectedKey: "DET", // The first operand should be DET
652 },
653 {
654 name: "test-mapper-2 with simple mapping",
655 mapID: "test-mapper-2",
656 input: `{
657 "@type": "koral:token",
658 "wrap": {
659 "@type": "koral:term",
660 "foundry": "stts",
661 "key": "DET",
662 "layer": "p",
663 "match": "match:eq"
664 }
665 }`,
666 expectGroup: false, // This mapping creates a simple term
667 expectedKey: "PRON",
668 },
669 }
670
671 for _, tc := range testCases {
672 t.Run(tc.name, func(t *testing.T) {
673 req := httptest.NewRequest(http.MethodPost, "/"+tc.mapID+"/query?dir=atob", bytes.NewBufferString(tc.input))
674 req.Header.Set("Content-Type", "application/json")
675
676 resp, err := app.Test(req)
677 require.NoError(t, err)
678 defer resp.Body.Close()
679
680 assert.Equal(t, http.StatusOK, resp.StatusCode)
681
682 var result map[string]interface{}
683 err = json.NewDecoder(resp.Body).Decode(&result)
684 require.NoError(t, err)
685
686 // Check that the mapping was applied
687 wrap := result["wrap"].(map[string]interface{})
688 if tc.expectGroup {
689 // For complex mappings, check the first operand
690 assert.Equal(t, "koral:termGroup", wrap["@type"])
691 operands := wrap["operands"].([]interface{})
692 require.Greater(t, len(operands), 0)
693 firstOperand := operands[0].(map[string]interface{})
694 assert.Equal(t, tc.expectedKey, firstOperand["key"])
695 } else {
696 // For simple mappings, check the key directly
697 assert.Equal(t, "koral:term", wrap["@type"])
698 assert.Equal(t, tc.expectedKey, wrap["key"])
699 }
700 })
701 }
702}
703
704func TestCombinedConfigAndMappingFiles(t *testing.T) {
705 // Create main config file
706 mainConfigContent := `
707sdk: "https://custom.example.com/sdk.js"
708server: "https://custom.example.com/"
709lists:
710- id: main-mapper
711 foundryA: opennlp
712 layerA: p
713 mappings:
714 - "[A] <> [B]"
715`
716 mainConfigFile, err := os.CreateTemp("", "main-config-*.yaml")
717 require.NoError(t, err)
718 defer os.Remove(mainConfigFile.Name())
719
720 _, err = mainConfigFile.WriteString(mainConfigContent)
721 require.NoError(t, err)
722 err = mainConfigFile.Close()
723 require.NoError(t, err)
724
725 // Create individual mapping file
726 mappingFileContent := `
727id: additional-mapper
728foundryA: stts
729layerA: p
730mappings:
731 - "[C] <> [D]"
732`
733 mappingFile, err := os.CreateTemp("", "mapping-*.yaml")
734 require.NoError(t, err)
735 defer os.Remove(mappingFile.Name())
736
737 _, err = mappingFile.WriteString(mappingFileContent)
738 require.NoError(t, err)
739 err = mappingFile.Close()
740 require.NoError(t, err)
741
742 // Load configuration from both sources
743 config, err := tmconfig.LoadFromSources(mainConfigFile.Name(), []string{mappingFile.Name()})
744 require.NoError(t, err)
745
746 // Verify that both mappers are loaded
747 require.Len(t, config.Lists, 2)
748
749 ids := make([]string, len(config.Lists))
750 for i, list := range config.Lists {
751 ids[i] = list.ID
752 }
753 assert.Contains(t, ids, "main-mapper")
754 assert.Contains(t, ids, "additional-mapper")
755
756 // Verify custom SDK and server are preserved from main config
757 assert.Equal(t, "https://custom.example.com/sdk.js", config.SDK)
758 assert.Equal(t, "https://custom.example.com/", config.Server)
759
760 // Create mapper and test it works
761 m, err := mapper.NewMapper(config.Lists)
762 require.NoError(t, err)
763 require.NotNil(t, m)
764}
Akron14678dc2025-06-05 13:01:38 +0200765
766func TestExpandGlobs(t *testing.T) {
767 // Create a temporary directory for test files
768 tempDir, err := os.MkdirTemp("", "glob_test_*")
769 require.NoError(t, err)
770 defer os.RemoveAll(tempDir)
771
772 // Create test files with .yaml and .yml extensions
773 testFiles := []struct {
774 name string
775 content string
776 }{
777 {
778 name: "mapper1.yaml",
779 content: `
780id: test-mapper-1
781mappings:
782 - "[A] <> [B]"
783`,
784 },
785 {
786 name: "mapper2.yml",
787 content: `
788id: test-mapper-2
789mappings:
790 - "[C] <> [D]"
791`,
792 },
793 {
794 name: "mapper3.yaml",
795 content: `
796id: test-mapper-3
797mappings:
798 - "[E] <> [F]"
799`,
800 },
801 {
802 name: "other.txt",
803 content: "not a yaml file",
804 },
805 }
806
807 for _, file := range testFiles {
808 filePath := filepath.Join(tempDir, file.name)
809 err := os.WriteFile(filePath, []byte(file.content), 0644)
810 require.NoError(t, err)
811 }
812
813 tests := []struct {
814 name string
815 patterns []string
816 expected []string
817 expectErr bool
818 }{
819 {
820 name: "Single literal file",
821 patterns: []string{filepath.Join(tempDir, "mapper1.yaml")},
822 expected: []string{filepath.Join(tempDir, "mapper1.yaml")},
823 },
824 {
825 name: "Multiple literal files",
826 patterns: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper2.yml")},
827 expected: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper2.yml")},
828 },
829 {
830 name: "Glob pattern for yaml files",
831 patterns: []string{filepath.Join(tempDir, "*.yaml")},
832 expected: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper3.yaml")},
833 },
834 {
835 name: "Glob pattern for yml files",
836 patterns: []string{filepath.Join(tempDir, "*.yml")},
837 expected: []string{filepath.Join(tempDir, "mapper2.yml")},
838 },
839 {
840 name: "Glob pattern for all yaml/yml files",
841 patterns: []string{filepath.Join(tempDir, "*.y*ml")},
842 expected: []string{
843 filepath.Join(tempDir, "mapper1.yaml"),
844 filepath.Join(tempDir, "mapper2.yml"),
845 filepath.Join(tempDir, "mapper3.yaml"),
846 },
847 },
848 {
849 name: "Mixed literal and glob",
850 patterns: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "*.yml")},
851 expected: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper2.yml")},
852 },
853 {
854 name: "No matches - treats as literal",
855 patterns: []string{filepath.Join(tempDir, "nonexistent*.yaml")},
856 expected: []string{filepath.Join(tempDir, "nonexistent*.yaml")},
857 },
858 {
859 name: "Invalid glob pattern",
860 patterns: []string{filepath.Join(tempDir, "[")},
861 expectErr: true,
862 },
863 }
864
865 for _, tt := range tests {
866 t.Run(tt.name, func(t *testing.T) {
867 result, err := expandGlobs(tt.patterns)
868
869 if tt.expectErr {
870 assert.Error(t, err)
871 return
872 }
873
874 require.NoError(t, err)
875
876 // Sort both slices for comparison since glob results may not be in consistent order
877 sort.Strings(result)
878 sort.Strings(tt.expected)
879
880 assert.Equal(t, tt.expected, result)
881 })
882 }
883}
884
885func TestGlobMappingFileLoading(t *testing.T) {
886 // Create a temporary directory for test files
887 tempDir, err := os.MkdirTemp("", "glob_mapping_test_*")
888 require.NoError(t, err)
889 defer os.RemoveAll(tempDir)
890
891 // Create test mapping files
892 testFiles := []struct {
893 name string
894 content string
895 }{
896 {
897 name: "pos-mapper.yaml",
898 content: `
899id: pos-mapper
900foundryA: opennlp
901layerA: p
902foundryB: upos
903layerB: p
904mappings:
905 - "[PIDAT] <> [DET]"
906 - "[ADJA] <> [ADJ]"
907`,
908 },
909 {
910 name: "ner-mapper.yml",
911 content: `
912id: ner-mapper
913foundryA: opennlp
914layerA: ner
915foundryB: upos
916layerB: ner
917mappings:
918 - "[PER] <> [PERSON]"
919 - "[LOC] <> [LOCATION]"
920`,
921 },
922 {
923 name: "special-mapper.yaml",
924 content: `
925id: special-mapper
926mappings:
927 - "[X] <> [Y]"
928`,
929 },
930 }
931
932 for _, file := range testFiles {
933 filePath := filepath.Join(tempDir, file.name)
934 err := os.WriteFile(filePath, []byte(file.content), 0644)
935 require.NoError(t, err)
936 }
937
938 tests := []struct {
939 name string
940 configFile string
941 mappingPattern string
942 expectedIDs []string
943 }{
944 {
945 name: "Load all yaml files",
946 mappingPattern: filepath.Join(tempDir, "*.yaml"),
947 expectedIDs: []string{"pos-mapper", "special-mapper"},
948 },
949 {
950 name: "Load all yml files",
951 mappingPattern: filepath.Join(tempDir, "*.yml"),
952 expectedIDs: []string{"ner-mapper"},
953 },
954 {
955 name: "Load all yaml/yml files",
956 mappingPattern: filepath.Join(tempDir, "*-mapper.y*ml"),
957 expectedIDs: []string{"pos-mapper", "ner-mapper", "special-mapper"},
958 },
959 }
960
961 for _, tt := range tests {
962 t.Run(tt.name, func(t *testing.T) {
963 // Expand the glob pattern
964 expanded, err := expandGlobs([]string{tt.mappingPattern})
965 require.NoError(t, err)
966
967 // Load configuration using the expanded file list
968 config, err := tmconfig.LoadFromSources(tt.configFile, expanded)
969 require.NoError(t, err)
970
971 // Verify that the expected mappers are loaded
972 require.Len(t, config.Lists, len(tt.expectedIDs))
973
974 actualIDs := make([]string, len(config.Lists))
975 for i, list := range config.Lists {
976 actualIDs[i] = list.ID
977 }
978
979 // Sort both slices for comparison
980 sort.Strings(actualIDs)
981 sort.Strings(tt.expectedIDs)
982 assert.Equal(t, tt.expectedIDs, actualIDs)
983
984 // Create mapper to ensure all loaded configs are valid
985 m, err := mapper.NewMapper(config.Lists)
986 require.NoError(t, err)
987 require.NotNil(t, m)
988 })
989 }
990}
991
992func TestGlobErrorHandling(t *testing.T) {
993 tests := []struct {
994 name string
995 patterns []string
996 expectErr bool
997 }{
998 {
999 name: "Empty patterns",
1000 patterns: []string{},
1001 expectErr: false, // Should return empty slice, no error
1002 },
1003 {
1004 name: "Invalid glob pattern",
1005 patterns: []string{"["},
1006 expectErr: true,
1007 },
1008 {
1009 name: "Valid and invalid mixed",
1010 patterns: []string{"valid.yaml", "["},
1011 expectErr: true,
1012 },
1013 }
1014
1015 for _, tt := range tests {
1016 t.Run(tt.name, func(t *testing.T) {
1017 result, err := expandGlobs(tt.patterns)
1018
1019 if tt.expectErr {
1020 assert.Error(t, err)
1021 assert.Nil(t, result)
1022 } else {
1023 assert.NoError(t, err)
1024 if len(tt.patterns) == 0 {
1025 assert.Empty(t, result)
1026 }
1027 }
1028 })
1029 }
1030}
1031
1032func TestGlobIntegrationWithTestData(t *testing.T) {
1033 // Test that our glob functionality works with the actual testdata files
1034 // This ensures the feature works end-to-end in a realistic scenario
1035
1036 // Expand glob pattern for the example mapper files
1037 expanded, err := expandGlobs([]string{"../../testdata/example-mapper*.yaml"})
1038 require.NoError(t, err)
1039
1040 // Should match exactly the two mapper files
1041 sort.Strings(expanded)
1042 assert.Len(t, expanded, 2)
1043 assert.Contains(t, expanded[0], "example-mapper1.yaml")
1044 assert.Contains(t, expanded[1], "example-mapper2.yaml")
1045
1046 // Load configuration using the expanded files
1047 config, err := tmconfig.LoadFromSources("", expanded)
1048 require.NoError(t, err)
1049
1050 // Verify that both mappers are loaded correctly
1051 require.Len(t, config.Lists, 2)
1052
1053 // Get the IDs to verify they match the expected ones
1054 actualIDs := make([]string, len(config.Lists))
1055 for i, list := range config.Lists {
1056 actualIDs[i] = list.ID
1057 }
1058 sort.Strings(actualIDs)
1059
1060 expectedIDs := []string{"example-mapper-1", "example-mapper-2"}
1061 assert.Equal(t, expectedIDs, actualIDs)
1062
1063 // Create mapper to ensure everything works
1064 m, err := mapper.NewMapper(config.Lists)
1065 require.NoError(t, err)
1066 require.NotNil(t, m)
1067
1068 // Test that the mapper actually works with a real transformation
1069 app := fiber.New()
1070 setupRoutes(app, m, config)
1071
1072 // Test a transformation from example-mapper-1
1073 testInput := `{
1074 "@type": "koral:token",
1075 "wrap": {
1076 "@type": "koral:term",
1077 "foundry": "opennlp",
1078 "key": "PIDAT",
1079 "layer": "p",
1080 "match": "match:eq"
1081 }
1082 }`
1083
1084 req := httptest.NewRequest(http.MethodPost, "/example-mapper-1/query?dir=atob", bytes.NewBufferString(testInput))
1085 req.Header.Set("Content-Type", "application/json")
1086
1087 resp, err := app.Test(req)
1088 require.NoError(t, err)
1089 defer resp.Body.Close()
1090
1091 assert.Equal(t, http.StatusOK, resp.StatusCode)
1092
1093 var result map[string]interface{}
1094 err = json.NewDecoder(resp.Body).Decode(&result)
1095 require.NoError(t, err)
1096
1097 // Verify the transformation was applied
1098 wrap := result["wrap"].(map[string]interface{})
1099 assert.Equal(t, "koral:termGroup", wrap["@type"])
1100 operands := wrap["operands"].([]interface{})
1101 require.Greater(t, len(operands), 0)
1102 firstOperand := operands[0].(map[string]interface{})
1103 assert.Equal(t, "DET", firstOperand["key"])
1104}
Akron2ac2ec02025-06-05 15:26:42 +02001105
1106func TestConfigurableServiceURL(t *testing.T) {
1107 // Create test mapping list
1108 mappingList := tmconfig.MappingList{
1109 ID: "test-mapper",
1110 Mappings: []tmconfig.MappingRule{
1111 "[A] <> [B]",
1112 },
1113 }
1114
1115 tests := []struct {
1116 name string
1117 customServiceURL string
1118 expectedServiceURL string
1119 }{
1120 {
1121 name: "Custom service URL",
1122 customServiceURL: "https://custom.example.com/plugin/termmapper",
1123 expectedServiceURL: "https://custom.example.com/plugin/termmapper",
1124 },
1125 {
1126 name: "Default service URL when not specified",
1127 customServiceURL: "", // Will use default
1128 expectedServiceURL: "https://korap.ids-mannheim.de/plugin/termmapper",
1129 },
1130 {
1131 name: "Custom service URL with different path",
1132 customServiceURL: "https://my-server.org/api/v1/termmapper",
1133 expectedServiceURL: "https://my-server.org/api/v1/termmapper",
1134 },
1135 }
1136
1137 for _, tt := range tests {
1138 t.Run(tt.name, func(t *testing.T) {
1139 // Create mapper
1140 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
1141 require.NoError(t, err)
1142
1143 // Create mock config with custom service URL
1144 mockConfig := &tmconfig.MappingConfig{
1145 ServiceURL: tt.customServiceURL,
1146 Lists: []tmconfig.MappingList{mappingList},
1147 }
1148
1149 // Apply defaults to simulate the real loading process
1150 tmconfig.ApplyDefaults(mockConfig)
1151
1152 // Create fiber app
1153 app := fiber.New()
1154 setupRoutes(app, m, mockConfig)
1155
1156 // Test Kalamar plugin endpoint with a specific mapID
1157 req := httptest.NewRequest(http.MethodGet, "/test-mapper", nil)
1158 resp, err := app.Test(req)
1159 require.NoError(t, err)
1160 defer resp.Body.Close()
1161
1162 assert.Equal(t, http.StatusOK, resp.StatusCode)
1163 body, err := io.ReadAll(resp.Body)
1164 require.NoError(t, err)
1165
1166 htmlContent := string(body)
1167
1168 // Check that the HTML contains the expected service URL in the JavaScript
1169 expectedJSURL := tt.expectedServiceURL + "/test-mapper/query"
1170 assert.Contains(t, htmlContent, "'service' : '"+expectedJSURL+"'")
1171
1172 // Ensure it's still a valid HTML page
1173 assert.Contains(t, htmlContent, "KoralPipe-TermMapper")
1174 assert.Contains(t, htmlContent, "<!DOCTYPE html>")
1175 })
1176 }
1177}
1178
1179func TestServiceURLConfigFileLoading(t *testing.T) {
1180 // Create a temporary config file with custom service URL
1181 configContent := `
1182sdk: "https://custom.example.com/sdk.js"
1183server: "https://custom.example.com/"
1184serviceURL: "https://custom.example.com/api/termmapper"
1185lists:
1186- id: config-mapper
1187 mappings:
1188 - "[X] <> [Y]"
1189`
1190 configFile, err := os.CreateTemp("", "service-url-config-*.yaml")
1191 require.NoError(t, err)
1192 defer os.Remove(configFile.Name())
1193
1194 _, err = configFile.WriteString(configContent)
1195 require.NoError(t, err)
1196 err = configFile.Close()
1197 require.NoError(t, err)
1198
1199 // Load configuration from file
1200 config, err := tmconfig.LoadFromSources(configFile.Name(), nil)
1201 require.NoError(t, err)
1202
1203 // Verify that the service URL was loaded correctly
1204 assert.Equal(t, "https://custom.example.com/api/termmapper", config.ServiceURL)
1205
1206 // Verify other fields are also preserved
1207 assert.Equal(t, "https://custom.example.com/sdk.js", config.SDK)
1208 assert.Equal(t, "https://custom.example.com/", config.Server)
1209
1210 // Create mapper and test the service URL is used in the HTML
1211 m, err := mapper.NewMapper(config.Lists)
1212 require.NoError(t, err)
1213
1214 app := fiber.New()
1215 setupRoutes(app, m, config)
1216
1217 req := httptest.NewRequest(http.MethodGet, "/config-mapper", nil)
1218 resp, err := app.Test(req)
1219 require.NoError(t, err)
1220 defer resp.Body.Close()
1221
1222 assert.Equal(t, http.StatusOK, resp.StatusCode)
1223 body, err := io.ReadAll(resp.Body)
1224 require.NoError(t, err)
1225
1226 htmlContent := string(body)
1227 expectedJSURL := "https://custom.example.com/api/termmapper/config-mapper/query"
1228 assert.Contains(t, htmlContent, "'service' : '"+expectedJSURL+"'")
1229}
1230
1231func TestServiceURLDefaults(t *testing.T) {
1232 // Test that defaults are applied correctly when creating a config
1233 config := &tmconfig.MappingConfig{
1234 Lists: []tmconfig.MappingList{
1235 {
1236 ID: "test",
1237 Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
1238 },
1239 },
1240 }
1241
1242 // Apply defaults (simulating what happens during loading)
1243 tmconfig.ApplyDefaults(config)
1244
1245 // Check that the default service URL was applied
1246 assert.Equal(t, "https://korap.ids-mannheim.de/plugin/termmapper", config.ServiceURL)
1247
1248 // Check that other defaults were also applied
1249 assert.Equal(t, "https://korap.ids-mannheim.de/", config.Server)
1250 assert.Equal(t, "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", config.SDK)
Akron24ab22e2025-06-24 16:37:33 +02001251 assert.Equal(t, 5725, config.Port)
Akron2ac2ec02025-06-05 15:26:42 +02001252 assert.Equal(t, "warn", config.LogLevel)
1253}
1254
1255func TestServiceURLWithExampleConfig(t *testing.T) {
1256 // Test that the actual example config file works with the new serviceURL functionality
1257 // and that defaults are properly applied when serviceURL is not specified
1258
1259 config, err := tmconfig.LoadFromSources("../../testdata/example-config.yaml", nil)
1260 require.NoError(t, err)
1261
1262 // Verify that the default service URL was applied since it's not in the example config
1263 assert.Equal(t, "https://korap.ids-mannheim.de/plugin/termmapper", config.ServiceURL)
1264
1265 // Verify other values from the example config are preserved
1266 assert.Equal(t, "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", config.SDK)
1267 assert.Equal(t, "https://korap.ids-mannheim.de/", config.Server)
1268
1269 // Verify the mapper was loaded correctly
1270 require.Len(t, config.Lists, 1)
1271 assert.Equal(t, "main-config-mapper", config.Lists[0].ID)
1272
1273 // Create mapper and test that the service URL is used correctly in the HTML
1274 m, err := mapper.NewMapper(config.Lists)
1275 require.NoError(t, err)
1276
1277 app := fiber.New()
1278 setupRoutes(app, m, config)
1279
1280 req := httptest.NewRequest(http.MethodGet, "/main-config-mapper", nil)
1281 resp, err := app.Test(req)
1282 require.NoError(t, err)
1283 defer resp.Body.Close()
1284
1285 assert.Equal(t, http.StatusOK, resp.StatusCode)
1286 body, err := io.ReadAll(resp.Body)
1287 require.NoError(t, err)
1288
1289 htmlContent := string(body)
1290 expectedJSURL := "https://korap.ids-mannheim.de/plugin/termmapper/main-config-mapper/query"
1291 assert.Contains(t, htmlContent, "'service' : '"+expectedJSURL+"'")
1292}
Akron80067202025-06-06 14:16:25 +02001293
1294func TestGenerateKalamarPluginHTMLWithURLJoining(t *testing.T) {
1295 tests := []struct {
1296 name string
1297 serviceURL string
1298 mapID string
1299 expected string
1300 }{
1301 {
1302 name: "Service URL without trailing slash",
1303 serviceURL: "https://example.com/plugin/termmapper",
1304 mapID: "test-mapper",
1305 expected: "'service' : 'https://example.com/plugin/termmapper/test-mapper/query'",
1306 },
1307 {
1308 name: "Service URL with trailing slash",
1309 serviceURL: "https://example.com/plugin/termmapper/",
1310 mapID: "test-mapper",
1311 expected: "'service' : 'https://example.com/plugin/termmapper/test-mapper/query'",
1312 },
1313 {
1314 name: "Map ID with leading slash",
1315 serviceURL: "https://example.com/plugin/termmapper",
1316 mapID: "/test-mapper",
1317 expected: "'service' : 'https://example.com/plugin/termmapper/test-mapper/query'",
1318 },
1319 {
1320 name: "Both with slashes",
1321 serviceURL: "https://example.com/plugin/termmapper/",
1322 mapID: "/test-mapper",
1323 expected: "'service' : 'https://example.com/plugin/termmapper/test-mapper/query'",
1324 },
1325 {
1326 name: "Complex map ID",
1327 serviceURL: "https://example.com/api/v1/",
1328 mapID: "complex-mapper-name_123",
1329 expected: "'service' : 'https://example.com/api/v1/complex-mapper-name_123/query'",
1330 },
1331 }
1332
1333 for _, tt := range tests {
1334 t.Run(tt.name, func(t *testing.T) {
1335 data := TemplateData{
1336 Title: "Test Mapper",
1337 Version: "1.0.0",
1338 Hash: "abcd1234",
1339 Date: "2024-01-01",
1340 Description: "Test description",
1341 Server: "https://example.com/",
1342 SDK: "https://example.com/js/sdk.js",
1343 ServiceURL: tt.serviceURL,
1344 MapID: tt.mapID,
1345 Mappings: []TemplateMapping{},
1346 }
1347
1348 html := generateKalamarPluginHTML(data)
1349 assert.Contains(t, html, tt.expected)
1350 })
1351 }
1352}