Skip to content

Commit

Permalink
Merge pull request #27 from krnkl/issue-26
Browse files Browse the repository at this point in the history
IgnoreBasePath option to skip validation of requests without base path prefix
  • Loading branch information
krnkl committed Feb 15, 2018
2 parents 8b8b2dd + d18786e commit 388bd13
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 38 deletions.
3 changes: 3 additions & 0 deletions internal/testdata/sample_open_api_v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,9 @@ definitions:
type: integer
phone:
type: string
birthday:
type: string
format: date
user_status:
type: integer
format: int32
Expand Down
12 changes: 7 additions & 5 deletions mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ package revisor

import (
"net/http"
"strings"

"github.com/gorilla/mux"
)

func newSimpleMapper(templateMap map[string][]string) *simpleMapper {
func newSimpleMapper(basePath string, templateMap map[string][]string) *simpleMapper {

router := mux.NewRouter().StrictSlash(true)
mapper := &simpleMapper{router: router}
router := mux.NewRouter().StrictSlash(true).PathPrefix(basePath).Subrouter()
mapper := &simpleMapper{router: router, basePath: basePath}
for k, v := range templateMap {
for _, tmpl := range v {
router.Methods(k).Path(tmpl)
Expand All @@ -20,7 +21,8 @@ func newSimpleMapper(templateMap map[string][]string) *simpleMapper {
}

type simpleMapper struct {
router *mux.Router
router *mux.Router
basePath string
}

// mapRequest returns configured template that matches HTTP
Expand All @@ -35,7 +37,7 @@ func (s *simpleMapper) mapRequest(r *http.Request) (tmpl string, vars map[string
if err != nil {
return "", nil, false
}
return tmpl, match.Vars, true
return strings.TrimPrefix(tmpl, s.basePath), match.Vars, true
}
return "", nil, false
}
4 changes: 2 additions & 2 deletions mapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import (
)

func TestSimpleMapper_New(t *testing.T) {
mapper := newSimpleMapper(map[string][]string{
mapper := newSimpleMapper("", map[string][]string{
"GET": []string{"/path", "/path/{id}"},
})
assert.NotNil(t, mapper)
}

func TestSimpleMapper_MapRequest(t *testing.T) {

mapper := newSimpleMapper(map[string][]string{
mapper := newSimpleMapper("", map[string][]string{
"GET": []string{"/path", "/path/{id}"},
})
mapper.router.Methods("OPTIONS")
Expand Down
35 changes: 28 additions & 7 deletions revisor.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type option func(*apiVerifier)
// options is a struct that holds all possible options
type options struct {
strictContentType bool
ignoreBasePath bool
}

// NoStrictContentType disables strict content-type validation which is enabled by default.
Expand All @@ -36,19 +37,40 @@ func NoStrictContentType(a *apiVerifier) {
a.opts.strictContentType = false
}

// IgnoreBasePath disables check if request path is contains base path configured in API document.
// By default, base path is always checked.
func IgnoreBasePath(a *apiVerifier) {
a.opts.ignoreBasePath = true
}

// NewRequestVerifier returns a function that can be used to verify if request
// satisfies OpenAPI definition constraints
func NewRequestVerifier(definitionPath string, options ...option) (func(*http.Request) error, error) {
a, err := newAPIVerifier(definitionPath)
if err != nil {
return nil, errors.Wrap(err, "failed to create verifier function")
}
a.setOptions(options...)
err = a.initMapper(a.doc.Spec().BasePath)
if err != nil {
return nil, errors.Wrap(err, "failed to create request mapper")
}
return a.verifyRequest, err
}

// NewVerifier returns a function that can be used to verify both - a request
// and the response made in the context of the request
func NewVerifier(definitionPath string, options ...option) (func(*http.Response, *http.Request) error, error) {
a, err := newAPIVerifier(definitionPath)
if err != nil {
return nil, errors.Wrap(err, "failed to create verifier function")
}
a.setOptions(options...)

err = a.initMapper(a.doc.Spec().BasePath)
if err != nil {
return nil, errors.Wrap(err, "failed to create request mapper")
}
return a.verifyRequestAndReponse, err
}

Expand All @@ -65,16 +87,12 @@ func newAPIVerifier(definitionPath string) (*apiVerifier, error) {
if err != nil {
return nil, errors.Wrap(err, "failed to build Document")
}

err = a.initMapper()
if err != nil {
return nil, errors.Wrap(err, "failed to create request mapper")
}
return a, nil
}

func withDefaults(a *apiVerifier) *apiVerifier {
a.opts.strictContentType = true
a.opts.ignoreBasePath = false
return a
}

Expand Down Expand Up @@ -331,7 +349,7 @@ func (a *apiVerifier) initDocument(raw []byte) error {
return nil
}

func (a *apiVerifier) initMapper() error {
func (a *apiVerifier) initMapper(basePath string) error {
requestsMap := make(map[string][]string)
for path, pathItem := range a.doc.Spec().Paths.Paths {
if pathItem.Get != nil {
Expand All @@ -356,7 +374,10 @@ func (a *apiVerifier) initMapper() error {
requestsMap[http.MethodPatch] = append(requestsMap[http.MethodPatch], path)
}
}
a.mapper = newSimpleMapper(requestsMap)
if a.opts.ignoreBasePath {
basePath = ""
}
a.mapper = newSimpleMapper(basePath, requestsMap)
return nil
}

Expand Down
54 changes: 30 additions & 24 deletions revisor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ var (
func TestRequestVerifier(t *testing.T) {
verifier, err := NewRequestVerifier(testdata + sampleV2YAML)
assert.NoError(t, err)
err = verifier(httptest.NewRequest("PUT", "/user/testuser", nil))
err = verifier(httptest.NewRequest("PUT", "/v2/user/testuser", nil))
assert.Regexp(t, "body is empty", err)
}

Expand Down Expand Up @@ -92,6 +92,7 @@ type TestUser struct {
Password string `json:"password,omitempty"`
Phone string `json:"phone,omitempty"`
UserStatus int32 `json:"user_status,omitempty"`
Birthday string `json:"birthday,omitempty"`
AdditionalField string `json:"additional_field,omitempty"`
}

Expand All @@ -100,6 +101,8 @@ func TestAPIVerifierV2_VerifyResponse(t *testing.T) {
a, err := newAPIVerifier(testdata + sampleV2YAML)
require.NoError(t, err)
require.NotNil(t, a)
err = a.initMapper(a.doc.Spec().BasePath)
require.NoError(t, err)

validUser := func() *TestUser {
return &TestUser{
Expand All @@ -124,14 +127,14 @@ func TestAPIVerifierV2_VerifyResponse(t *testing.T) {
}{
{
"valid response",
httptest.NewRequest("GET", "/user/testuser", nil),
httptest.NewRequest("GET", "/v2/user/testuser", nil),
http.StatusOK,
"",
func(u *TestUser) interface{} { return u },
},
{
"validates default",
httptest.NewRequest("GET", "/user/testuser", nil),
httptest.NewRequest("GET", "/v2/user/testuser", nil),
http.StatusInternalServerError,
"",
func(u *TestUser) interface{} {
Expand All @@ -140,7 +143,7 @@ func TestAPIVerifierV2_VerifyResponse(t *testing.T) {
},
{
"no schema configured",
httptest.NewRequest("PUT", "/user/testuser", nil),
httptest.NewRequest("PUT", "/v2/user/testuser", nil),
http.StatusPermanentRedirect,
"neither default nor response schema for current status code is defined",
func(u *TestUser) interface{} { return u },
Expand All @@ -154,35 +157,35 @@ func TestAPIVerifierV2_VerifyResponse(t *testing.T) {
},
{
"no schema for http method",
httptest.NewRequest("HEAD", "/user/testuser", nil),
httptest.NewRequest("HEAD", "/v2/user/testuser", nil),
http.StatusMethodNotAllowed,
"no path template matches current request",
func(u *TestUser) interface{} { return u },
},
{
"schema is not defined but response body is not empty",
httptest.NewRequest("GET", "/user/testuser", nil),
httptest.NewRequest("GET", "/v2/user/testuser", nil),
http.StatusNotFound,
"schema is not defined",
func(u *TestUser) interface{} { return u },
},
{
"missing required field",
httptest.NewRequest("GET", "/user/testuser", nil),
httptest.NewRequest("GET", "/v2/user/testuser", nil),
http.StatusOK,
".id in body is required",
func(u *TestUser) interface{} { u.ID = 0; return u },
},
{
"type incorrect",
httptest.NewRequest("GET", "/user/testuser", nil),
httptest.NewRequest("GET", "/v2/user/testuser", nil),
http.StatusOK,
"firstname in body must be of type integer",
func(u *TestUser) interface{} { u.FirstName = "firstname"; return u },
},
{
"format incorrect",
httptest.NewRequest("GET", "/user/testuser", nil),
httptest.NewRequest("GET", "/v2/user/testuser", nil),
http.StatusOK,
"email in body must be of type email",
func(u *TestUser) interface{} { u.Email = "invalid-email"; return u },
Expand Down Expand Up @@ -211,7 +214,7 @@ func TestAPIVerifierV2_VerifyResponse(t *testing.T) {
t.Run("schema defined but response body is empty", func(t *testing.T) {
rec := httptest.NewRecorder()
rec.WriteHeader(http.StatusOK)
err = a.verifyResponse(rec.Result(), httptest.NewRequest("GET", "/user/testuser", nil))
err = a.verifyResponse(rec.Result(), httptest.NewRequest("GET", "/v2/user/testuser", nil))
assert.Regexp(t, "response body is empty", err)
})

Expand All @@ -223,7 +226,7 @@ func TestAPIVerifierV2_VerifyResponse(t *testing.T) {
_, err = rec.Write(invalid)
assert.NoError(t, err)

err = a.verifyResponse(rec.Result(), httptest.NewRequest("GET", "/user/testuser", nil))
err = a.verifyResponse(rec.Result(), httptest.NewRequest("GET", "/v2/user/testuser", nil))
assert.Regexp(t, "failed to decode response", err)
})

Expand All @@ -232,7 +235,7 @@ func TestAPIVerifierV2_VerifyResponse(t *testing.T) {
rec.WriteHeader(http.StatusOK)
res := rec.Result()
res.Body = ioutil.NopCloser(&brokenReader{})
err = a.verifyResponse(res, httptest.NewRequest("GET", "/user/testuser", nil))
err = a.verifyResponse(res, httptest.NewRequest("GET", "/v2/user/testuser", nil))
assert.Regexp(t, "error reading response body", err)
})

Expand All @@ -242,7 +245,7 @@ func TestAPIVerifierV2_VerifyResponse(t *testing.T) {
rec.WriteHeader(http.StatusOK)
_, err := rec.WriteString("binary-payload")
assert.NoError(t, err)
err = a.verifyResponse(rec.Result(), httptest.NewRequest("GET", "/user/testuser", nil))
err = a.verifyResponse(rec.Result(), httptest.NewRequest("GET", "/v2/user/testuser", nil))
assert.Regexp(t, "Content-Type is not configured", err)
})
// TODO produces is empty in strict mode raises error and don't with desabled strictContentType
Expand All @@ -255,6 +258,8 @@ func TestAPIVerifierV2_VerifyRequest(t *testing.T) {
a, err := newAPIVerifier(testdata + sampleV2YAML)
require.NoError(t, err)
require.NotNil(t, a)
err = a.initMapper(a.doc.Spec().BasePath)
require.NoError(t, err)

validUser := func() *TestUser {
return &TestUser{
Expand All @@ -266,6 +271,7 @@ func TestAPIVerifierV2_VerifyRequest(t *testing.T) {
LastName: "Bar",
Password: "supersecret",
Phone: "+12 (34) 5678 910",
Birthday: "2017-08-01",
UserStatus: 1,
}

Expand All @@ -280,49 +286,49 @@ func TestAPIVerifierV2_VerifyRequest(t *testing.T) {
{
"valid request",
"PUT",
"/user/testuser",
"/v2/user/testuser",
"",
func(u *TestUser) interface{} { return u },
},
{
"failed to find path",
"PATCH",
"/user/testuser",
"/v2/user/testuser",
"no path template matches current request",
func(u *TestUser) interface{} { return nil },
},
{
"no definition but body not empty",
"GET",
"/user/testuser",
"/v2/user/testuser",
"definition is not defined but body is not empty",
func(u *TestUser) interface{} { return u },
},
{
"missing required field",
"PUT",
"/user/testuser",
"/v2/user/testuser",
".id in body is required",
func(u *TestUser) interface{} { u.ID = 0; return u },
},
{
"type incorrect",
"PUT",
"/user/testuser",
"/v2/user/testuser",
"firstname in body must be of type integer",
func(u *TestUser) interface{} { u.FirstName = "firstname"; return u },
},
{
"format incorrect",
"PUT",
"/user/testuser",
"/v2/user/testuser",
"email in body must be of type email",
func(u *TestUser) interface{} { u.Email = "invalid-email"; return u },
},
{
"no definition with payload",
"GET",
"/user/testuser",
"/v2/user/testuser",
"definition is not defined but body is not empty",
func(u *TestUser) interface{} { return u },
},
Expand All @@ -345,28 +351,28 @@ func TestAPIVerifierV2_VerifyRequest(t *testing.T) {
})
}
t.Run("schema defined but request body is empty", func(t *testing.T) {
req, err := http.NewRequest("PUT", "/user/testuser", nil)
req, err := http.NewRequest("PUT", "/v2/user/testuser", nil)
assert.NoError(t, err)
err = a.verifyRequest(req)
assert.Regexp(t, "body is empty", err)
})
t.Run("fails to decode request body", func(t *testing.T) {
invalid := []byte("invalid-json")

req, err := http.NewRequest("PUT", "/user/testuser", bytes.NewReader(invalid))
req, err := http.NewRequest("PUT", "/v2/user/testuser", bytes.NewReader(invalid))
assert.NoError(t, err)
req.Header.Add("Content-Type", "application/json")
err = a.verifyRequest(req)
assert.Regexp(t, "failed to decode request", err)
})
t.Run("fails to read body", func(t *testing.T) {
req, err := http.NewRequest("PUT", "/user/testuser", &brokenReader{})
req, err := http.NewRequest("PUT", "/v2/user/testuser", &brokenReader{})
assert.NoError(t, err)
err = a.verifyRequest(req)
assert.Regexp(t, "error reading request body", err)
})
t.Run("content-type not configured", func(t *testing.T) {
req, err := http.NewRequest("PUT", "/user/testuser", bytes.NewReader([]byte("{}")))
req, err := http.NewRequest("PUT", "/v2/user/testuser", bytes.NewReader([]byte("{}")))
assert.NoError(t, err)
req.Header.Add("Content-Type", "image/jpeg")
err = a.verifyRequest(req)
Expand Down

0 comments on commit 388bd13

Please sign in to comment.