blob: ea802684c49192d4d1705fb12489aa42763a08cf [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"
11 "os"
12 "path/filepath"
Akron74e1c072025-05-26 14:38:25 +020013 "strings"
Akron2cbdab52025-05-23 17:57:10 +020014 "testing"
15
16 "github.com/KorAP/KoralPipe-TermMapper2/pkg/mapper"
17 "github.com/gofiber/fiber/v2"
Akron74e1c072025-05-26 14:38:25 +020018 "github.com/stretchr/testify/assert"
19 "github.com/stretchr/testify/require"
Akron2cbdab52025-05-23 17:57:10 +020020)
21
22// FuzzInput represents the input data for the fuzzer
23type FuzzInput struct {
24 MapID string
25 Direction string
26 FoundryA string
27 FoundryB string
28 LayerA string
29 LayerB string
30 Body []byte
31}
32
33func FuzzTransformEndpoint(f *testing.F) {
34 // Create a temporary config file with valid mappings
35 tmpDir := f.TempDir()
36 configFile := filepath.Join(tmpDir, "test-config.yaml")
37
38 configContent := `- id: test-mapper
39 foundryA: opennlp
40 layerA: p
41 foundryB: upos
42 layerB: p
43 mappings:
44 - "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]"
45 - "[DET] <> [opennlp/p=DET]"`
46
47 err := os.WriteFile(configFile, []byte(configContent), 0644)
48 if err != nil {
49 f.Fatal(err)
50 }
51
52 // Create mapper
53 m, err := mapper.NewMapper(configFile)
54 if err != nil {
55 f.Fatal(err)
56 }
57
58 // Create fiber app
59 app := fiber.New(fiber.Config{
60 DisableStartupMessage: true,
61 ErrorHandler: func(c *fiber.Ctx, err error) error {
62 // Ensure we always return a valid JSON response even for panic cases
63 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
64 "error": "internal server error",
65 })
66 },
Akron74e1c072025-05-26 14:38:25 +020067 BodyLimit: maxInputLength,
Akron2cbdab52025-05-23 17:57:10 +020068 })
69 setupRoutes(app, m)
70
71 // Add seed corpus
72 f.Add("test-mapper", "atob", "", "", "", "", []byte(`{"@type": "koral:token"}`)) // Valid minimal input
73 f.Add("test-mapper", "btoa", "custom", "", "", "", []byte(`{"@type": "koral:token"}`)) // Valid with foundry override
74 f.Add("", "", "", "", "", "", []byte(`{}`)) // Empty parameters
75 f.Add("nonexistent", "invalid", "!@#$", "%^&*", "()", "[]", []byte(`invalid json`)) // Invalid everything
76 f.Add("test-mapper", "atob", "", "", "", "", []byte(`{"@type": "koral:token", "wrap": null}`)) // Valid JSON, invalid structure
77 f.Add("test-mapper", "atob", "", "", "", "", []byte(`{"@type": "koral:token", "wrap": {"@type": "unknown"}}`)) // Unknown type
78 f.Add("test-mapper", "atob", "", "", "", "", []byte(`{"@type": "koral:token", "wrap": {"@type": "koral:term"}}`)) // Missing required fields
Akron74e1c072025-05-26 14:38:25 +020079 f.Add("0", "0", strings.Repeat("\x83", 1000), "0", "Q", "", []byte("0")) // Failing fuzz test case
Akron2cbdab52025-05-23 17:57:10 +020080
81 f.Fuzz(func(t *testing.T, mapID, dir, foundryA, foundryB, layerA, layerB string, body []byte) {
Akron74e1c072025-05-26 14:38:25 +020082
83 // Validate input first
84 if err := validateInput(mapID, dir, foundryA, foundryB, layerA, layerB, body); err != nil {
85 // Skip this test case as it's invalid
86 t.Skip(err)
87 }
88
Akron2cbdab52025-05-23 17:57:10 +020089 // Build URL with query parameters
90 params := url.Values{}
91 if dir != "" {
92 params.Set("dir", dir)
93 }
94 if foundryA != "" {
95 params.Set("foundryA", foundryA)
96 }
97 if foundryB != "" {
98 params.Set("foundryB", foundryB)
99 }
100 if layerA != "" {
101 params.Set("layerA", layerA)
102 }
103 if layerB != "" {
104 params.Set("layerB", layerB)
105 }
106
107 url := fmt.Sprintf("/%s/query", url.PathEscape(mapID))
108 if len(params) > 0 {
109 url += "?" + params.Encode()
110 }
111
112 // Make request
113 req := httptest.NewRequest(http.MethodPost, url, bytes.NewReader(body))
114 req.Header.Set("Content-Type", "application/json")
115 resp, err := app.Test(req)
116 if err != nil {
117 t.Fatal(err)
118 }
119 defer resp.Body.Close()
120
121 // Verify that we always get a valid response
122 if resp.StatusCode != http.StatusOK &&
123 resp.StatusCode != http.StatusBadRequest &&
124 resp.StatusCode != http.StatusInternalServerError {
125 t.Errorf("unexpected status code: %d", resp.StatusCode)
126 }
127
128 // Verify that the response is valid JSON
129 var result map[string]interface{}
130 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
131 t.Errorf("invalid JSON response: %v", err)
132 }
133
134 // For error responses, verify that we have an error message
135 if resp.StatusCode != http.StatusOK {
136 if errMsg, ok := result["error"].(string); !ok || errMsg == "" {
137 t.Error("error response missing error message")
138 }
139 }
140 })
141}
Akron74e1c072025-05-26 14:38:25 +0200142
143func TestLargeInput(t *testing.T) {
144 // Create a temporary config file
145 tmpDir := t.TempDir()
146 configFile := filepath.Join(tmpDir, "test-config.yaml")
147
148 configContent := `- id: test-mapper
149 mappings:
150 - "[A] <> [B]"`
151
152 err := os.WriteFile(configFile, []byte(configContent), 0644)
153 require.NoError(t, err)
154
155 // Create mapper
156 m, err := mapper.NewMapper(configFile)
157 require.NoError(t, err)
158
159 // Create fiber app
160 app := fiber.New(fiber.Config{
161 DisableStartupMessage: true,
162 ErrorHandler: func(c *fiber.Ctx, err error) error {
163 // For body limit errors, return 413 status code
164 if err.Error() == "body size exceeds the given limit" || errors.Is(err, fiber.ErrRequestEntityTooLarge) {
165 return c.Status(fiber.StatusRequestEntityTooLarge).JSON(fiber.Map{
166 "error": fmt.Sprintf("request body too large (max %d bytes)", maxInputLength),
167 })
168 }
169 // For other errors, return 500 status code
170 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
171 "error": err.Error(),
172 })
173 },
174 BodyLimit: maxInputLength,
175 })
176 setupRoutes(app, m)
177
178 tests := []struct {
179 name string
180 mapID string
181 direction string
182 foundryA string
183 foundryB string
184 layerA string
185 layerB string
186 input string
187 expectedCode int
188 expectedError string
189 }{
190 {
191 name: "Large map ID",
192 mapID: strings.Repeat("a", maxParamLength+1),
193 direction: "atob",
194 input: "{}",
195 expectedCode: http.StatusBadRequest,
196 expectedError: "map ID too long (max 1024 bytes)",
197 },
198 {
199 name: "Large direction",
200 mapID: "test-mapper",
201 direction: strings.Repeat("a", maxParamLength+1),
202 input: "{}",
203 expectedCode: http.StatusBadRequest,
204 expectedError: "direction too long (max 1024 bytes)",
205 },
206 {
207 name: "Large foundryA",
208 mapID: "test-mapper",
209 direction: "atob",
210 foundryA: strings.Repeat("a", maxParamLength+1),
211 input: "{}",
212 expectedCode: http.StatusBadRequest,
213 expectedError: "foundryA too long (max 1024 bytes)",
214 },
215 {
216 name: "Invalid characters in mapID",
217 mapID: "test<>mapper",
218 direction: "atob",
219 input: "{}",
220 expectedCode: http.StatusBadRequest,
221 expectedError: "mapID contains invalid characters",
222 },
223 {
224 name: "Large request body",
225 mapID: "test-mapper",
226 direction: "atob",
227 input: strings.Repeat("a", maxInputLength+1),
228 expectedCode: http.StatusRequestEntityTooLarge,
229 expectedError: "body size exceeds the given limit",
230 },
231 }
232
233 for _, tt := range tests {
234 t.Run(tt.name, func(t *testing.T) {
235 // Build URL with query parameters
236 url := "/" + tt.mapID + "/query"
237 if tt.direction != "" {
238 url += "?dir=" + tt.direction
239 }
240 if tt.foundryA != "" {
241 url += "&foundryA=" + tt.foundryA
242 }
243 if tt.foundryB != "" {
244 url += "&foundryB=" + tt.foundryB
245 }
246 if tt.layerA != "" {
247 url += "&layerA=" + tt.layerA
248 }
249 if tt.layerB != "" {
250 url += "&layerB=" + tt.layerB
251 }
252
253 // Make request
254 req := httptest.NewRequest(http.MethodPost, url, strings.NewReader(tt.input))
255 req.Header.Set("Content-Type", "application/json")
256 resp, err := app.Test(req)
257
258 if resp == nil {
259 assert.Equal(t, tt.expectedError, err.Error())
260 return
261 }
262
263 require.NoError(t, err)
264 defer resp.Body.Close()
265
266 // Check status code
267 assert.Equal(t, tt.expectedCode, resp.StatusCode)
268
269 // Check error message
270 var result map[string]interface{}
271 err = json.NewDecoder(resp.Body).Decode(&result)
272 require.NoError(t, err)
273 errMsg, ok := result["error"].(string)
274 require.True(t, ok)
275 assert.Equal(t, tt.expectedError, errMsg)
276 })
277 }
278}