diff --git a/go.mod b/go.mod index bed4503..daf8fc3 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module vmware-rest-proxy go 1.21 require ( + github.com/dodevops/golang-handlerinspector v0.1.0 github.com/gin-gonic/gin v1.9.1 github.com/go-playground/assert/v2 v2.2.0 github.com/go-resty/resty/v2 v2.10.0 @@ -12,7 +13,6 @@ require ( require ( github.com/bytedance/sonic v1.9.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -26,7 +26,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect diff --git a/go.sum b/go.sum index c472a04..6a11366 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583j github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dodevops/golang-handlerinspector v0.1.0 h1:iPSaw9izmNSvoVGUn3btGbpjXz6iaus27QEeGv6tKrI= +github.com/dodevops/golang-handlerinspector v0.1.0/go.mod h1:0oVA3heviGeSZQ1eqt/yjcouhP3B95g/2l732lK48r4= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= diff --git a/internal/endpoints/vms_test.go b/internal/endpoints/vms_test.go index 3b482a9..86ab618 100644 --- a/internal/endpoints/vms_test.go +++ b/internal/endpoints/vms_test.go @@ -3,6 +3,8 @@ package endpoints import ( "encoding/json" "fmt" + "github.com/dodevops/golang-handlerinspector/pkg/builder" + "github.com/dodevops/golang-handlerinspector/pkg/inspector" "github.com/gin-gonic/gin" "github.com/go-playground/assert/v2" "github.com/go-resty/resty/v2" @@ -10,15 +12,14 @@ import ( "net/http/httptest" "testing" "vmware-rest-proxy/internal/api" - "vmware-rest-proxy/pkg/handlerinspector" ) // AUTHTOKEN holds a test token that should be issued and used in all tests const AUTHTOKEN = "testtoken" -// sessionRule holds a handlerinspector Rule for the session api -var sessionRule = handlerinspector.NewRule("session"). - WithCondition(handlerinspector.HasPath("/api/session")). +// sessionRule holds a builder Rule for the session api +var sessionRule = builder.NewRule("session"). + WithCondition(builder.HasPath("/api/session")). ReturnBodyFromFunction(func(r *http.Request) string { if r.Method == "POST" { return fmt.Sprintf(`"%s"`, AUTHTOKEN) @@ -52,13 +53,13 @@ func testRequests(handler http.Handler, requests []*http.Request) *httptest.Resp // TestVMSEndpoint_GetSession checks if the session endpoint is called func TestVMSEndpoint_GetSession(t *testing.T) { - b := handlerinspector.NewBuilder(). + b := builder.NewBuilder(). WithRule(sessionRule). WithRule( - handlerinspector.NewRule("vms"). - WithCondition(handlerinspector.HasPath("/api/vcenter/vm")). - WithCondition(handlerinspector.HasMethod("GET")). - WithCondition(handlerinspector.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). + builder.NewRule("vms"). + WithCondition(builder.HasPath("/api/vcenter/vm")). + WithCondition(builder.HasMethod("GET")). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). ReturnBody("[]"). ReturnHeader("Content-Type", "application/json"). Build(), @@ -68,7 +69,7 @@ func TestVMSEndpoint_GetSession(t *testing.T) { req.SetBasicAuth("test", "test") w := testRequests(b.Build(), []*http.Request{req}) - i := handlerinspector.NewInspector(b) + i := inspector.NewInspector(b) assert.Equal(t, i.Failed(), false) assert.Equal(t, i.AllWereCalled(), true) assert.Equal(t, http.StatusOK, w.Code) @@ -76,13 +77,13 @@ func TestVMSEndpoint_GetSession(t *testing.T) { // TestVMSEndpoint_GetVMS checks the vms endpoint func TestVMSEndpoint_GetVMS(t *testing.T) { - b := handlerinspector.NewBuilder(). + b := builder.NewBuilder(). WithRule(sessionRule). WithRule( - handlerinspector.NewRule("vms"). - WithCondition(handlerinspector.HasPath("/api/vcenter/vm")). - WithCondition(handlerinspector.HasMethod("GET")). - WithCondition(handlerinspector.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). + builder.NewRule("vms"). + WithCondition(builder.HasPath("/api/vcenter/vm")). + WithCondition(builder.HasMethod("GET")). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). ReturnBody(`[{"VM": "1", "Name": "test1"}, {"VM": "2", "Name": "test2"}]`). ReturnHeader("Content-Type", "application/json"). Build(), @@ -101,7 +102,7 @@ func TestVMSEndpoint_GetVMS(t *testing.T) { err := json.NewDecoder(w.Body).Decode(&r) assert.Equal(t, err, nil) - i := handlerinspector.NewInspector(b) + i := inspector.NewInspector(b) assert.Equal(t, i.Failed(), false) assert.Equal(t, i.AllWereCalled(), true) assert.Equal(t, http.StatusOK, w.Code) @@ -115,51 +116,51 @@ func TestVMSEndpoint_GetVMS(t *testing.T) { // TestVMSEndpoint_GetVMTags checks the /vms/tags endpoint func TestVMSEndpoint_GetVMTags(t *testing.T) { - b := handlerinspector.NewBuilder(). + b := builder.NewBuilder(). WithRule(sessionRule). WithRule( - handlerinspector.NewRule("list-associated-tags"). - WithCondition(handlerinspector.HasPath("/api/cis/tagging/tag-association")). - WithCondition(handlerinspector.HasMethod("POST")). - WithCondition(handlerinspector.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). - WithCondition(handlerinspector.HasQueryParam("action", "list-attached-tags")). - WithCondition(handlerinspector.HasBody(`{"object_id":{"id":"1","type":"VirtualMachine"}}`)). + builder.NewRule("list-associated-tags"). + WithCondition(builder.HasPath("/api/cis/tagging/tag-association")). + WithCondition(builder.HasMethod("POST")). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). + WithCondition(builder.HasQueryParam("action", "list-attached-tags")). + WithCondition(builder.HasBody(`{"object_id":{"id":"1","type":"VirtualMachine"}}`)). ReturnBody(`["1", "2"]`). ReturnHeader("Content-Type", "application/json"). Build(), ). WithRule( - handlerinspector.NewRule("tag-data-1"). - WithCondition(handlerinspector.HasPath("/api/cis/tagging/tag/1")). - WithCondition(handlerinspector.HasMethod("GET")). - WithCondition(handlerinspector.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). + builder.NewRule("tag-data-1"). + WithCondition(builder.HasPath("/api/cis/tagging/tag/1")). + WithCondition(builder.HasMethod("GET")). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). ReturnBody(`{"category_id": "1", "name": "testtag1"}`). ReturnHeader("Content-Type", "application/json"). Build(), ). WithRule( - handlerinspector.NewRule("tag-data-2"). - WithCondition(handlerinspector.HasPath("/api/cis/tagging/tag/2")). - WithCondition(handlerinspector.HasMethod("GET")). - WithCondition(handlerinspector.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). + builder.NewRule("tag-data-2"). + WithCondition(builder.HasPath("/api/cis/tagging/tag/2")). + WithCondition(builder.HasMethod("GET")). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). ReturnBody(`{"category_id": "2", "name": "testtag2"}`). ReturnHeader("Content-Type", "application/json"). Build(), ). WithRule( - handlerinspector.NewRule("tag-category-1"). - WithCondition(handlerinspector.HasPath("/api/cis/tagging/category/1")). - WithCondition(handlerinspector.HasMethod("GET")). - WithCondition(handlerinspector.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). + builder.NewRule("tag-category-1"). + WithCondition(builder.HasPath("/api/cis/tagging/category/1")). + WithCondition(builder.HasMethod("GET")). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). ReturnBody(`{"name": "testcategory1"}`). ReturnHeader("Content-Type", "application/json"). Build(), ). WithRule( - handlerinspector.NewRule("tag-category-2"). - WithCondition(handlerinspector.HasPath("/api/cis/tagging/category/2")). - WithCondition(handlerinspector.HasMethod("GET")). - WithCondition(handlerinspector.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). + builder.NewRule("tag-category-2"). + WithCondition(builder.HasPath("/api/cis/tagging/category/2")). + WithCondition(builder.HasMethod("GET")). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). ReturnBody(`{"name": "testcategory2"}`). ReturnHeader("Content-Type", "application/json"). Build(), @@ -178,7 +179,7 @@ func TestVMSEndpoint_GetVMTags(t *testing.T) { err := json.NewDecoder(w.Body).Decode(&r) assert.Equal(t, err, nil) - i := handlerinspector.NewInspector(b) + i := inspector.NewInspector(b) assert.Equal(t, i.Failed(), false) assert.Equal(t, i.AllWereCalled(), true) assert.Equal(t, http.StatusOK, w.Code) @@ -192,13 +193,13 @@ func TestVMSEndpoint_GetVMTags(t *testing.T) { // TestVMSEndpoint_GetFQDN checks the vm/fqdn endpoint func TestVMSEndpoint_GetFQDN(t *testing.T) { - b := handlerinspector.NewBuilder(). + b := builder.NewBuilder(). WithRule(sessionRule). WithRule( - handlerinspector.NewRule("get-fqdm"). - WithCondition(handlerinspector.HasPath("/api/vcenter/vm/1/guest/networking")). - WithCondition(handlerinspector.HasMethod("GET")). - WithCondition(handlerinspector.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). + builder.NewRule("get-fqdm"). + WithCondition(builder.HasPath("/api/vcenter/vm/1/guest/networking")). + WithCondition(builder.HasMethod("GET")). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). ReturnBody(`{"dns_values":{"domain_name":"example.com","host_name":"test"}}`). ReturnHeader("Content-Type", "application/json"). Build(), @@ -214,7 +215,7 @@ func TestVMSEndpoint_GetFQDN(t *testing.T) { err := json.NewDecoder(w.Body).Decode(&r) assert.Equal(t, err, nil) - i := handlerinspector.NewInspector(b) + i := inspector.NewInspector(b) assert.Equal(t, i.Failed(), false) assert.Equal(t, i.AllWereCalled(), true) assert.Equal(t, http.StatusOK, w.Code) diff --git a/pkg/handlerinspector/builder.go b/pkg/handlerinspector/builder.go deleted file mode 100644 index 305da74..0000000 --- a/pkg/handlerinspector/builder.go +++ /dev/null @@ -1,73 +0,0 @@ -package handlerinspector - -import ( - "fmt" - "github.com/sirupsen/logrus" - "net/http" -) - -// HandlerBuilder is a builder interface to generating http.handlers for mock servers -type HandlerBuilder struct { - // rules is a list of HandlerInspector rules - rules []Rule - // called records which rules have been called for the Inspector - called map[string]int - // failed records if no non-matching rule was called for the Inspector - failed bool -} - -// NewBuilder creates a new HandlerBuilder. Start here. -func NewBuilder() *HandlerBuilder { - return &HandlerBuilder{} -} - -// WithRule appends a Rule to the builder -func (b *HandlerBuilder) WithRule(r Rule) *HandlerBuilder { - b.rules = append(b.rules, r) - return b -} - -// Build builds a http.Handler from the rules in the builder -func (b *HandlerBuilder) Build() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - logrus.Debugf("Checking rules for request %v", r) - foundRule := false - for _, rule := range b.rules { - logrus.Debugf("Checking rule %s", rule.name) - matches := true - for _, c := range rule.conditions { - logrus.Debugf("Checking condition %v", c) - if !c.Matches(r) { - matches = false - } - } - if matches { - foundRule = true - logrus.Debugf("Carrying out matching rule %s", rule.name) - if b.called == nil { - b.called = make(map[string]int) - } - if v, ok := b.called[rule.name]; ok { - v++ - } else { - b.called[rule.name] = 1 - } - for key, value := range rule.headers { - w.Header().Add(key, value) - } - w.WriteHeader(rule.code) - if rule.useBodyFunc { - _, _ = fmt.Fprint(w, rule.bodyFunc(r)) - } else { - _, _ = fmt.Fprint(w, rule.body) - } - return - } - } - - if !foundRule { - logrus.Errorf("Didn't find a rule for request %v with body %v", r, r.Body) - b.failed = true - } - }) -} diff --git a/pkg/handlerinspector/conditions.go b/pkg/handlerinspector/conditions.go deleted file mode 100644 index ce8eb2e..0000000 --- a/pkg/handlerinspector/conditions.go +++ /dev/null @@ -1,99 +0,0 @@ -package handlerinspector - -import ( - "io" - "net/http" -) - -// A Condition to match the incoming http.Request again -type Condition interface { - Matches(*http.Request) bool -} - -// HasPathCondition checks if the given path was called -type HasPathCondition struct { - path string -} - -// HasPath creates a new HasPathCondition -func HasPath(path string) HasPathCondition { - return HasPathCondition{ - path: path, - } -} - -func (h HasPathCondition) Matches(request *http.Request) bool { - return request.URL.Path == h.path -} - -// HasMethodCondition checks if the given method was used -type HasMethodCondition struct { - method string -} - -// HasMethod creates a new HasMethodCondition -func HasMethod(method string) HasMethodCondition { - return HasMethodCondition{ - method: method, - } -} - -func (h HasMethodCondition) Matches(request *http.Request) bool { - return request.Method == h.method -} - -// HasHeaderCondition checks if the given header (key/value) exist in the request -type HasHeaderCondition struct { - key string - value string -} - -// HasHeader creates a new HasHeaderCondition -func HasHeader(key string, value string) *HasHeaderCondition { - return &HasHeaderCondition{ - key: key, - value: value, - } -} - -func (h HasHeaderCondition) Matches(request *http.Request) bool { - return request.Header.Get(h.key) == h.value -} - -// HasQueryParamCondition checks if the given query param (key/value) was specified -type HasQueryParamCondition struct { - key string - value string -} - -// HasQueryParam creates a new HasHeaderCondition -func HasQueryParam(key string, value string) *HasQueryParamCondition { - return &HasQueryParamCondition{ - key: key, - value: value, - } -} - -func (h HasQueryParamCondition) Matches(request *http.Request) bool { - return request.URL.Query().Get(h.key) == h.value -} - -// HasBodyCondition checks if the request's body is equal to the given string -type HasBodyCondition struct { - body string -} - -// HasBody creates a new HasBodyCondition -func HasBody(body string) *HasBodyCondition { - return &HasBodyCondition{ - body: body, - } -} - -func (h HasBodyCondition) Matches(request *http.Request) bool { - if b, err := io.ReadAll(request.Body); err != nil { - return false - } else { - return string(b) == h.body - } -} diff --git a/pkg/handlerinspector/inspector.go b/pkg/handlerinspector/inspector.go deleted file mode 100644 index 75529b3..0000000 --- a/pkg/handlerinspector/inspector.go +++ /dev/null @@ -1,36 +0,0 @@ -package handlerinspector - -// The Inspector inspects the given HandlerBuilder instance and answers questions about its usage -type Inspector struct { - hb *HandlerBuilder -} - -// NewInspector creates a new Inspector -func NewInspector(hb *HandlerBuilder) *Inspector { - return &Inspector{hb: hb} -} - -// Called returns how often rule ruleName was called -func (i *Inspector) Called(ruleName string) int { - if v, ok := i.hb.called[ruleName]; ok { - return v - } else { - return 0 - } -} - -// Failed returns if the handler fails at least once -func (i *Inspector) Failed() bool { - return i.hb.failed -} - -// AllWereCalled returns whether all rules were called at least once -func (i *Inspector) AllWereCalled() bool { - allWereCalled := true - for _, rule := range i.hb.rules { - if i.Called(rule.name) == 0 { - allWereCalled = false - } - } - return allWereCalled -} diff --git a/pkg/handlerinspector/rule.go b/pkg/handlerinspector/rule.go deleted file mode 100644 index 4f90880..0000000 --- a/pkg/handlerinspector/rule.go +++ /dev/null @@ -1,78 +0,0 @@ -package handlerinspector - -import "net/http" - -// A Rule is a collection of conditions with a name that will apply headers, body and return code if all conditions match -type Rule struct { - // The name of this rule - name string - // The conditions that must be met for this rule to be applied - conditions []Condition - // headers that should be set for the response - headers map[string]string - // the body to set in the response - body string - // the bodyFunc can return the body dynamically from a request - bodyFunc func(r *http.Request) string - // useBodyFunc tells the builder to use the bodyFunc instead of the body - useBodyFunc bool - // the code to set for the response code - code int -} - -// The RuleBuilder is a builder interface to generate a new Rule -type RuleBuilder struct { - rule Rule -} - -// NewRule creates a new named rule -func NewRule(name string) *RuleBuilder { - return (&RuleBuilder{}).Named(name) -} - -// Named sets the name for the rule -func (r *RuleBuilder) Named(n string) *RuleBuilder { - r.rule.name = n - return r -} - -// WithCondition adds a new Condition to the rule -func (r *RuleBuilder) WithCondition(c Condition) *RuleBuilder { - r.rule.conditions = append(r.rule.conditions, c) - return r -} - -// ReturnHeader adds a new header to set on the response -func (r *RuleBuilder) ReturnHeader(key string, value string) *RuleBuilder { - if r.rule.headers == nil { - r.rule.headers = make(map[string]string) - } - r.rule.headers[key] = value - return r -} - -// ReturnBody sets the body to set on the response -func (r *RuleBuilder) ReturnBody(body string) *RuleBuilder { - r.rule.body = body - return r -} - -func (r *RuleBuilder) ReturnBodyFromFunction(f func(r *http.Request) string) *RuleBuilder { - r.rule.bodyFunc = f - r.rule.useBodyFunc = true - return r -} - -// ReturnCode sets the response code in the response -func (r *RuleBuilder) ReturnCode(code int) *RuleBuilder { - r.rule.code = code - return r -} - -// Build creates a new rule -func (r *RuleBuilder) Build() Rule { - if r.rule.code == 0 { - r.rule.code = 200 - } - return r.rule -}