blob: 80c7f4bd5626dc1062c83360e5fc9d097937b5b2 [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"
Akroncb51f812025-06-30 15:24:20 +020012 "strings"
Akron49ceeb42025-05-23 17:46:01 +020013 "testing"
14
Akrona00d4752025-05-26 17:34:36 +020015 tmconfig "github.com/KorAP/KoralPipe-TermMapper/config"
Akronfa55bb22025-05-26 15:10:42 +020016 "github.com/KorAP/KoralPipe-TermMapper/mapper"
Akron49ceeb42025-05-23 17:46:01 +020017 "github.com/gofiber/fiber/v2"
18 "github.com/stretchr/testify/assert"
19 "github.com/stretchr/testify/require"
20)
21
22func TestTransformEndpoint(t *testing.T) {
Akrona00d4752025-05-26 17:34:36 +020023 // Create test mapping list
24 mappingList := tmconfig.MappingList{
25 ID: "test-mapper",
26 FoundryA: "opennlp",
27 LayerA: "p",
28 FoundryB: "upos",
29 LayerB: "p",
30 Mappings: []tmconfig.MappingRule{
31 "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]",
32 "[DET] <> [opennlp/p=DET]",
33 },
34 }
Akron49ceeb42025-05-23 17:46:01 +020035
36 // Create mapper
Akrona00d4752025-05-26 17:34:36 +020037 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
Akron49ceeb42025-05-23 17:46:01 +020038 require.NoError(t, err)
39
Akron40aaa632025-06-03 17:57:52 +020040 // Create mock config for testing
Akron06d21f02025-06-04 14:36:07 +020041 mockConfig := &tmconfig.MappingConfig{
Akron40aaa632025-06-03 17:57:52 +020042 Lists: []tmconfig.MappingList{mappingList},
43 }
44
Akron49ceeb42025-05-23 17:46:01 +020045 // Create fiber app
46 app := fiber.New()
Akron40aaa632025-06-03 17:57:52 +020047 setupRoutes(app, m, mockConfig)
Akron49ceeb42025-05-23 17:46:01 +020048
49 tests := []struct {
50 name string
51 mapID string
52 direction string
53 foundryA string
54 foundryB string
55 layerA string
56 layerB string
57 input string
58 expectedCode int
59 expectedBody string
60 expectedError string
61 }{
62 {
63 name: "Simple A to B mapping",
64 mapID: "test-mapper",
65 direction: "atob",
66 input: `{
67 "@type": "koral:token",
68 "wrap": {
69 "@type": "koral:term",
70 "foundry": "opennlp",
71 "key": "PIDAT",
72 "layer": "p",
73 "match": "match:eq"
74 }
75 }`,
76 expectedCode: http.StatusOK,
77 expectedBody: `{
78 "@type": "koral:token",
79 "wrap": {
80 "@type": "koral:termGroup",
81 "operands": [
82 {
83 "@type": "koral:term",
84 "foundry": "opennlp",
85 "key": "PIDAT",
86 "layer": "p",
87 "match": "match:eq"
88 },
89 {
90 "@type": "koral:term",
91 "foundry": "opennlp",
92 "key": "AdjType",
93 "layer": "p",
94 "match": "match:eq",
95 "value": "Pdt"
96 }
97 ],
98 "relation": "relation:and"
99 }
100 }`,
101 },
102 {
103 name: "B to A mapping",
104 mapID: "test-mapper",
105 direction: "btoa",
106 input: `{
107 "@type": "koral:token",
108 "wrap": {
109 "@type": "koral:termGroup",
110 "operands": [
111 {
112 "@type": "koral:term",
113 "foundry": "opennlp",
114 "key": "PIDAT",
115 "layer": "p",
116 "match": "match:eq"
117 },
118 {
119 "@type": "koral:term",
120 "foundry": "opennlp",
121 "key": "AdjType",
122 "layer": "p",
123 "match": "match:eq",
124 "value": "Pdt"
125 }
126 ],
127 "relation": "relation:and"
128 }
129 }`,
130 expectedCode: http.StatusOK,
131 expectedBody: `{
132 "@type": "koral:token",
133 "wrap": {
134 "@type": "koral:term",
135 "foundry": "opennlp",
136 "key": "PIDAT",
137 "layer": "p",
138 "match": "match:eq"
139 }
140 }`,
141 },
142 {
143 name: "Mapping with foundry override",
144 mapID: "test-mapper",
145 direction: "atob",
146 foundryB: "custom",
147 input: `{
148 "@type": "koral:token",
149 "wrap": {
150 "@type": "koral:term",
151 "foundry": "opennlp",
152 "key": "PIDAT",
153 "layer": "p",
154 "match": "match:eq"
155 }
156 }`,
157 expectedCode: http.StatusOK,
158 expectedBody: `{
159 "@type": "koral:token",
160 "wrap": {
161 "@type": "koral:termGroup",
162 "operands": [
163 {
164 "@type": "koral:term",
165 "foundry": "custom",
166 "key": "PIDAT",
167 "layer": "p",
168 "match": "match:eq"
169 },
170 {
171 "@type": "koral:term",
172 "foundry": "custom",
173 "key": "AdjType",
174 "layer": "p",
175 "match": "match:eq",
176 "value": "Pdt"
177 }
178 ],
179 "relation": "relation:and"
180 }
181 }`,
182 },
183 {
184 name: "Invalid mapping ID",
185 mapID: "nonexistent",
186 direction: "atob",
187 input: `{"@type": "koral:token"}`,
188 expectedCode: http.StatusInternalServerError,
189 expectedError: "mapping list with ID nonexistent not found",
190 },
191 {
192 name: "Invalid direction",
193 mapID: "test-mapper",
194 direction: "invalid",
195 input: `{"@type": "koral:token"}`,
196 expectedCode: http.StatusBadRequest,
197 expectedError: "invalid direction, must be 'atob' or 'btoa'",
198 },
199 {
200 name: "Invalid JSON",
201 mapID: "test-mapper",
202 direction: "atob",
203 input: `invalid json`,
204 expectedCode: http.StatusBadRequest,
205 expectedError: "invalid JSON in request body",
206 },
207 }
208
209 for _, tt := range tests {
210 t.Run(tt.name, func(t *testing.T) {
211 // Build URL with query parameters
212 url := "/" + tt.mapID + "/query"
213 if tt.direction != "" {
214 url += "?dir=" + tt.direction
215 }
216 if tt.foundryA != "" {
217 url += "&foundryA=" + tt.foundryA
218 }
219 if tt.foundryB != "" {
220 url += "&foundryB=" + tt.foundryB
221 }
222 if tt.layerA != "" {
223 url += "&layerA=" + tt.layerA
224 }
225 if tt.layerB != "" {
226 url += "&layerB=" + tt.layerB
227 }
228
229 // Make request
230 req := httptest.NewRequest(http.MethodPost, url, bytes.NewBufferString(tt.input))
231 req.Header.Set("Content-Type", "application/json")
232 resp, err := app.Test(req)
233 require.NoError(t, err)
234 defer resp.Body.Close()
235
236 // Check status code
237 assert.Equal(t, tt.expectedCode, resp.StatusCode)
238
239 // Read response body
240 body, err := io.ReadAll(resp.Body)
241 require.NoError(t, err)
242
243 if tt.expectedError != "" {
244 // Check error message
245 var errResp fiber.Map
246 err = json.Unmarshal(body, &errResp)
247 require.NoError(t, err)
248 assert.Equal(t, tt.expectedError, errResp["error"])
249 } else {
250 // Compare JSON responses
Akron121c66e2025-06-02 16:34:05 +0200251 var expected, actual any
Akron49ceeb42025-05-23 17:46:01 +0200252 err = json.Unmarshal([]byte(tt.expectedBody), &expected)
253 require.NoError(t, err)
254 err = json.Unmarshal(body, &actual)
255 require.NoError(t, err)
256 assert.Equal(t, expected, actual)
257 }
258 })
259 }
260}
261
Akron4de47a92025-06-27 11:58:11 +0200262func TestResponseTransformEndpoint(t *testing.T) {
263 // Create test mapping list
264 mappingList := tmconfig.MappingList{
265 ID: "test-response-mapper",
266 FoundryA: "marmot",
267 LayerA: "m",
268 FoundryB: "opennlp",
269 LayerB: "p",
270 Mappings: []tmconfig.MappingRule{
271 "[gender=masc] <> [p=M & m=M]",
272 },
273 }
274
275 // Create mapper
276 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
277 require.NoError(t, err)
278
279 // Create mock config for testing
280 mockConfig := &tmconfig.MappingConfig{
281 Lists: []tmconfig.MappingList{mappingList},
282 }
283
284 // Create fiber app
285 app := fiber.New()
286 setupRoutes(app, m, mockConfig)
287
288 tests := []struct {
289 name string
290 mapID string
291 direction string
292 foundryA string
293 foundryB string
294 layerA string
295 layerB string
296 input string
297 expectedCode int
298 expectedBody string
299 expectedError string
300 }{
301 {
302 name: "Simple response mapping with snippet transformation",
303 mapID: "test-response-mapper",
304 direction: "atob",
305 input: `{
306 "snippet": "<span title=\"marmot/m:gender:masc\">Der</span>"
307 }`,
308 expectedCode: http.StatusOK,
309 expectedBody: `{
310 "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>"
311 }`,
312 },
313 {
314 name: "Response with no snippet field",
315 mapID: "test-response-mapper",
316 direction: "atob",
317 input: `{
318 "@type": "koral:response",
319 "meta": {
320 "version": "Krill-0.64.1"
321 }
322 }`,
323 expectedCode: http.StatusOK,
324 expectedBody: `{
325 "@type": "koral:response",
326 "meta": {
327 "version": "Krill-0.64.1"
328 }
329 }`,
330 },
331 {
332 name: "Response with null snippet",
333 mapID: "test-response-mapper",
334 direction: "atob",
335 input: `{
336 "snippet": null
337 }`,
338 expectedCode: http.StatusOK,
339 expectedBody: `{
340 "snippet": null
341 }`,
342 },
343 {
344 name: "Response with non-string snippet",
345 mapID: "test-response-mapper",
346 direction: "atob",
347 input: `{
348 "snippet": 123
349 }`,
350 expectedCode: http.StatusOK,
351 expectedBody: `{
352 "snippet": 123
353 }`,
354 },
355 {
356 name: "Response mapping with foundry override",
357 mapID: "test-response-mapper",
358 direction: "atob",
359 foundryB: "custom",
360 input: `{
361 "snippet": "<span title=\"marmot/m:gender:masc\">Der</span>"
362 }`,
363 expectedCode: http.StatusOK,
364 expectedBody: `{
365 "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>"
366 }`,
367 },
368 {
369 name: "Invalid mapping ID for response",
370 mapID: "nonexistent",
371 direction: "atob",
372 input: `{"snippet": "<span>test</span>"}`,
373 expectedCode: http.StatusInternalServerError,
374 expectedError: "mapping list with ID nonexistent not found",
375 },
376 {
377 name: "Invalid direction for response",
378 mapID: "test-response-mapper",
379 direction: "invalid",
380 input: `{"snippet": "<span>test</span>"}`,
381 expectedCode: http.StatusBadRequest,
382 expectedError: "invalid direction, must be 'atob' or 'btoa'",
383 },
384 {
385 name: "Invalid JSON for response",
386 mapID: "test-response-mapper",
387 direction: "atob",
388 input: `{invalid json}`,
389 expectedCode: http.StatusBadRequest,
390 expectedError: "invalid JSON in request body",
391 },
392 }
393
394 for _, tt := range tests {
395 t.Run(tt.name, func(t *testing.T) {
396 // Build URL with query parameters
397 url := "/" + tt.mapID + "/response"
398 if tt.direction != "" {
399 url += "?dir=" + tt.direction
400 }
401 if tt.foundryA != "" {
402 url += "&foundryA=" + tt.foundryA
403 }
404 if tt.foundryB != "" {
405 url += "&foundryB=" + tt.foundryB
406 }
407 if tt.layerA != "" {
408 url += "&layerA=" + tt.layerA
409 }
410 if tt.layerB != "" {
411 url += "&layerB=" + tt.layerB
412 }
413
414 // Make request
415 req := httptest.NewRequest(http.MethodPost, url, bytes.NewBufferString(tt.input))
416 req.Header.Set("Content-Type", "application/json")
417 resp, err := app.Test(req)
418 require.NoError(t, err)
419 defer resp.Body.Close()
420
421 // Check status code
422 assert.Equal(t, tt.expectedCode, resp.StatusCode)
423
424 // Read response body
425 body, err := io.ReadAll(resp.Body)
426 require.NoError(t, err)
427
428 if tt.expectedError != "" {
429 // Check error message
430 var errResp fiber.Map
431 err = json.Unmarshal(body, &errResp)
432 require.NoError(t, err)
433 assert.Equal(t, tt.expectedError, errResp["error"])
434 } else {
435 // Compare JSON responses
436 var expected, actual any
437 err = json.Unmarshal([]byte(tt.expectedBody), &expected)
438 require.NoError(t, err)
439 err = json.Unmarshal(body, &actual)
440 require.NoError(t, err)
441 assert.Equal(t, expected, actual)
442 }
443 })
444 }
445}
446
Akron49ceeb42025-05-23 17:46:01 +0200447func TestHealthEndpoint(t *testing.T) {
Akrona00d4752025-05-26 17:34:36 +0200448 // Create test mapping list
449 mappingList := tmconfig.MappingList{
450 ID: "test-mapper",
451 Mappings: []tmconfig.MappingRule{
452 "[A] <> [B]",
453 },
454 }
Akron49ceeb42025-05-23 17:46:01 +0200455
Akrona00d4752025-05-26 17:34:36 +0200456 // Create mapper
457 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
Akron49ceeb42025-05-23 17:46:01 +0200458 require.NoError(t, err)
459
Akron40aaa632025-06-03 17:57:52 +0200460 // Create mock config for testing
Akron06d21f02025-06-04 14:36:07 +0200461 mockConfig := &tmconfig.MappingConfig{
Akron40aaa632025-06-03 17:57:52 +0200462 Lists: []tmconfig.MappingList{mappingList},
463 }
464
Akron49ceeb42025-05-23 17:46:01 +0200465 // Create fiber app
466 app := fiber.New()
Akron40aaa632025-06-03 17:57:52 +0200467 setupRoutes(app, m, mockConfig)
Akron49ceeb42025-05-23 17:46:01 +0200468
469 // Test health endpoint
470 req := httptest.NewRequest(http.MethodGet, "/health", nil)
471 resp, err := app.Test(req)
472 require.NoError(t, err)
473 defer resp.Body.Close()
474
475 assert.Equal(t, http.StatusOK, resp.StatusCode)
476 body, err := io.ReadAll(resp.Body)
477 require.NoError(t, err)
478 assert.Equal(t, "OK", string(body))
Akron40aaa632025-06-03 17:57:52 +0200479
Akronc471c0a2025-06-04 11:56:22 +0200480 req = httptest.NewRequest(http.MethodGet, "/", nil)
Akron40aaa632025-06-03 17:57:52 +0200481 resp, err = app.Test(req)
482 require.NoError(t, err)
483 defer resp.Body.Close()
484
485 assert.Equal(t, http.StatusOK, resp.StatusCode)
486 body, err = io.ReadAll(resp.Body)
487 require.NoError(t, err)
Akronfc77b5e2025-06-04 11:44:43 +0200488 assert.Contains(t, string(body), "KoralPipe-TermMapper")
Akron40aaa632025-06-03 17:57:52 +0200489
Akron49ceeb42025-05-23 17:46:01 +0200490}
Akron06d21f02025-06-04 14:36:07 +0200491
492func TestKalamarPluginWithCustomSdkAndServer(t *testing.T) {
493 // Create test mapping list
494 mappingList := tmconfig.MappingList{
495 ID: "test-mapper",
496 Mappings: []tmconfig.MappingRule{
497 "[A] <> [B]",
498 },
499 }
500
501 // Create mapper
502 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
503 require.NoError(t, err)
504
505 tests := []struct {
506 name string
507 customSDK string
508 customServer string
509 expectedSDK string
510 expectedServer string
511 }{
512 {
513 name: "Custom SDK and Server values",
514 customSDK: "https://custom.example.com/custom-sdk.js",
515 customServer: "https://custom.example.com/",
516 expectedSDK: "https://custom.example.com/custom-sdk.js",
517 expectedServer: "https://custom.example.com/",
518 },
519 {
520 name: "Only custom SDK value",
521 customSDK: "https://custom.example.com/custom-sdk.js",
522 customServer: "https://korap.ids-mannheim.de/", // defaults applied during parsing
523 expectedSDK: "https://custom.example.com/custom-sdk.js",
524 expectedServer: "https://korap.ids-mannheim.de/",
525 },
526 {
527 name: "Only custom Server value",
528 customSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // defaults applied during parsing
529 customServer: "https://custom.example.com/",
530 expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js",
531 expectedServer: "https://custom.example.com/",
532 },
533 {
534 name: "Defaults applied during parsing",
535 customSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // defaults applied during parsing
536 customServer: "https://korap.ids-mannheim.de/", // defaults applied during parsing
537 expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js",
538 expectedServer: "https://korap.ids-mannheim.de/",
539 },
540 }
541
542 for _, tt := range tests {
543 t.Run(tt.name, func(t *testing.T) {
544 // Create mock config with custom values
545 mockConfig := &tmconfig.MappingConfig{
546 SDK: tt.customSDK,
547 Server: tt.customServer,
548 Lists: []tmconfig.MappingList{mappingList},
549 }
550
551 // Create fiber app
552 app := fiber.New()
553 setupRoutes(app, m, mockConfig)
554
555 // Test Kalamar plugin endpoint
556 req := httptest.NewRequest(http.MethodGet, "/", nil)
557 resp, err := app.Test(req)
558 require.NoError(t, err)
559 defer resp.Body.Close()
560
561 assert.Equal(t, http.StatusOK, resp.StatusCode)
562 body, err := io.ReadAll(resp.Body)
563 require.NoError(t, err)
564
565 htmlContent := string(body)
566
567 // Check that the HTML contains the expected SDK and Server values
568 assert.Contains(t, htmlContent, `src="`+tt.expectedSDK+`"`)
569 assert.Contains(t, htmlContent, `data-server="`+tt.expectedServer+`"`)
570
571 // Ensure it's still a valid HTML page
572 assert.Contains(t, htmlContent, "KoralPipe-TermMapper")
573 assert.Contains(t, htmlContent, "<!DOCTYPE html>")
574 })
575 }
576}
Akrone1cff7c2025-06-04 18:43:32 +0200577
578func TestMultipleMappingFiles(t *testing.T) {
579 // Create test mapping files
580 mappingFile1Content := `
581id: test-mapper-1
582foundryA: opennlp
583layerA: p
584foundryB: upos
585layerB: p
586mappings:
587 - "[PIDAT] <> [DET & AdjType=Pdt]"
588 - "[PAV] <> [ADV & PronType=Dem]"
589`
590 mappingFile1, err := os.CreateTemp("", "mapping1-*.yaml")
591 require.NoError(t, err)
592 defer os.Remove(mappingFile1.Name())
593
594 _, err = mappingFile1.WriteString(mappingFile1Content)
595 require.NoError(t, err)
596 err = mappingFile1.Close()
597 require.NoError(t, err)
598
599 mappingFile2Content := `
600id: test-mapper-2
601foundryA: stts
602layerA: p
603foundryB: upos
604layerB: p
605mappings:
606 - "[DET] <> [PRON]"
607 - "[ADJ] <> [NOUN]"
608`
609 mappingFile2, err := os.CreateTemp("", "mapping2-*.yaml")
610 require.NoError(t, err)
611 defer os.Remove(mappingFile2.Name())
612
613 _, err = mappingFile2.WriteString(mappingFile2Content)
614 require.NoError(t, err)
615 err = mappingFile2.Close()
616 require.NoError(t, err)
617
618 // Load configuration using multiple mapping files
619 config, err := tmconfig.LoadFromSources("", []string{mappingFile1.Name(), mappingFile2.Name()})
620 require.NoError(t, err)
621
622 // Create mapper
623 m, err := mapper.NewMapper(config.Lists)
624 require.NoError(t, err)
625
626 // Create fiber app
627 app := fiber.New()
628 setupRoutes(app, m, config)
629
630 // Test that both mappers work
631 testCases := []struct {
632 name string
633 mapID string
634 input string
635 expectGroup bool
636 expectedKey string
637 }{
638 {
639 name: "test-mapper-1 with complex mapping",
640 mapID: "test-mapper-1",
641 input: `{
642 "@type": "koral:token",
643 "wrap": {
644 "@type": "koral:term",
645 "foundry": "opennlp",
646 "key": "PIDAT",
647 "layer": "p",
648 "match": "match:eq"
649 }
650 }`,
651 expectGroup: true, // This mapping creates a termGroup because of "&"
652 expectedKey: "DET", // The first operand should be DET
653 },
654 {
655 name: "test-mapper-2 with simple mapping",
656 mapID: "test-mapper-2",
657 input: `{
658 "@type": "koral:token",
659 "wrap": {
660 "@type": "koral:term",
661 "foundry": "stts",
662 "key": "DET",
663 "layer": "p",
664 "match": "match:eq"
665 }
666 }`,
667 expectGroup: false, // This mapping creates a simple term
668 expectedKey: "PRON",
669 },
670 }
671
672 for _, tc := range testCases {
673 t.Run(tc.name, func(t *testing.T) {
674 req := httptest.NewRequest(http.MethodPost, "/"+tc.mapID+"/query?dir=atob", bytes.NewBufferString(tc.input))
675 req.Header.Set("Content-Type", "application/json")
676
677 resp, err := app.Test(req)
678 require.NoError(t, err)
679 defer resp.Body.Close()
680
681 assert.Equal(t, http.StatusOK, resp.StatusCode)
682
683 var result map[string]interface{}
684 err = json.NewDecoder(resp.Body).Decode(&result)
685 require.NoError(t, err)
686
687 // Check that the mapping was applied
688 wrap := result["wrap"].(map[string]interface{})
689 if tc.expectGroup {
690 // For complex mappings, check the first operand
691 assert.Equal(t, "koral:termGroup", wrap["@type"])
692 operands := wrap["operands"].([]interface{})
693 require.Greater(t, len(operands), 0)
694 firstOperand := operands[0].(map[string]interface{})
695 assert.Equal(t, tc.expectedKey, firstOperand["key"])
696 } else {
697 // For simple mappings, check the key directly
698 assert.Equal(t, "koral:term", wrap["@type"])
699 assert.Equal(t, tc.expectedKey, wrap["key"])
700 }
701 })
702 }
703}
704
705func TestCombinedConfigAndMappingFiles(t *testing.T) {
706 // Create main config file
707 mainConfigContent := `
708sdk: "https://custom.example.com/sdk.js"
709server: "https://custom.example.com/"
710lists:
711- id: main-mapper
712 foundryA: opennlp
713 layerA: p
714 mappings:
715 - "[A] <> [B]"
716`
717 mainConfigFile, err := os.CreateTemp("", "main-config-*.yaml")
718 require.NoError(t, err)
719 defer os.Remove(mainConfigFile.Name())
720
721 _, err = mainConfigFile.WriteString(mainConfigContent)
722 require.NoError(t, err)
723 err = mainConfigFile.Close()
724 require.NoError(t, err)
725
726 // Create individual mapping file
727 mappingFileContent := `
728id: additional-mapper
729foundryA: stts
730layerA: p
731mappings:
732 - "[C] <> [D]"
733`
734 mappingFile, err := os.CreateTemp("", "mapping-*.yaml")
735 require.NoError(t, err)
736 defer os.Remove(mappingFile.Name())
737
738 _, err = mappingFile.WriteString(mappingFileContent)
739 require.NoError(t, err)
740 err = mappingFile.Close()
741 require.NoError(t, err)
742
743 // Load configuration from both sources
744 config, err := tmconfig.LoadFromSources(mainConfigFile.Name(), []string{mappingFile.Name()})
745 require.NoError(t, err)
746
747 // Verify that both mappers are loaded
748 require.Len(t, config.Lists, 2)
749
750 ids := make([]string, len(config.Lists))
751 for i, list := range config.Lists {
752 ids[i] = list.ID
753 }
754 assert.Contains(t, ids, "main-mapper")
755 assert.Contains(t, ids, "additional-mapper")
756
757 // Verify custom SDK and server are preserved from main config
758 assert.Equal(t, "https://custom.example.com/sdk.js", config.SDK)
759 assert.Equal(t, "https://custom.example.com/", config.Server)
760
761 // Create mapper and test it works
762 m, err := mapper.NewMapper(config.Lists)
763 require.NoError(t, err)
764 require.NotNil(t, m)
765}
Akron14678dc2025-06-05 13:01:38 +0200766
767func TestExpandGlobs(t *testing.T) {
768 // Create a temporary directory for test files
769 tempDir, err := os.MkdirTemp("", "glob_test_*")
770 require.NoError(t, err)
771 defer os.RemoveAll(tempDir)
772
773 // Create test files with .yaml and .yml extensions
774 testFiles := []struct {
775 name string
776 content string
777 }{
778 {
779 name: "mapper1.yaml",
780 content: `
781id: test-mapper-1
782mappings:
783 - "[A] <> [B]"
784`,
785 },
786 {
787 name: "mapper2.yml",
788 content: `
789id: test-mapper-2
790mappings:
791 - "[C] <> [D]"
792`,
793 },
794 {
795 name: "mapper3.yaml",
796 content: `
797id: test-mapper-3
798mappings:
799 - "[E] <> [F]"
800`,
801 },
802 {
803 name: "other.txt",
804 content: "not a yaml file",
805 },
806 }
807
808 for _, file := range testFiles {
809 filePath := filepath.Join(tempDir, file.name)
810 err := os.WriteFile(filePath, []byte(file.content), 0644)
811 require.NoError(t, err)
812 }
813
814 tests := []struct {
815 name string
816 patterns []string
817 expected []string
818 expectErr bool
819 }{
820 {
821 name: "Single literal file",
822 patterns: []string{filepath.Join(tempDir, "mapper1.yaml")},
823 expected: []string{filepath.Join(tempDir, "mapper1.yaml")},
824 },
825 {
826 name: "Multiple literal files",
827 patterns: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper2.yml")},
828 expected: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper2.yml")},
829 },
830 {
831 name: "Glob pattern for yaml files",
832 patterns: []string{filepath.Join(tempDir, "*.yaml")},
833 expected: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper3.yaml")},
834 },
835 {
836 name: "Glob pattern for yml files",
837 patterns: []string{filepath.Join(tempDir, "*.yml")},
838 expected: []string{filepath.Join(tempDir, "mapper2.yml")},
839 },
840 {
841 name: "Glob pattern for all yaml/yml files",
842 patterns: []string{filepath.Join(tempDir, "*.y*ml")},
843 expected: []string{
844 filepath.Join(tempDir, "mapper1.yaml"),
845 filepath.Join(tempDir, "mapper2.yml"),
846 filepath.Join(tempDir, "mapper3.yaml"),
847 },
848 },
849 {
850 name: "Mixed literal and glob",
851 patterns: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "*.yml")},
852 expected: []string{filepath.Join(tempDir, "mapper1.yaml"), filepath.Join(tempDir, "mapper2.yml")},
853 },
854 {
855 name: "No matches - treats as literal",
856 patterns: []string{filepath.Join(tempDir, "nonexistent*.yaml")},
857 expected: []string{filepath.Join(tempDir, "nonexistent*.yaml")},
858 },
859 {
860 name: "Invalid glob pattern",
861 patterns: []string{filepath.Join(tempDir, "[")},
862 expectErr: true,
863 },
864 }
865
866 for _, tt := range tests {
867 t.Run(tt.name, func(t *testing.T) {
868 result, err := expandGlobs(tt.patterns)
869
870 if tt.expectErr {
871 assert.Error(t, err)
872 return
873 }
874
875 require.NoError(t, err)
876
877 // Sort both slices for comparison since glob results may not be in consistent order
878 sort.Strings(result)
879 sort.Strings(tt.expected)
880
881 assert.Equal(t, tt.expected, result)
882 })
883 }
884}
885
886func TestGlobMappingFileLoading(t *testing.T) {
887 // Create a temporary directory for test files
888 tempDir, err := os.MkdirTemp("", "glob_mapping_test_*")
889 require.NoError(t, err)
890 defer os.RemoveAll(tempDir)
891
892 // Create test mapping files
893 testFiles := []struct {
894 name string
895 content string
896 }{
897 {
898 name: "pos-mapper.yaml",
899 content: `
900id: pos-mapper
901foundryA: opennlp
902layerA: p
903foundryB: upos
904layerB: p
905mappings:
906 - "[PIDAT] <> [DET]"
907 - "[ADJA] <> [ADJ]"
908`,
909 },
910 {
911 name: "ner-mapper.yml",
912 content: `
913id: ner-mapper
914foundryA: opennlp
915layerA: ner
916foundryB: upos
917layerB: ner
918mappings:
919 - "[PER] <> [PERSON]"
920 - "[LOC] <> [LOCATION]"
921`,
922 },
923 {
924 name: "special-mapper.yaml",
925 content: `
926id: special-mapper
927mappings:
928 - "[X] <> [Y]"
929`,
930 },
931 }
932
933 for _, file := range testFiles {
934 filePath := filepath.Join(tempDir, file.name)
935 err := os.WriteFile(filePath, []byte(file.content), 0644)
936 require.NoError(t, err)
937 }
938
939 tests := []struct {
940 name string
941 configFile string
942 mappingPattern string
943 expectedIDs []string
944 }{
945 {
946 name: "Load all yaml files",
947 mappingPattern: filepath.Join(tempDir, "*.yaml"),
948 expectedIDs: []string{"pos-mapper", "special-mapper"},
949 },
950 {
951 name: "Load all yml files",
952 mappingPattern: filepath.Join(tempDir, "*.yml"),
953 expectedIDs: []string{"ner-mapper"},
954 },
955 {
956 name: "Load all yaml/yml files",
957 mappingPattern: filepath.Join(tempDir, "*-mapper.y*ml"),
958 expectedIDs: []string{"pos-mapper", "ner-mapper", "special-mapper"},
959 },
960 }
961
962 for _, tt := range tests {
963 t.Run(tt.name, func(t *testing.T) {
964 // Expand the glob pattern
965 expanded, err := expandGlobs([]string{tt.mappingPattern})
966 require.NoError(t, err)
967
968 // Load configuration using the expanded file list
969 config, err := tmconfig.LoadFromSources(tt.configFile, expanded)
970 require.NoError(t, err)
971
972 // Verify that the expected mappers are loaded
973 require.Len(t, config.Lists, len(tt.expectedIDs))
974
975 actualIDs := make([]string, len(config.Lists))
976 for i, list := range config.Lists {
977 actualIDs[i] = list.ID
978 }
979
980 // Sort both slices for comparison
981 sort.Strings(actualIDs)
982 sort.Strings(tt.expectedIDs)
983 assert.Equal(t, tt.expectedIDs, actualIDs)
984
985 // Create mapper to ensure all loaded configs are valid
986 m, err := mapper.NewMapper(config.Lists)
987 require.NoError(t, err)
988 require.NotNil(t, m)
989 })
990 }
991}
992
993func TestGlobErrorHandling(t *testing.T) {
994 tests := []struct {
995 name string
996 patterns []string
997 expectErr bool
998 }{
999 {
1000 name: "Empty patterns",
1001 patterns: []string{},
1002 expectErr: false, // Should return empty slice, no error
1003 },
1004 {
1005 name: "Invalid glob pattern",
1006 patterns: []string{"["},
1007 expectErr: true,
1008 },
1009 {
1010 name: "Valid and invalid mixed",
1011 patterns: []string{"valid.yaml", "["},
1012 expectErr: true,
1013 },
1014 }
1015
1016 for _, tt := range tests {
1017 t.Run(tt.name, func(t *testing.T) {
1018 result, err := expandGlobs(tt.patterns)
1019
1020 if tt.expectErr {
1021 assert.Error(t, err)
1022 assert.Nil(t, result)
1023 } else {
1024 assert.NoError(t, err)
1025 if len(tt.patterns) == 0 {
1026 assert.Empty(t, result)
1027 }
1028 }
1029 })
1030 }
1031}
1032
1033func TestGlobIntegrationWithTestData(t *testing.T) {
1034 // Test that our glob functionality works with the actual testdata files
1035 // This ensures the feature works end-to-end in a realistic scenario
1036
1037 // Expand glob pattern for the example mapper files
1038 expanded, err := expandGlobs([]string{"../../testdata/example-mapper*.yaml"})
1039 require.NoError(t, err)
1040
1041 // Should match exactly the two mapper files
1042 sort.Strings(expanded)
1043 assert.Len(t, expanded, 2)
1044 assert.Contains(t, expanded[0], "example-mapper1.yaml")
1045 assert.Contains(t, expanded[1], "example-mapper2.yaml")
1046
1047 // Load configuration using the expanded files
1048 config, err := tmconfig.LoadFromSources("", expanded)
1049 require.NoError(t, err)
1050
1051 // Verify that both mappers are loaded correctly
1052 require.Len(t, config.Lists, 2)
1053
1054 // Get the IDs to verify they match the expected ones
1055 actualIDs := make([]string, len(config.Lists))
1056 for i, list := range config.Lists {
1057 actualIDs[i] = list.ID
1058 }
1059 sort.Strings(actualIDs)
1060
1061 expectedIDs := []string{"example-mapper-1", "example-mapper-2"}
1062 assert.Equal(t, expectedIDs, actualIDs)
1063
1064 // Create mapper to ensure everything works
1065 m, err := mapper.NewMapper(config.Lists)
1066 require.NoError(t, err)
1067 require.NotNil(t, m)
1068
1069 // Test that the mapper actually works with a real transformation
1070 app := fiber.New()
1071 setupRoutes(app, m, config)
1072
1073 // Test a transformation from example-mapper-1
1074 testInput := `{
1075 "@type": "koral:token",
1076 "wrap": {
1077 "@type": "koral:term",
1078 "foundry": "opennlp",
1079 "key": "PIDAT",
1080 "layer": "p",
1081 "match": "match:eq"
1082 }
1083 }`
1084
1085 req := httptest.NewRequest(http.MethodPost, "/example-mapper-1/query?dir=atob", bytes.NewBufferString(testInput))
1086 req.Header.Set("Content-Type", "application/json")
1087
1088 resp, err := app.Test(req)
1089 require.NoError(t, err)
1090 defer resp.Body.Close()
1091
1092 assert.Equal(t, http.StatusOK, resp.StatusCode)
1093
1094 var result map[string]interface{}
1095 err = json.NewDecoder(resp.Body).Decode(&result)
1096 require.NoError(t, err)
1097
1098 // Verify the transformation was applied
1099 wrap := result["wrap"].(map[string]interface{})
1100 assert.Equal(t, "koral:termGroup", wrap["@type"])
1101 operands := wrap["operands"].([]interface{})
1102 require.Greater(t, len(operands), 0)
1103 firstOperand := operands[0].(map[string]interface{})
1104 assert.Equal(t, "DET", firstOperand["key"])
1105}
Akron2ac2ec02025-06-05 15:26:42 +02001106
1107func TestConfigurableServiceURL(t *testing.T) {
1108 // Create test mapping list
1109 mappingList := tmconfig.MappingList{
1110 ID: "test-mapper",
1111 Mappings: []tmconfig.MappingRule{
1112 "[A] <> [B]",
1113 },
1114 }
1115
1116 tests := []struct {
1117 name string
1118 customServiceURL string
1119 expectedServiceURL string
1120 }{
1121 {
1122 name: "Custom service URL",
1123 customServiceURL: "https://custom.example.com/plugin/termmapper",
1124 expectedServiceURL: "https://custom.example.com/plugin/termmapper",
1125 },
1126 {
1127 name: "Default service URL when not specified",
1128 customServiceURL: "", // Will use default
1129 expectedServiceURL: "https://korap.ids-mannheim.de/plugin/termmapper",
1130 },
1131 {
1132 name: "Custom service URL with different path",
1133 customServiceURL: "https://my-server.org/api/v1/termmapper",
1134 expectedServiceURL: "https://my-server.org/api/v1/termmapper",
1135 },
1136 }
1137
1138 for _, tt := range tests {
1139 t.Run(tt.name, func(t *testing.T) {
1140 // Create mapper
1141 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
1142 require.NoError(t, err)
1143
1144 // Create mock config with custom service URL
1145 mockConfig := &tmconfig.MappingConfig{
1146 ServiceURL: tt.customServiceURL,
1147 Lists: []tmconfig.MappingList{mappingList},
1148 }
1149
1150 // Apply defaults to simulate the real loading process
1151 tmconfig.ApplyDefaults(mockConfig)
1152
1153 // Create fiber app
1154 app := fiber.New()
1155 setupRoutes(app, m, mockConfig)
1156
1157 // Test Kalamar plugin endpoint with a specific mapID
1158 req := httptest.NewRequest(http.MethodGet, "/test-mapper", nil)
1159 resp, err := app.Test(req)
1160 require.NoError(t, err)
1161 defer resp.Body.Close()
1162
1163 assert.Equal(t, http.StatusOK, resp.StatusCode)
1164 body, err := io.ReadAll(resp.Body)
1165 require.NoError(t, err)
1166
1167 htmlContent := string(body)
1168
1169 // Check that the HTML contains the expected service URL in the JavaScript
1170 expectedJSURL := tt.expectedServiceURL + "/test-mapper/query"
Akrond0c88602025-06-27 16:57:21 +02001171 assert.Contains(t, htmlContent, "'service' : '"+expectedJSURL)
Akron2ac2ec02025-06-05 15:26:42 +02001172
1173 // Ensure it's still a valid HTML page
1174 assert.Contains(t, htmlContent, "KoralPipe-TermMapper")
1175 assert.Contains(t, htmlContent, "<!DOCTYPE html>")
1176 })
1177 }
1178}
1179
1180func TestServiceURLConfigFileLoading(t *testing.T) {
1181 // Create a temporary config file with custom service URL
1182 configContent := `
1183sdk: "https://custom.example.com/sdk.js"
1184server: "https://custom.example.com/"
1185serviceURL: "https://custom.example.com/api/termmapper"
1186lists:
1187- id: config-mapper
1188 mappings:
1189 - "[X] <> [Y]"
1190`
1191 configFile, err := os.CreateTemp("", "service-url-config-*.yaml")
1192 require.NoError(t, err)
1193 defer os.Remove(configFile.Name())
1194
1195 _, err = configFile.WriteString(configContent)
1196 require.NoError(t, err)
1197 err = configFile.Close()
1198 require.NoError(t, err)
1199
1200 // Load configuration from file
1201 config, err := tmconfig.LoadFromSources(configFile.Name(), nil)
1202 require.NoError(t, err)
1203
1204 // Verify that the service URL was loaded correctly
1205 assert.Equal(t, "https://custom.example.com/api/termmapper", config.ServiceURL)
1206
1207 // Verify other fields are also preserved
1208 assert.Equal(t, "https://custom.example.com/sdk.js", config.SDK)
1209 assert.Equal(t, "https://custom.example.com/", config.Server)
1210
1211 // Create mapper and test the service URL is used in the HTML
1212 m, err := mapper.NewMapper(config.Lists)
1213 require.NoError(t, err)
1214
1215 app := fiber.New()
1216 setupRoutes(app, m, config)
1217
1218 req := httptest.NewRequest(http.MethodGet, "/config-mapper", nil)
1219 resp, err := app.Test(req)
1220 require.NoError(t, err)
1221 defer resp.Body.Close()
1222
1223 assert.Equal(t, http.StatusOK, resp.StatusCode)
1224 body, err := io.ReadAll(resp.Body)
1225 require.NoError(t, err)
1226
1227 htmlContent := string(body)
1228 expectedJSURL := "https://custom.example.com/api/termmapper/config-mapper/query"
Akrond0c88602025-06-27 16:57:21 +02001229 assert.Contains(t, htmlContent, "'service' : '"+expectedJSURL)
Akron2ac2ec02025-06-05 15:26:42 +02001230}
1231
1232func TestServiceURLDefaults(t *testing.T) {
1233 // Test that defaults are applied correctly when creating a config
1234 config := &tmconfig.MappingConfig{
1235 Lists: []tmconfig.MappingList{
1236 {
1237 ID: "test",
1238 Mappings: []tmconfig.MappingRule{"[A] <> [B]"},
1239 },
1240 },
1241 }
1242
1243 // Apply defaults (simulating what happens during loading)
1244 tmconfig.ApplyDefaults(config)
1245
1246 // Check that the default service URL was applied
1247 assert.Equal(t, "https://korap.ids-mannheim.de/plugin/termmapper", config.ServiceURL)
1248
1249 // Check that other defaults were also applied
1250 assert.Equal(t, "https://korap.ids-mannheim.de/", config.Server)
1251 assert.Equal(t, "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", config.SDK)
Akron24ab22e2025-06-24 16:37:33 +02001252 assert.Equal(t, 5725, config.Port)
Akron2ac2ec02025-06-05 15:26:42 +02001253 assert.Equal(t, "warn", config.LogLevel)
1254}
1255
1256func TestServiceURLWithExampleConfig(t *testing.T) {
1257 // Test that the actual example config file works with the new serviceURL functionality
1258 // and that defaults are properly applied when serviceURL is not specified
1259
1260 config, err := tmconfig.LoadFromSources("../../testdata/example-config.yaml", nil)
1261 require.NoError(t, err)
1262
1263 // Verify that the default service URL was applied since it's not in the example config
1264 assert.Equal(t, "https://korap.ids-mannheim.de/plugin/termmapper", config.ServiceURL)
1265
1266 // Verify other values from the example config are preserved
1267 assert.Equal(t, "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", config.SDK)
1268 assert.Equal(t, "https://korap.ids-mannheim.de/", config.Server)
1269
1270 // Verify the mapper was loaded correctly
1271 require.Len(t, config.Lists, 1)
1272 assert.Equal(t, "main-config-mapper", config.Lists[0].ID)
1273
1274 // Create mapper and test that the service URL is used correctly in the HTML
1275 m, err := mapper.NewMapper(config.Lists)
1276 require.NoError(t, err)
1277
1278 app := fiber.New()
1279 setupRoutes(app, m, config)
1280
1281 req := httptest.NewRequest(http.MethodGet, "/main-config-mapper", nil)
1282 resp, err := app.Test(req)
1283 require.NoError(t, err)
1284 defer resp.Body.Close()
1285
1286 assert.Equal(t, http.StatusOK, resp.StatusCode)
1287 body, err := io.ReadAll(resp.Body)
1288 require.NoError(t, err)
1289
1290 htmlContent := string(body)
1291 expectedJSURL := "https://korap.ids-mannheim.de/plugin/termmapper/main-config-mapper/query"
Akrond0c88602025-06-27 16:57:21 +02001292 assert.Contains(t, htmlContent, "'service' : '"+expectedJSURL)
Akron2ac2ec02025-06-05 15:26:42 +02001293}
Akron80067202025-06-06 14:16:25 +02001294
1295func TestGenerateKalamarPluginHTMLWithURLJoining(t *testing.T) {
1296 tests := []struct {
1297 name string
1298 serviceURL string
1299 mapID string
1300 expected string
1301 }{
1302 {
1303 name: "Service URL without trailing slash",
1304 serviceURL: "https://example.com/plugin/termmapper",
1305 mapID: "test-mapper",
Akrond0c88602025-06-27 16:57:21 +02001306 expected: "'service' : 'https://example.com/plugin/termmapper/test-mapper/query",
Akron80067202025-06-06 14:16:25 +02001307 },
1308 {
1309 name: "Service URL with trailing slash",
1310 serviceURL: "https://example.com/plugin/termmapper/",
1311 mapID: "test-mapper",
Akrond0c88602025-06-27 16:57:21 +02001312 expected: "'service' : 'https://example.com/plugin/termmapper/test-mapper/query",
Akron80067202025-06-06 14:16:25 +02001313 },
1314 {
1315 name: "Map ID with leading slash",
1316 serviceURL: "https://example.com/plugin/termmapper",
1317 mapID: "/test-mapper",
Akrond0c88602025-06-27 16:57:21 +02001318 expected: "'service' : 'https://example.com/plugin/termmapper/test-mapper/query",
Akron80067202025-06-06 14:16:25 +02001319 },
1320 {
1321 name: "Both with slashes",
1322 serviceURL: "https://example.com/plugin/termmapper/",
1323 mapID: "/test-mapper",
Akrond0c88602025-06-27 16:57:21 +02001324 expected: "'service' : 'https://example.com/plugin/termmapper/test-mapper/query",
Akron80067202025-06-06 14:16:25 +02001325 },
1326 {
1327 name: "Complex map ID",
1328 serviceURL: "https://example.com/api/v1/",
1329 mapID: "complex-mapper-name_123",
Akrond0c88602025-06-27 16:57:21 +02001330 expected: "'service' : 'https://example.com/api/v1/complex-mapper-name_123/query",
Akron80067202025-06-06 14:16:25 +02001331 },
1332 }
1333
1334 for _, tt := range tests {
1335 t.Run(tt.name, func(t *testing.T) {
1336 data := TemplateData{
1337 Title: "Test Mapper",
1338 Version: "1.0.0",
1339 Hash: "abcd1234",
1340 Date: "2024-01-01",
1341 Description: "Test description",
1342 Server: "https://example.com/",
1343 SDK: "https://example.com/js/sdk.js",
1344 ServiceURL: tt.serviceURL,
1345 MapID: tt.mapID,
1346 Mappings: []TemplateMapping{},
1347 }
1348
Akroncb51f812025-06-30 15:24:20 +02001349 // Use default query parameters for this test
1350 queryParams := QueryParams{
1351 Dir: "atob",
1352 FoundryA: "",
1353 FoundryB: "",
1354 LayerA: "",
1355 LayerB: "",
1356 }
1357
1358 html := generateKalamarPluginHTML(data, queryParams)
Akron80067202025-06-06 14:16:25 +02001359 assert.Contains(t, html, tt.expected)
1360 })
1361 }
1362}
Akroncb51f812025-06-30 15:24:20 +02001363
1364func TestKalamarPluginWithQueryParameters(t *testing.T) {
1365 // Create test mapping list
1366 mappingList := tmconfig.MappingList{
1367 ID: "test-mapper",
1368 Mappings: []tmconfig.MappingRule{
1369 "[A] <> [B]",
1370 },
1371 }
1372
1373 // Create mapper
1374 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
1375 require.NoError(t, err)
1376
1377 // Create mock config
1378 mockConfig := &tmconfig.MappingConfig{
1379 ServiceURL: "https://example.com/plugin/termmapper",
1380 Lists: []tmconfig.MappingList{mappingList},
1381 }
1382
1383 // Apply defaults
1384 tmconfig.ApplyDefaults(mockConfig)
1385
1386 // Create fiber app
1387 app := fiber.New()
1388 setupRoutes(app, m, mockConfig)
1389
1390 tests := []struct {
1391 name string
1392 url string
1393 expectedQueryURL string
1394 expectedRespURL string
1395 expectedStatus int
1396 expectedError string
1397 }{
1398 {
1399 name: "Default parameters (no query params)",
1400 url: "/test-mapper",
1401 expectedQueryURL: "https://example.com/plugin/termmapper/test-mapper/query?dir=atob",
1402 expectedRespURL: "https://example.com/plugin/termmapper/test-mapper/response?dir=btoa",
1403 expectedStatus: http.StatusOK,
1404 },
1405 {
1406 name: "Explicit dir=atob",
1407 url: "/test-mapper?dir=atob",
1408 expectedQueryURL: "https://example.com/plugin/termmapper/test-mapper/query?dir=atob",
1409 expectedRespURL: "https://example.com/plugin/termmapper/test-mapper/response?dir=btoa",
1410 expectedStatus: http.StatusOK,
1411 },
1412 {
1413 name: "Explicit dir=btoa",
1414 url: "/test-mapper?dir=btoa",
1415 expectedQueryURL: "https://example.com/plugin/termmapper/test-mapper/query?dir=btoa",
1416 expectedRespURL: "https://example.com/plugin/termmapper/test-mapper/response?dir=atob",
1417 expectedStatus: http.StatusOK,
1418 },
1419 {
1420 name: "With foundry parameters",
1421 url: "/test-mapper?dir=atob&foundryA=opennlp&foundryB=upos",
1422 expectedQueryURL: "https://example.com/plugin/termmapper/test-mapper/query?dir=atob&foundryA=opennlp&foundryB=upos",
1423 expectedRespURL: "https://example.com/plugin/termmapper/test-mapper/response?dir=btoa&foundryA=opennlp&foundryB=upos",
1424 expectedStatus: http.StatusOK,
1425 },
1426 {
1427 name: "With layer parameters",
1428 url: "/test-mapper?dir=btoa&layerA=pos&layerB=upos",
1429 expectedQueryURL: "https://example.com/plugin/termmapper/test-mapper/query?dir=btoa&layerA=pos&layerB=upos",
1430 expectedRespURL: "https://example.com/plugin/termmapper/test-mapper/response?dir=atob&layerA=pos&layerB=upos",
1431 expectedStatus: http.StatusOK,
1432 },
1433 {
1434 name: "All parameters",
1435 url: "/test-mapper?dir=atob&foundryA=opennlp&foundryB=upos&layerA=pos&layerB=upos",
1436 expectedQueryURL: "https://example.com/plugin/termmapper/test-mapper/query?dir=atob&foundryA=opennlp&foundryB=upos&layerA=pos&layerB=upos",
1437 expectedRespURL: "https://example.com/plugin/termmapper/test-mapper/response?dir=btoa&foundryA=opennlp&foundryB=upos&layerA=pos&layerB=upos",
1438 expectedStatus: http.StatusOK,
1439 },
1440 {
1441 name: "Invalid direction",
1442 url: "/test-mapper?dir=invalid",
1443 expectedStatus: http.StatusBadRequest,
1444 expectedError: "invalid direction, must be 'atob' or 'btoa'",
1445 },
1446 {
1447 name: "Parameter too long",
1448 url: "/test-mapper?foundryA=" + strings.Repeat("a", 1025),
1449 expectedStatus: http.StatusBadRequest,
1450 expectedError: "foundryA too long (max 1024 bytes)",
1451 },
1452 {
1453 name: "Invalid characters in parameter",
1454 url: "/test-mapper?foundryA=invalid<>chars",
1455 expectedStatus: http.StatusBadRequest,
1456 expectedError: "foundryA contains invalid characters",
1457 },
1458 }
1459
1460 for _, tt := range tests {
1461 t.Run(tt.name, func(t *testing.T) {
1462 req := httptest.NewRequest(http.MethodGet, tt.url, nil)
1463 resp, err := app.Test(req)
1464 require.NoError(t, err)
1465 defer resp.Body.Close()
1466
1467 assert.Equal(t, tt.expectedStatus, resp.StatusCode)
1468
1469 body, err := io.ReadAll(resp.Body)
1470 require.NoError(t, err)
1471
1472 if tt.expectedError != "" {
1473 // Check error message
1474 var errResp fiber.Map
1475 err = json.Unmarshal(body, &errResp)
1476 require.NoError(t, err)
1477 assert.Equal(t, tt.expectedError, errResp["error"])
1478 } else {
1479 htmlContent := string(body)
1480
1481 // Check that both query and response URLs are present with correct parameters
1482 assert.Contains(t, htmlContent, "'service' : '"+tt.expectedQueryURL+"'")
1483 assert.Contains(t, htmlContent, "'service' : '"+tt.expectedRespURL+"'")
1484
1485 // Ensure it's still a valid HTML page
1486 assert.Contains(t, htmlContent, "KoralPipe-TermMapper")
1487 assert.Contains(t, htmlContent, "<!DOCTYPE html>")
1488 }
1489 })
1490 }
1491}
1492
1493func TestBuildQueryParams(t *testing.T) {
1494 tests := []struct {
1495 name string
1496 dir string
1497 foundryA string
1498 foundryB string
1499 layerA string
1500 layerB string
1501 expected string
1502 }{
1503 {
1504 name: "Only direction parameter",
1505 dir: "atob",
1506 expected: "dir=atob",
1507 },
1508 {
1509 name: "All parameters",
1510 dir: "btoa",
1511 foundryA: "opennlp",
1512 foundryB: "upos",
1513 layerA: "pos",
1514 layerB: "upos",
1515 expected: "dir=btoa&foundryA=opennlp&foundryB=upos&layerA=pos&layerB=upos",
1516 },
1517 {
1518 name: "Some parameters empty",
1519 dir: "atob",
1520 foundryA: "opennlp",
1521 foundryB: "",
1522 layerA: "pos",
1523 layerB: "",
1524 expected: "dir=atob&foundryA=opennlp&layerA=pos",
1525 },
1526 {
1527 name: "All parameters empty",
1528 dir: "",
1529 foundryA: "",
1530 foundryB: "",
1531 layerA: "",
1532 layerB: "",
1533 expected: "",
1534 },
1535 {
1536 name: "URL encoding needed",
1537 dir: "atob",
1538 foundryA: "test space",
1539 foundryB: "test&special",
1540 expected: "dir=atob&foundryA=test+space&foundryB=test%26special",
1541 },
1542 }
1543
1544 for _, tt := range tests {
1545 t.Run(tt.name, func(t *testing.T) {
1546 result := buildQueryParams(tt.dir, tt.foundryA, tt.foundryB, tt.layerA, tt.layerB)
1547 assert.Equal(t, tt.expected, result)
1548 })
1549 }
1550}