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