github.com/snyk/vervet/v3@v3.7.0/versionware/validator_test.go (about) 1 package versionware_test 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "net/http" 10 "net/http/httptest" 11 "regexp" 12 "testing" 13 "time" 14 15 qt "github.com/frankban/quicktest" 16 "github.com/getkin/kin-openapi/openapi3" 17 "github.com/getkin/kin-openapi/openapi3filter" 18 19 "github.com/snyk/vervet/v3/versionware" 20 ) 21 22 const ( 23 v20210820 = ` 24 openapi: 3.0.0 25 x-snyk-api-version: 2021-08-20 26 info: 27 title: 'Validator' 28 version: '0.0.0' 29 paths: 30 /test/{id}: 31 get: 32 operationId: getTest 33 description: get a test 34 parameters: 35 - in: path 36 name: id 37 schema: 38 type: string 39 required: true 40 - in: query 41 name: version 42 schema: 43 type: string 44 required: true 45 responses: 46 '200': 47 description: 'respond with test resource' 48 content: 49 application/json: 50 schema: { $ref: '#/components/schemas/TestResource' } 51 '400': { $ref: '#/components/responses/ErrorResponse' } 52 '404': { $ref: '#/components/responses/ErrorResponse' } 53 '500': { $ref: '#/components/responses/ErrorResponse' } 54 components: 55 schemas: 56 TestContents: 57 type: object 58 properties: 59 name: 60 type: string 61 expected: 62 type: number 63 actual: 64 type: number 65 required: [name, expected, actual] 66 additionalProperties: false 67 TestResource: 68 type: object 69 properties: 70 id: 71 type: string 72 contents: 73 { $ref: '#/components/schemas/TestContents' } 74 required: [id, contents] 75 additionalProperties: false 76 Error: 77 type: object 78 properties: 79 code: 80 type: string 81 message: 82 type: string 83 required: [code, message] 84 additionalProperties: false 85 responses: 86 ErrorResponse: 87 description: 'an error occurred' 88 content: 89 application/json: 90 schema: { $ref: '#/components/schemas/Error' } 91 ` 92 v20210916 = ` 93 openapi: 3.0.0 94 x-snyk-api-version: 2021-09-16 95 info: 96 title: 'Validator' 97 version: '0.0.0' 98 paths: 99 /test: 100 post: 101 operationId: newTest 102 description: create a new test 103 parameters: 104 - in: query 105 name: version 106 schema: 107 type: string 108 required: true 109 requestBody: 110 required: true 111 content: 112 application/json: 113 schema: { $ref: '#/components/schemas/TestContents' } 114 responses: 115 '201': 116 description: 'created test' 117 content: 118 application/json: 119 schema: { $ref: '#/components/schemas/TestResource' } 120 '400': { $ref: '#/components/responses/ErrorResponse' } 121 '500': { $ref: '#/components/responses/ErrorResponse' } 122 /test/{id}: 123 get: 124 operationId: getTest 125 description: get a test 126 parameters: 127 - in: path 128 name: id 129 schema: 130 type: string 131 required: true 132 - in: query 133 name: version 134 schema: 135 type: string 136 required: true 137 responses: 138 '200': 139 description: 'respond with test resource' 140 content: 141 application/json: 142 schema: { $ref: '#/components/schemas/TestResource' } 143 '400': { $ref: '#/components/responses/ErrorResponse' } 144 '404': { $ref: '#/components/responses/ErrorResponse' } 145 '500': { $ref: '#/components/responses/ErrorResponse' } 146 components: 147 schemas: 148 TestContents: 149 type: object 150 properties: 151 name: 152 type: string 153 expected: 154 type: number 155 actual: 156 type: number 157 noodles: 158 type: boolean 159 required: [name, expected, actual, noodles] 160 additionalProperties: false 161 TestResource: 162 type: object 163 properties: 164 id: 165 type: string 166 contents: 167 { $ref: '#/components/schemas/TestContents' } 168 required: [id, contents] 169 additionalProperties: false 170 Error: 171 type: object 172 properties: 173 code: 174 type: string 175 message: 176 type: string 177 required: [code, message] 178 additionalProperties: false 179 responses: 180 ErrorResponse: 181 description: 'an error occurred' 182 content: 183 application/json: 184 schema: { $ref: '#/components/schemas/Error' } 185 ` 186 ) 187 188 type validatorTestHandler struct { 189 contentType string 190 getBody, postBody string 191 errBody string 192 errStatusCode int 193 } 194 195 const v20210916_Body = `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10, "noodles": true}}` 196 197 func (h validatorTestHandler) withDefaults() validatorTestHandler { 198 if h.contentType == "" { 199 h.contentType = "application/json" 200 } 201 if h.getBody == "" { 202 h.getBody = v20210916_Body 203 } 204 if h.postBody == "" { 205 h.postBody = v20210916_Body 206 } 207 if h.errBody == "" { 208 h.errBody = `{"code":"bad","message":"bad things"}` 209 } 210 return h 211 } 212 213 var testUrlRE = regexp.MustCompile(`^/test(/\d+)?$`) 214 215 func (h *validatorTestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 216 w.Header().Set("Content-Type", h.contentType) 217 if h.errStatusCode != 0 { 218 w.WriteHeader(h.errStatusCode) 219 _, err := w.Write([]byte(h.errBody)) 220 if err != nil { 221 panic(err) 222 } 223 return 224 } 225 if !testUrlRE.MatchString(r.URL.Path) { 226 w.WriteHeader(http.StatusNotFound) 227 _, err := w.Write([]byte(h.errBody)) 228 if err != nil { 229 panic(err) 230 } 231 return 232 } 233 switch r.Method { 234 case "GET": 235 w.WriteHeader(http.StatusOK) 236 _, err := w.Write([]byte(h.getBody)) 237 if err != nil { 238 panic(err) 239 } 240 case "POST": 241 w.WriteHeader(http.StatusCreated) 242 _, err := w.Write([]byte(h.postBody)) 243 if err != nil { 244 panic(err) 245 } 246 default: 247 http.Error(w, h.errBody, http.StatusMethodNotAllowed) 248 } 249 } 250 251 func TestValidator(t *testing.T) { 252 c := qt.New(t) 253 ctx := context.Background() 254 docs := make([]*openapi3.T, 2) 255 for i, specStr := range []string{v20210820, v20210916} { 256 doc, err := openapi3.NewLoader().LoadFromData([]byte(specStr)) 257 c.Assert(err, qt.IsNil) 258 err = doc.Validate(ctx) 259 c.Assert(err, qt.IsNil) 260 docs[i] = doc 261 } 262 263 type testRequest struct { 264 method, path, body, contentType string 265 } 266 type testResponse struct { 267 statusCode int 268 body string 269 } 270 tests := []struct { 271 name string 272 handler validatorTestHandler 273 options []openapi3filter.ValidatorOption 274 request testRequest 275 response testResponse 276 strict bool 277 }{{ 278 name: "valid GET", 279 handler: validatorTestHandler{}.withDefaults(), 280 request: testRequest{ 281 method: "GET", 282 path: "/test/42?version=2021-09-17", 283 }, 284 response: testResponse{ 285 200, v20210916_Body, 286 }, 287 strict: true, 288 }, { 289 name: "valid POST", 290 handler: validatorTestHandler{}.withDefaults(), 291 request: testRequest{ 292 method: "POST", 293 path: "/test?version=2021-09-17", 294 body: `{"name": "foo", "expected": 9, "actual": 10, "noodles": true}`, 295 contentType: "application/json", 296 }, 297 response: testResponse{ 298 201, v20210916_Body, 299 }, 300 strict: true, 301 }, { 302 name: "not found; no GET operation for /test", 303 handler: validatorTestHandler{}.withDefaults(), 304 request: testRequest{ 305 method: "GET", 306 path: "/test?version=2021-09-17", 307 }, 308 response: testResponse{ 309 404, "Not Found\n", 310 }, 311 strict: true, 312 }, { 313 name: "not found; no POST operation for /test/42", 314 handler: validatorTestHandler{}.withDefaults(), 315 request: testRequest{ 316 method: "POST", 317 path: "/test/42?version=2021-09-17", 318 }, 319 response: testResponse{ 320 404, "Not Found\n", 321 }, 322 strict: true, 323 }, { 324 name: "invalid request; missing version", 325 handler: validatorTestHandler{}.withDefaults(), 326 request: testRequest{ 327 method: "GET", 328 path: "/test/42", 329 }, 330 response: testResponse{ 331 400, "Bad Request\n", 332 }, 333 strict: true, 334 }, { 335 name: "invalid POST request; wrong property type", 336 handler: validatorTestHandler{}.withDefaults(), 337 request: testRequest{ 338 method: "POST", 339 path: "/test?version=2021-09-17", 340 body: `{"name": "foo", "expected": "nine", "actual": "ten", "noodles": false}`, 341 contentType: "application/json", 342 }, 343 response: testResponse{ 344 400, "Bad Request\n", 345 }, 346 strict: true, 347 }, { 348 name: "invalid POST request; missing property", 349 handler: validatorTestHandler{}.withDefaults(), 350 request: testRequest{ 351 method: "POST", 352 path: "/test?version=2021-09-17", 353 body: `{"name": "foo", "expected": 9}`, 354 contentType: "application/json", 355 }, 356 response: testResponse{ 357 400, "Bad Request\n", 358 }, 359 strict: true, 360 }, { 361 name: "invalid POST request; extra property", 362 handler: validatorTestHandler{}.withDefaults(), 363 request: testRequest{ 364 method: "POST", 365 path: "/test?version=2021-09-17", 366 body: `{"name": "foo", "expected": 9, "actual": 10, "noodles": false, "ideal": 8}`, 367 contentType: "application/json", 368 }, 369 response: testResponse{ 370 400, "Bad Request\n", 371 }, 372 strict: true, 373 }, { 374 name: "valid response; 404 error", 375 handler: validatorTestHandler{ 376 contentType: "application/json", 377 errBody: `{"code": "404", "message": "not found"}`, 378 errStatusCode: 404, 379 }.withDefaults(), 380 request: testRequest{ 381 method: "GET", 382 path: "/test/42?version=2021-09-17", 383 }, 384 response: testResponse{ 385 404, `{"code": "404", "message": "not found"}`, 386 }, 387 strict: true, 388 }, { 389 name: "invalid response; invalid error", 390 handler: validatorTestHandler{ 391 errBody: `"not found"`, 392 errStatusCode: 404, 393 }.withDefaults(), 394 request: testRequest{ 395 method: "GET", 396 path: "/test/42?version=2021-09-17", 397 }, 398 response: testResponse{ 399 500, "Internal Server Error\n", 400 }, 401 strict: true, 402 }, { 403 name: "invalid POST response; not strict", 404 handler: validatorTestHandler{ 405 postBody: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`, 406 }.withDefaults(), 407 request: testRequest{ 408 method: "POST", 409 path: "/test?version=2021-09-17", 410 body: `{"name": "foo", "expected": 9, "actual": 10, "noodles": true}`, 411 contentType: "application/json", 412 }, 413 response: testResponse{ 414 statusCode: 201, 415 body: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`, 416 }, 417 strict: false, 418 }, { 419 name: "invalid GET for API in the future", 420 handler: validatorTestHandler{}.withDefaults(), 421 request: testRequest{ 422 method: "GET", 423 path: "/test/42?version=2023-09-17", 424 }, 425 response: testResponse{ 426 400, "Bad Request\n", 427 }, 428 strict: true, 429 }} 430 for i, test := range tests { 431 c.Run(fmt.Sprintf("%d %s", i, test.name), func(c *qt.C) { 432 // Set up a test HTTP server 433 var h http.Handler 434 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 435 h.ServeHTTP(w, r) 436 })) 437 defer s.Close() 438 439 config := versionware.DefaultValidatorConfig 440 config.ServerURL = s.URL 441 config.Options = append(config.Options, append(test.options, openapi3filter.Strict(test.strict))...) 442 v, err := versionware.NewValidator(&config, docs...) 443 c.Assert(err, qt.IsNil) 444 v.SetToday(func() time.Time { 445 return time.Date(2022, time.January, 21, 0, 0, 0, 0, time.UTC) 446 }) 447 h = v.Middleware(&test.handler) 448 449 // Test: make a client request 450 var requestBody io.Reader 451 if test.request.body != "" { 452 requestBody = bytes.NewBufferString(test.request.body) 453 } 454 req, err := http.NewRequest(test.request.method, s.URL+test.request.path, requestBody) 455 c.Assert(err, qt.IsNil) 456 457 if test.request.contentType != "" { 458 req.Header.Set("Content-Type", test.request.contentType) 459 } 460 resp, err := s.Client().Do(req) 461 c.Assert(err, qt.IsNil) 462 defer resp.Body.Close() 463 c.Assert(test.response.statusCode, qt.Equals, resp.StatusCode) 464 465 body, err := ioutil.ReadAll(resp.Body) 466 c.Assert(err, qt.IsNil) 467 c.Assert(test.response.body, qt.Equals, string(body)) 468 }) 469 } 470 } 471 472 func TestValidatorConfig(t *testing.T) { 473 c := qt.New(t) 474 475 // No specs provided 476 _, err := versionware.NewValidator(&versionware.ValidatorConfig{ServerURL: "://"}) 477 c.Assert(err, qt.ErrorMatches, `no OpenAPI versions provided`) 478 479 // Invalid server URL 480 _, err = versionware.NewValidator(&versionware.ValidatorConfig{ServerURL: "://"}, &openapi3.T{}) 481 c.Assert(err, qt.ErrorMatches, `invalid ServerURL: parse "://": missing protocol scheme`) 482 483 // Missing version in OpenAPI spec 484 _, err = versionware.NewValidator(&versionware.ValidatorConfig{ServerURL: "http://example.com"}, &openapi3.T{}) 485 c.Assert(err, qt.ErrorMatches, `extension "x-snyk-api-version" not found`) 486 487 docs := make([]*openapi3.T, 2) 488 for i, specStr := range []string{v20210820, v20210916} { 489 doc, err := openapi3.NewLoader().LoadFromData([]byte(specStr)) 490 c.Assert(err, qt.IsNil) 491 err = doc.Validate(context.Background()) 492 c.Assert(err, qt.IsNil) 493 docs[i] = doc 494 } 495 496 // Invalid server URL 497 _, err = versionware.NewValidator(&versionware.ValidatorConfig{ServerURL: "localhost:8080"}, docs...) 498 c.Assert(err, qt.ErrorMatches, `invalid ServerURL: unsupported scheme "localhost" \(did you forget to specify the scheme://\?\)`) 499 500 // Valid 501 _, err = versionware.NewValidator(&versionware.ValidatorConfig{ServerURL: "http://localhost:8080"}, docs...) 502 c.Assert(err, qt.IsNil) 503 c.Assert(docs[0].Servers[0].URL, qt.Equals, "http://localhost:8080") 504 }