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  }