blob: c045e0bf1603856196245fc5e088ede4f46c0b93 [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"
Akron49ceeb42025-05-23 17:46:01 +020010 "testing"
11
Akrona00d4752025-05-26 17:34:36 +020012 tmconfig "github.com/KorAP/KoralPipe-TermMapper/config"
Akronfa55bb22025-05-26 15:10:42 +020013 "github.com/KorAP/KoralPipe-TermMapper/mapper"
Akron49ceeb42025-05-23 17:46:01 +020014 "github.com/gofiber/fiber/v2"
15 "github.com/stretchr/testify/assert"
16 "github.com/stretchr/testify/require"
17)
18
19func TestTransformEndpoint(t *testing.T) {
Akrona00d4752025-05-26 17:34:36 +020020 // Create test mapping list
21 mappingList := tmconfig.MappingList{
22 ID: "test-mapper",
23 FoundryA: "opennlp",
24 LayerA: "p",
25 FoundryB: "upos",
26 LayerB: "p",
27 Mappings: []tmconfig.MappingRule{
28 "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]",
29 "[DET] <> [opennlp/p=DET]",
30 },
31 }
Akron49ceeb42025-05-23 17:46:01 +020032
33 // Create mapper
Akrona00d4752025-05-26 17:34:36 +020034 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
Akron49ceeb42025-05-23 17:46:01 +020035 require.NoError(t, err)
36
Akron40aaa632025-06-03 17:57:52 +020037 // Create mock config for testing
Akron06d21f02025-06-04 14:36:07 +020038 mockConfig := &tmconfig.MappingConfig{
Akron40aaa632025-06-03 17:57:52 +020039 Lists: []tmconfig.MappingList{mappingList},
40 }
41
Akron49ceeb42025-05-23 17:46:01 +020042 // Create fiber app
43 app := fiber.New()
Akron40aaa632025-06-03 17:57:52 +020044 setupRoutes(app, m, mockConfig)
Akron49ceeb42025-05-23 17:46:01 +020045
46 tests := []struct {
47 name string
48 mapID string
49 direction string
50 foundryA string
51 foundryB string
52 layerA string
53 layerB string
54 input string
55 expectedCode int
56 expectedBody string
57 expectedError string
58 }{
59 {
60 name: "Simple A to B mapping",
61 mapID: "test-mapper",
62 direction: "atob",
63 input: `{
64 "@type": "koral:token",
65 "wrap": {
66 "@type": "koral:term",
67 "foundry": "opennlp",
68 "key": "PIDAT",
69 "layer": "p",
70 "match": "match:eq"
71 }
72 }`,
73 expectedCode: http.StatusOK,
74 expectedBody: `{
75 "@type": "koral:token",
76 "wrap": {
77 "@type": "koral:termGroup",
78 "operands": [
79 {
80 "@type": "koral:term",
81 "foundry": "opennlp",
82 "key": "PIDAT",
83 "layer": "p",
84 "match": "match:eq"
85 },
86 {
87 "@type": "koral:term",
88 "foundry": "opennlp",
89 "key": "AdjType",
90 "layer": "p",
91 "match": "match:eq",
92 "value": "Pdt"
93 }
94 ],
95 "relation": "relation:and"
96 }
97 }`,
98 },
99 {
100 name: "B to A mapping",
101 mapID: "test-mapper",
102 direction: "btoa",
103 input: `{
104 "@type": "koral:token",
105 "wrap": {
106 "@type": "koral:termGroup",
107 "operands": [
108 {
109 "@type": "koral:term",
110 "foundry": "opennlp",
111 "key": "PIDAT",
112 "layer": "p",
113 "match": "match:eq"
114 },
115 {
116 "@type": "koral:term",
117 "foundry": "opennlp",
118 "key": "AdjType",
119 "layer": "p",
120 "match": "match:eq",
121 "value": "Pdt"
122 }
123 ],
124 "relation": "relation:and"
125 }
126 }`,
127 expectedCode: http.StatusOK,
128 expectedBody: `{
129 "@type": "koral:token",
130 "wrap": {
131 "@type": "koral:term",
132 "foundry": "opennlp",
133 "key": "PIDAT",
134 "layer": "p",
135 "match": "match:eq"
136 }
137 }`,
138 },
139 {
140 name: "Mapping with foundry override",
141 mapID: "test-mapper",
142 direction: "atob",
143 foundryB: "custom",
144 input: `{
145 "@type": "koral:token",
146 "wrap": {
147 "@type": "koral:term",
148 "foundry": "opennlp",
149 "key": "PIDAT",
150 "layer": "p",
151 "match": "match:eq"
152 }
153 }`,
154 expectedCode: http.StatusOK,
155 expectedBody: `{
156 "@type": "koral:token",
157 "wrap": {
158 "@type": "koral:termGroup",
159 "operands": [
160 {
161 "@type": "koral:term",
162 "foundry": "custom",
163 "key": "PIDAT",
164 "layer": "p",
165 "match": "match:eq"
166 },
167 {
168 "@type": "koral:term",
169 "foundry": "custom",
170 "key": "AdjType",
171 "layer": "p",
172 "match": "match:eq",
173 "value": "Pdt"
174 }
175 ],
176 "relation": "relation:and"
177 }
178 }`,
179 },
180 {
181 name: "Invalid mapping ID",
182 mapID: "nonexistent",
183 direction: "atob",
184 input: `{"@type": "koral:token"}`,
185 expectedCode: http.StatusInternalServerError,
186 expectedError: "mapping list with ID nonexistent not found",
187 },
188 {
189 name: "Invalid direction",
190 mapID: "test-mapper",
191 direction: "invalid",
192 input: `{"@type": "koral:token"}`,
193 expectedCode: http.StatusBadRequest,
194 expectedError: "invalid direction, must be 'atob' or 'btoa'",
195 },
196 {
197 name: "Invalid JSON",
198 mapID: "test-mapper",
199 direction: "atob",
200 input: `invalid json`,
201 expectedCode: http.StatusBadRequest,
202 expectedError: "invalid JSON in request body",
203 },
204 }
205
206 for _, tt := range tests {
207 t.Run(tt.name, func(t *testing.T) {
208 // Build URL with query parameters
209 url := "/" + tt.mapID + "/query"
210 if tt.direction != "" {
211 url += "?dir=" + tt.direction
212 }
213 if tt.foundryA != "" {
214 url += "&foundryA=" + tt.foundryA
215 }
216 if tt.foundryB != "" {
217 url += "&foundryB=" + tt.foundryB
218 }
219 if tt.layerA != "" {
220 url += "&layerA=" + tt.layerA
221 }
222 if tt.layerB != "" {
223 url += "&layerB=" + tt.layerB
224 }
225
226 // Make request
227 req := httptest.NewRequest(http.MethodPost, url, bytes.NewBufferString(tt.input))
228 req.Header.Set("Content-Type", "application/json")
229 resp, err := app.Test(req)
230 require.NoError(t, err)
231 defer resp.Body.Close()
232
233 // Check status code
234 assert.Equal(t, tt.expectedCode, resp.StatusCode)
235
236 // Read response body
237 body, err := io.ReadAll(resp.Body)
238 require.NoError(t, err)
239
240 if tt.expectedError != "" {
241 // Check error message
242 var errResp fiber.Map
243 err = json.Unmarshal(body, &errResp)
244 require.NoError(t, err)
245 assert.Equal(t, tt.expectedError, errResp["error"])
246 } else {
247 // Compare JSON responses
Akron121c66e2025-06-02 16:34:05 +0200248 var expected, actual any
Akron49ceeb42025-05-23 17:46:01 +0200249 err = json.Unmarshal([]byte(tt.expectedBody), &expected)
250 require.NoError(t, err)
251 err = json.Unmarshal(body, &actual)
252 require.NoError(t, err)
253 assert.Equal(t, expected, actual)
254 }
255 })
256 }
257}
258
259func TestHealthEndpoint(t *testing.T) {
Akrona00d4752025-05-26 17:34:36 +0200260 // Create test mapping list
261 mappingList := tmconfig.MappingList{
262 ID: "test-mapper",
263 Mappings: []tmconfig.MappingRule{
264 "[A] <> [B]",
265 },
266 }
Akron49ceeb42025-05-23 17:46:01 +0200267
Akrona00d4752025-05-26 17:34:36 +0200268 // Create mapper
269 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
Akron49ceeb42025-05-23 17:46:01 +0200270 require.NoError(t, err)
271
Akron40aaa632025-06-03 17:57:52 +0200272 // Create mock config for testing
Akron06d21f02025-06-04 14:36:07 +0200273 mockConfig := &tmconfig.MappingConfig{
Akron40aaa632025-06-03 17:57:52 +0200274 Lists: []tmconfig.MappingList{mappingList},
275 }
276
Akron49ceeb42025-05-23 17:46:01 +0200277 // Create fiber app
278 app := fiber.New()
Akron40aaa632025-06-03 17:57:52 +0200279 setupRoutes(app, m, mockConfig)
Akron49ceeb42025-05-23 17:46:01 +0200280
281 // Test health endpoint
282 req := httptest.NewRequest(http.MethodGet, "/health", nil)
283 resp, err := app.Test(req)
284 require.NoError(t, err)
285 defer resp.Body.Close()
286
287 assert.Equal(t, http.StatusOK, resp.StatusCode)
288 body, err := io.ReadAll(resp.Body)
289 require.NoError(t, err)
290 assert.Equal(t, "OK", string(body))
Akron40aaa632025-06-03 17:57:52 +0200291
Akronc471c0a2025-06-04 11:56:22 +0200292 req = httptest.NewRequest(http.MethodGet, "/", nil)
Akron40aaa632025-06-03 17:57:52 +0200293 resp, err = app.Test(req)
294 require.NoError(t, err)
295 defer resp.Body.Close()
296
297 assert.Equal(t, http.StatusOK, resp.StatusCode)
298 body, err = io.ReadAll(resp.Body)
299 require.NoError(t, err)
Akronfc77b5e2025-06-04 11:44:43 +0200300 assert.Contains(t, string(body), "KoralPipe-TermMapper")
Akron40aaa632025-06-03 17:57:52 +0200301
Akron49ceeb42025-05-23 17:46:01 +0200302}
Akron06d21f02025-06-04 14:36:07 +0200303
304func TestKalamarPluginWithCustomSdkAndServer(t *testing.T) {
305 // Create test mapping list
306 mappingList := tmconfig.MappingList{
307 ID: "test-mapper",
308 Mappings: []tmconfig.MappingRule{
309 "[A] <> [B]",
310 },
311 }
312
313 // Create mapper
314 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
315 require.NoError(t, err)
316
317 tests := []struct {
318 name string
319 customSDK string
320 customServer string
321 expectedSDK string
322 expectedServer string
323 }{
324 {
325 name: "Custom SDK and Server values",
326 customSDK: "https://custom.example.com/custom-sdk.js",
327 customServer: "https://custom.example.com/",
328 expectedSDK: "https://custom.example.com/custom-sdk.js",
329 expectedServer: "https://custom.example.com/",
330 },
331 {
332 name: "Only custom SDK value",
333 customSDK: "https://custom.example.com/custom-sdk.js",
334 customServer: "https://korap.ids-mannheim.de/", // defaults applied during parsing
335 expectedSDK: "https://custom.example.com/custom-sdk.js",
336 expectedServer: "https://korap.ids-mannheim.de/",
337 },
338 {
339 name: "Only custom Server value",
340 customSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // defaults applied during parsing
341 customServer: "https://custom.example.com/",
342 expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js",
343 expectedServer: "https://custom.example.com/",
344 },
345 {
346 name: "Defaults applied during parsing",
347 customSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // defaults applied during parsing
348 customServer: "https://korap.ids-mannheim.de/", // defaults applied during parsing
349 expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js",
350 expectedServer: "https://korap.ids-mannheim.de/",
351 },
352 }
353
354 for _, tt := range tests {
355 t.Run(tt.name, func(t *testing.T) {
356 // Create mock config with custom values
357 mockConfig := &tmconfig.MappingConfig{
358 SDK: tt.customSDK,
359 Server: tt.customServer,
360 Lists: []tmconfig.MappingList{mappingList},
361 }
362
363 // Create fiber app
364 app := fiber.New()
365 setupRoutes(app, m, mockConfig)
366
367 // Test Kalamar plugin endpoint
368 req := httptest.NewRequest(http.MethodGet, "/", nil)
369 resp, err := app.Test(req)
370 require.NoError(t, err)
371 defer resp.Body.Close()
372
373 assert.Equal(t, http.StatusOK, resp.StatusCode)
374 body, err := io.ReadAll(resp.Body)
375 require.NoError(t, err)
376
377 htmlContent := string(body)
378
379 // Check that the HTML contains the expected SDK and Server values
380 assert.Contains(t, htmlContent, `src="`+tt.expectedSDK+`"`)
381 assert.Contains(t, htmlContent, `data-server="`+tt.expectedServer+`"`)
382
383 // Ensure it's still a valid HTML page
384 assert.Contains(t, htmlContent, "KoralPipe-TermMapper")
385 assert.Contains(t, htmlContent, "<!DOCTYPE html>")
386 })
387 }
388}
Akrone1cff7c2025-06-04 18:43:32 +0200389
390func TestMultipleMappingFiles(t *testing.T) {
391 // Create test mapping files
392 mappingFile1Content := `
393id: test-mapper-1
394foundryA: opennlp
395layerA: p
396foundryB: upos
397layerB: p
398mappings:
399 - "[PIDAT] <> [DET & AdjType=Pdt]"
400 - "[PAV] <> [ADV & PronType=Dem]"
401`
402 mappingFile1, err := os.CreateTemp("", "mapping1-*.yaml")
403 require.NoError(t, err)
404 defer os.Remove(mappingFile1.Name())
405
406 _, err = mappingFile1.WriteString(mappingFile1Content)
407 require.NoError(t, err)
408 err = mappingFile1.Close()
409 require.NoError(t, err)
410
411 mappingFile2Content := `
412id: test-mapper-2
413foundryA: stts
414layerA: p
415foundryB: upos
416layerB: p
417mappings:
418 - "[DET] <> [PRON]"
419 - "[ADJ] <> [NOUN]"
420`
421 mappingFile2, err := os.CreateTemp("", "mapping2-*.yaml")
422 require.NoError(t, err)
423 defer os.Remove(mappingFile2.Name())
424
425 _, err = mappingFile2.WriteString(mappingFile2Content)
426 require.NoError(t, err)
427 err = mappingFile2.Close()
428 require.NoError(t, err)
429
430 // Load configuration using multiple mapping files
431 config, err := tmconfig.LoadFromSources("", []string{mappingFile1.Name(), mappingFile2.Name()})
432 require.NoError(t, err)
433
434 // Create mapper
435 m, err := mapper.NewMapper(config.Lists)
436 require.NoError(t, err)
437
438 // Create fiber app
439 app := fiber.New()
440 setupRoutes(app, m, config)
441
442 // Test that both mappers work
443 testCases := []struct {
444 name string
445 mapID string
446 input string
447 expectGroup bool
448 expectedKey string
449 }{
450 {
451 name: "test-mapper-1 with complex mapping",
452 mapID: "test-mapper-1",
453 input: `{
454 "@type": "koral:token",
455 "wrap": {
456 "@type": "koral:term",
457 "foundry": "opennlp",
458 "key": "PIDAT",
459 "layer": "p",
460 "match": "match:eq"
461 }
462 }`,
463 expectGroup: true, // This mapping creates a termGroup because of "&"
464 expectedKey: "DET", // The first operand should be DET
465 },
466 {
467 name: "test-mapper-2 with simple mapping",
468 mapID: "test-mapper-2",
469 input: `{
470 "@type": "koral:token",
471 "wrap": {
472 "@type": "koral:term",
473 "foundry": "stts",
474 "key": "DET",
475 "layer": "p",
476 "match": "match:eq"
477 }
478 }`,
479 expectGroup: false, // This mapping creates a simple term
480 expectedKey: "PRON",
481 },
482 }
483
484 for _, tc := range testCases {
485 t.Run(tc.name, func(t *testing.T) {
486 req := httptest.NewRequest(http.MethodPost, "/"+tc.mapID+"/query?dir=atob", bytes.NewBufferString(tc.input))
487 req.Header.Set("Content-Type", "application/json")
488
489 resp, err := app.Test(req)
490 require.NoError(t, err)
491 defer resp.Body.Close()
492
493 assert.Equal(t, http.StatusOK, resp.StatusCode)
494
495 var result map[string]interface{}
496 err = json.NewDecoder(resp.Body).Decode(&result)
497 require.NoError(t, err)
498
499 // Check that the mapping was applied
500 wrap := result["wrap"].(map[string]interface{})
501 if tc.expectGroup {
502 // For complex mappings, check the first operand
503 assert.Equal(t, "koral:termGroup", wrap["@type"])
504 operands := wrap["operands"].([]interface{})
505 require.Greater(t, len(operands), 0)
506 firstOperand := operands[0].(map[string]interface{})
507 assert.Equal(t, tc.expectedKey, firstOperand["key"])
508 } else {
509 // For simple mappings, check the key directly
510 assert.Equal(t, "koral:term", wrap["@type"])
511 assert.Equal(t, tc.expectedKey, wrap["key"])
512 }
513 })
514 }
515}
516
517func TestCombinedConfigAndMappingFiles(t *testing.T) {
518 // Create main config file
519 mainConfigContent := `
520sdk: "https://custom.example.com/sdk.js"
521server: "https://custom.example.com/"
522lists:
523- id: main-mapper
524 foundryA: opennlp
525 layerA: p
526 mappings:
527 - "[A] <> [B]"
528`
529 mainConfigFile, err := os.CreateTemp("", "main-config-*.yaml")
530 require.NoError(t, err)
531 defer os.Remove(mainConfigFile.Name())
532
533 _, err = mainConfigFile.WriteString(mainConfigContent)
534 require.NoError(t, err)
535 err = mainConfigFile.Close()
536 require.NoError(t, err)
537
538 // Create individual mapping file
539 mappingFileContent := `
540id: additional-mapper
541foundryA: stts
542layerA: p
543mappings:
544 - "[C] <> [D]"
545`
546 mappingFile, err := os.CreateTemp("", "mapping-*.yaml")
547 require.NoError(t, err)
548 defer os.Remove(mappingFile.Name())
549
550 _, err = mappingFile.WriteString(mappingFileContent)
551 require.NoError(t, err)
552 err = mappingFile.Close()
553 require.NoError(t, err)
554
555 // Load configuration from both sources
556 config, err := tmconfig.LoadFromSources(mainConfigFile.Name(), []string{mappingFile.Name()})
557 require.NoError(t, err)
558
559 // Verify that both mappers are loaded
560 require.Len(t, config.Lists, 2)
561
562 ids := make([]string, len(config.Lists))
563 for i, list := range config.Lists {
564 ids[i] = list.ID
565 }
566 assert.Contains(t, ids, "main-mapper")
567 assert.Contains(t, ids, "additional-mapper")
568
569 // Verify custom SDK and server are preserved from main config
570 assert.Equal(t, "https://custom.example.com/sdk.js", config.SDK)
571 assert.Equal(t, "https://custom.example.com/", config.Server)
572
573 // Create mapper and test it works
574 m, err := mapper.NewMapper(config.Lists)
575 require.NoError(t, err)
576 require.NotNil(t, m)
577}