blob: ee5c80d1543872d7d180c83ce0a1f4796458d53a [file] [log] [blame]
Akron2cbdab52025-05-23 17:57:10 +02001package main
2
3import (
4 "bytes"
5 "encoding/json"
Akron74e1c072025-05-26 14:38:25 +02006 "errors"
Akron2cbdab52025-05-23 17:57:10 +02007 "fmt"
8 "net/http"
9 "net/http/httptest"
10 "net/url"
Akron74e1c072025-05-26 14:38:25 +020011 "strings"
Akron2cbdab52025-05-23 17:57:10 +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"
Akron2cbdab52025-05-23 17:57:10 +020016 "github.com/gofiber/fiber/v2"
Akron74e1c072025-05-26 14:38:25 +020017 "github.com/stretchr/testify/assert"
18 "github.com/stretchr/testify/require"
Akron2cbdab52025-05-23 17:57:10 +020019)
20
21// FuzzInput represents the input data for the fuzzer
22type FuzzInput struct {
23 MapID string
24 Direction string
25 FoundryA string
26 FoundryB string
27 LayerA string
28 LayerB string
29 Body []byte
30}
31
32func FuzzTransformEndpoint(f *testing.F) {
Akrona00d4752025-05-26 17:34:36 +020033 // Create test mapping list
34 mappingList := tmconfig.MappingList{
35 ID: "test-mapper",
36 FoundryA: "opennlp",
37 LayerA: "p",
38 FoundryB: "upos",
39 LayerB: "p",
40 Mappings: []tmconfig.MappingRule{
41 "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]",
42 "[DET] <> [opennlp/p=DET]",
43 },
Akron2cbdab52025-05-23 17:57:10 +020044 }
45
46 // Create mapper
Akrona00d4752025-05-26 17:34:36 +020047 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
Akron2cbdab52025-05-23 17:57:10 +020048 if err != nil {
49 f.Fatal(err)
50 }
51
Akron40aaa632025-06-03 17:57:52 +020052 // Create mock config for testing
53 mockConfig := &tmconfig.MappingLists{
54 Lists: []tmconfig.MappingList{mappingList},
55 }
56
Akron2cbdab52025-05-23 17:57:10 +020057 // Create fiber app
58 app := fiber.New(fiber.Config{
59 DisableStartupMessage: true,
60 ErrorHandler: func(c *fiber.Ctx, err error) error {
Akron40aaa632025-06-03 17:57:52 +020061 // For body limit errors, return 413 status code
62 if err.Error() == "body size exceeds the given limit" || errors.Is(err, fiber.ErrRequestEntityTooLarge) {
63 return c.Status(fiber.StatusRequestEntityTooLarge).JSON(fiber.Map{
64 "error": fmt.Sprintf("request body too large (max %d bytes)", maxInputLength),
65 })
66 }
67 // For other errors, return 500 status code
Akron2cbdab52025-05-23 17:57:10 +020068 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
Akron40aaa632025-06-03 17:57:52 +020069 "error": err.Error(),
Akron2cbdab52025-05-23 17:57:10 +020070 })
71 },
Akron74e1c072025-05-26 14:38:25 +020072 BodyLimit: maxInputLength,
Akron2cbdab52025-05-23 17:57:10 +020073 })
Akron40aaa632025-06-03 17:57:52 +020074 setupRoutes(app, m, mockConfig)
Akron2cbdab52025-05-23 17:57:10 +020075
76 // Add seed corpus
77 f.Add("test-mapper", "atob", "", "", "", "", []byte(`{"@type": "koral:token"}`)) // Valid minimal input
78 f.Add("test-mapper", "btoa", "custom", "", "", "", []byte(`{"@type": "koral:token"}`)) // Valid with foundry override
79 f.Add("", "", "", "", "", "", []byte(`{}`)) // Empty parameters
80 f.Add("nonexistent", "invalid", "!@#$", "%^&*", "()", "[]", []byte(`invalid json`)) // Invalid everything
81 f.Add("test-mapper", "atob", "", "", "", "", []byte(`{"@type": "koral:token", "wrap": null}`)) // Valid JSON, invalid structure
82 f.Add("test-mapper", "atob", "", "", "", "", []byte(`{"@type": "koral:token", "wrap": {"@type": "unknown"}}`)) // Unknown type
83 f.Add("test-mapper", "atob", "", "", "", "", []byte(`{"@type": "koral:token", "wrap": {"@type": "koral:term"}}`)) // Missing required fields
Akron74e1c072025-05-26 14:38:25 +020084 f.Add("0", "0", strings.Repeat("\x83", 1000), "0", "Q", "", []byte("0")) // Failing fuzz test case
Akron2cbdab52025-05-23 17:57:10 +020085
86 f.Fuzz(func(t *testing.T, mapID, dir, foundryA, foundryB, layerA, layerB string, body []byte) {
Akron74e1c072025-05-26 14:38:25 +020087
88 // Validate input first
89 if err := validateInput(mapID, dir, foundryA, foundryB, layerA, layerB, body); err != nil {
90 // Skip this test case as it's invalid
91 t.Skip(err)
92 }
93
Akron2cbdab52025-05-23 17:57:10 +020094 // Build URL with query parameters
95 params := url.Values{}
96 if dir != "" {
97 params.Set("dir", dir)
98 }
99 if foundryA != "" {
100 params.Set("foundryA", foundryA)
101 }
102 if foundryB != "" {
103 params.Set("foundryB", foundryB)
104 }
105 if layerA != "" {
106 params.Set("layerA", layerA)
107 }
108 if layerB != "" {
109 params.Set("layerB", layerB)
110 }
111
112 url := fmt.Sprintf("/%s/query", url.PathEscape(mapID))
113 if len(params) > 0 {
114 url += "?" + params.Encode()
115 }
116
117 // Make request
118 req := httptest.NewRequest(http.MethodPost, url, bytes.NewReader(body))
119 req.Header.Set("Content-Type", "application/json")
120 resp, err := app.Test(req)
121 if err != nil {
122 t.Fatal(err)
123 }
124 defer resp.Body.Close()
125
126 // Verify that we always get a valid response
127 if resp.StatusCode != http.StatusOK &&
128 resp.StatusCode != http.StatusBadRequest &&
129 resp.StatusCode != http.StatusInternalServerError {
130 t.Errorf("unexpected status code: %d", resp.StatusCode)
131 }
132
133 // Verify that the response is valid JSON
Akrone5aaf0a2025-06-02 16:43:54 +0200134 var result any
Akron2cbdab52025-05-23 17:57:10 +0200135 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
136 t.Errorf("invalid JSON response: %v", err)
137 }
138
139 // For error responses, verify that we have an error message
140 if resp.StatusCode != http.StatusOK {
Akrone5aaf0a2025-06-02 16:43:54 +0200141 // For error responses, we expect a JSON object with an error field
142 if resultMap, ok := result.(map[string]any); ok {
143 if errMsg, ok := resultMap["error"].(string); !ok || errMsg == "" {
144 t.Error("error response missing error message")
145 }
146 } else {
147 t.Error("error response should be a JSON object")
Akron2cbdab52025-05-23 17:57:10 +0200148 }
149 }
150 })
151}
Akron74e1c072025-05-26 14:38:25 +0200152
153func TestLargeInput(t *testing.T) {
Akrona00d4752025-05-26 17:34:36 +0200154 // Create test mapping list
155 mappingList := tmconfig.MappingList{
156 ID: "test-mapper",
157 Mappings: []tmconfig.MappingRule{
158 "[A] <> [B]",
159 },
160 }
Akron74e1c072025-05-26 14:38:25 +0200161
162 // Create mapper
Akrona00d4752025-05-26 17:34:36 +0200163 m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
Akron74e1c072025-05-26 14:38:25 +0200164 require.NoError(t, err)
165
Akron40aaa632025-06-03 17:57:52 +0200166 // Create mock config for testing
167 mockConfig := &tmconfig.MappingLists{
168 Lists: []tmconfig.MappingList{mappingList},
169 }
170
Akron74e1c072025-05-26 14:38:25 +0200171 // Create fiber app
172 app := fiber.New(fiber.Config{
173 DisableStartupMessage: true,
174 ErrorHandler: func(c *fiber.Ctx, err error) error {
175 // For body limit errors, return 413 status code
176 if err.Error() == "body size exceeds the given limit" || errors.Is(err, fiber.ErrRequestEntityTooLarge) {
177 return c.Status(fiber.StatusRequestEntityTooLarge).JSON(fiber.Map{
178 "error": fmt.Sprintf("request body too large (max %d bytes)", maxInputLength),
179 })
180 }
181 // For other errors, return 500 status code
182 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
183 "error": err.Error(),
184 })
185 },
186 BodyLimit: maxInputLength,
187 })
Akron40aaa632025-06-03 17:57:52 +0200188 setupRoutes(app, m, mockConfig)
Akron74e1c072025-05-26 14:38:25 +0200189
190 tests := []struct {
191 name string
192 mapID string
193 direction string
194 foundryA string
195 foundryB string
196 layerA string
197 layerB string
198 input string
199 expectedCode int
200 expectedError string
201 }{
202 {
203 name: "Large map ID",
204 mapID: strings.Repeat("a", maxParamLength+1),
205 direction: "atob",
206 input: "{}",
207 expectedCode: http.StatusBadRequest,
Akron69d43bf2025-05-26 17:09:00 +0200208 expectedError: "mapID too long (max 1024 bytes)",
Akron74e1c072025-05-26 14:38:25 +0200209 },
210 {
211 name: "Large direction",
212 mapID: "test-mapper",
213 direction: strings.Repeat("a", maxParamLength+1),
214 input: "{}",
215 expectedCode: http.StatusBadRequest,
Akron69d43bf2025-05-26 17:09:00 +0200216 expectedError: "dir too long (max 1024 bytes)",
Akron74e1c072025-05-26 14:38:25 +0200217 },
218 {
219 name: "Large foundryA",
220 mapID: "test-mapper",
221 direction: "atob",
222 foundryA: strings.Repeat("a", maxParamLength+1),
223 input: "{}",
224 expectedCode: http.StatusBadRequest,
225 expectedError: "foundryA too long (max 1024 bytes)",
226 },
227 {
228 name: "Invalid characters in mapID",
229 mapID: "test<>mapper",
230 direction: "atob",
231 input: "{}",
232 expectedCode: http.StatusBadRequest,
233 expectedError: "mapID contains invalid characters",
234 },
235 {
236 name: "Large request body",
237 mapID: "test-mapper",
238 direction: "atob",
239 input: strings.Repeat("a", maxInputLength+1),
240 expectedCode: http.StatusRequestEntityTooLarge,
241 expectedError: "body size exceeds the given limit",
242 },
243 }
244
245 for _, tt := range tests {
246 t.Run(tt.name, func(t *testing.T) {
247 // Build URL with query parameters
248 url := "/" + tt.mapID + "/query"
249 if tt.direction != "" {
250 url += "?dir=" + tt.direction
251 }
252 if tt.foundryA != "" {
253 url += "&foundryA=" + tt.foundryA
254 }
255 if tt.foundryB != "" {
256 url += "&foundryB=" + tt.foundryB
257 }
258 if tt.layerA != "" {
259 url += "&layerA=" + tt.layerA
260 }
261 if tt.layerB != "" {
262 url += "&layerB=" + tt.layerB
263 }
264
265 // Make request
266 req := httptest.NewRequest(http.MethodPost, url, strings.NewReader(tt.input))
267 req.Header.Set("Content-Type", "application/json")
268 resp, err := app.Test(req)
269
270 if resp == nil {
271 assert.Equal(t, tt.expectedError, err.Error())
272 return
273 }
274
275 require.NoError(t, err)
276 defer resp.Body.Close()
277
278 // Check status code
279 assert.Equal(t, tt.expectedCode, resp.StatusCode)
280
281 // Check error message
Akron121c66e2025-06-02 16:34:05 +0200282 var result map[string]any
Akron74e1c072025-05-26 14:38:25 +0200283 err = json.NewDecoder(resp.Body).Decode(&result)
284 require.NoError(t, err)
285 errMsg, ok := result["error"].(string)
286 require.True(t, ok)
287 assert.Equal(t, tt.expectedError, errMsg)
288 })
289 }
290}
Akron121c66e2025-06-02 16:34:05 +0200291
292// # Run fuzzing for 1 minute
293// go test -fuzz=FuzzTransformEndpoint -fuzztime=1m ./cmd/termmapper
294//
295// # Run fuzzing until a crash is found or Ctrl+C is pressed
296// go test -fuzz=FuzzTransformEndpoint ./cmd/termmapper
297//
298// # Run fuzzing with verbose output
299// go test -fuzz=FuzzTransformEndpoint -v ./cmd/termmapper
300//
301// go test -run=FuzzTransformEndpoint/testdata/fuzz/FuzzTransformEndpoint/$SEED