From 1c5da73417699f66c945d50f664568a01f0aa8b8 Mon Sep 17 00:00:00 2001 From: Dennis Ploeger Date: Fri, 22 Dec 2023 08:53:28 +0100 Subject: [PATCH 1/4] feat: Adding a test suite This introduces "handlerinspector", a tool that I might extract into its own module because I didn't find anything like it for Golang. --- .github/workflows/test.yaml | 18 +++ cmd/serve.go | 26 ++-- go.mod | 11 +- go.sum | 34 ++--- internal/api/api.go | 28 ++++ internal/api/auth.go | 17 +-- internal/api/vms.go | 92 +++++------- internal/config.go | 11 -- internal/endpoints/endpoint.go | 2 +- internal/endpoints/status.go | 7 +- internal/endpoints/status_test.go | 24 ++++ internal/endpoints/vms.go | 18 ++- internal/endpoints/vms_test.go | 222 +++++++++++++++++++++++++++++ pkg/handlerinspector/builder.go | 73 ++++++++++ pkg/handlerinspector/conditions.go | 99 +++++++++++++ pkg/handlerinspector/inspector.go | 36 +++++ pkg/handlerinspector/rule.go | 78 ++++++++++ 17 files changed, 663 insertions(+), 133 deletions(-) create mode 100644 .github/workflows/test.yaml create mode 100644 internal/api/api.go delete mode 100644 internal/config.go create mode 100644 internal/endpoints/status_test.go create mode 100644 internal/endpoints/vms_test.go create mode 100644 pkg/handlerinspector/builder.go create mode 100644 pkg/handlerinspector/conditions.go create mode 100644 pkg/handlerinspector/inspector.go create mode 100644 pkg/handlerinspector/rule.go diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..4744018 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,18 @@ +name: Test go code + +on: + - push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + + - name: Test + run: go test ./... -cover \ No newline at end of file diff --git a/cmd/serve.go b/cmd/serve.go index 45a8ca0..10f509b 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -7,14 +7,14 @@ import ( "github.com/sirupsen/logrus" "log" "os" - "vmware-rest-proxy/internal" + api2 "vmware-rest-proxy/internal/api" "vmware-rest-proxy/internal/endpoints" ) +// REST server that proxies request through to a vCenter web service making it easier to request certain details. + func main() { - c := internal.Config{ - Resty: resty.New(), - } + r := resty.New() if l, found := os.LookupEnv("LOG_LEVEL"); found { if lv, err := logrus.ParseLevel(l); err != nil { @@ -34,7 +34,7 @@ func main() { if b, found := os.LookupEnv("BASE_URL"); !found { log.Fatal("Please set BASE_URL to the base url of the vCenter you'd like to access.") } else { - c.Resty.SetBaseURL(b) + r.SetBaseURL(b) } bindAddress := "0.0.0.0:8080" @@ -45,25 +45,25 @@ func main() { if e, found := os.LookupEnv("TLS_INSECURE_SKIP_VERIFY"); found && e == "true" { logrus.Warn("Disabling TLS verification") - c.Resty.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) + r.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) } if p, found := os.LookupEnv("VCENTER_PROXY_URL"); found && p != "" { logrus.Debug("Setting proxy URL") - c.Resty.SetProxy(p) + r.SetProxy(p) } logrus.Debug("Starting server") gin.SetMode(gin.ReleaseMode) - r := gin.Default() + s := gin.Default() logrus.Debug("Disabling trusted proxies because it's recommended by gin") - if err := r.SetTrustedProxies(nil); err != nil { + if err := s.SetTrustedProxies(nil); err != nil { log.Fatalf("Error disabling trusted proxies: %s", err) } - e := []endpoints.Endpoint{&endpoints.VMSEndpoint{}, &endpoints.StatusEndpoint{}} - for _, endpoint := range e { - endpoint.Register(r, c) + api := api2.DefaultVSphereProxyApi{Resty: r} + for _, endpoint := range []endpoints.Endpoint{&endpoints.VMSEndpoint{API: api}, &endpoints.StatusEndpoint{}} { + endpoint.Register(s) } - _ = r.Run(bindAddress) + _ = s.Run(bindAddress) } diff --git a/go.mod b/go.mod index d7dca4c..bed4503 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,15 @@ go 1.21 require ( github.com/gin-gonic/gin v1.9.1 - github.com/go-resty/resty/v2 v2.8.0 + github.com/go-playground/assert/v2 v2.2.0 + github.com/go-resty/resty/v2 v2.10.0 github.com/sirupsen/logrus v1.9.3 ) 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 @@ -24,12 +26,13 @@ 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 - golang.org/x/crypto v0.13.0 // indirect - golang.org/x/net v0.15.0 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index ddfb776..c472a04 100644 --- a/go.sum +++ b/go.sum @@ -21,15 +21,13 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/go-resty/resty/v2 v2.8.0 h1:J29d0JFWwSWrDCysnOK/YjsPMLQTx0TvgJEHVGvf2L8= -github.com/go-resty/resty/v2 v2.8.0/go.mod h1:UCui0cMHekLrSntoMyofdSTaPpinlRHFtPpizuyDW2w= +github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo= +github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -53,7 +51,6 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -67,31 +64,25 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -102,28 +93,26 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -136,5 +125,4 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/api/api.go b/internal/api/api.go new file mode 100644 index 0000000..6292e8a --- /dev/null +++ b/internal/api/api.go @@ -0,0 +1,28 @@ +package api + +//go:generate mockgen -source api.go -package test -destination ../../test/api.go + +import ( + "github.com/go-resty/resty/v2" +) + +type VSphereProxyApi interface { + + // GetSession returns the vmware session id to be used by other requests + GetSession(username string, password string) (string, error) + + // GetVMs returns all VMs from the VM endpoint + GetVMs(username string, password string) ([]VMResponse, error) + + // GetVMTags retrieves a list of tags associated with the given vm + GetVMTags(username string, password string, VMID string) ([]VMTag, error) + + // GetFQDN uses the VMware guest tools to get the fqdn of a VM (if possible) + GetFQDN(username string, password string, VMID string) (string, error) +} + +type DefaultVSphereProxyApi struct { + Resty *resty.Client +} + +var _ VSphereProxyApi = DefaultVSphereProxyApi{} diff --git a/internal/api/auth.go b/internal/api/auth.go index 614faa5..04a3f66 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -3,24 +3,21 @@ package api import ( "fmt" "github.com/sirupsen/logrus" - "vmware-rest-proxy/internal" ) -type GetSessionResponse struct { - User string `json:"user"` -} - var sessionCache map[string]string // GetSession returns the vmware session id to be used by other requests -func GetSession(c internal.Config, username string, password string) (string, error) { +func (d DefaultVSphereProxyApi) GetSession(username string, password string) (string, error) { if sessionCache == nil { sessionCache = make(map[string]string) } if s, ok := sessionCache[username]; ok { logrus.Debugf("Checking cached session for user %s", username) - var getSessionResponse GetSessionResponse - if r, err := c.Resty. + var getSessionResponse struct { + User string `json:"user"` + } + if r, err := d.Resty. R(). SetHeader("vmware-api-session-id", s). SetBasicAuth(username, password). @@ -38,9 +35,9 @@ func GetSession(c internal.Config, username string, password string) (string, er } } } - logrus.Debugf("Creating VMware session for user %s at %s", username, c.Resty.BaseURL) + logrus.Debugf("Creating VMware session for user %s at %s", username, d.Resty.BaseURL) var sessionToken string - if r, err := c.Resty. + if r, err := d.Resty. R(). SetBasicAuth(username, password). SetResult(&sessionToken). diff --git a/internal/api/vms.go b/internal/api/vms.go index 86b96fb..60246f4 100644 --- a/internal/api/vms.go +++ b/internal/api/vms.go @@ -2,8 +2,8 @@ package api import ( "fmt" + "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" - "vmware-rest-proxy/internal" ) // VMResponse is the value in response from the VM endpoint @@ -13,13 +13,13 @@ type VMResponse struct { } // GetVMs returns all VMs from the VM endpoint -func GetVMs(c internal.Config, username string, password string) ([]VMResponse, error) { - if s, err := GetSession(c, username, password); err != nil { +func (d DefaultVSphereProxyApi) GetVMs(username string, password string) ([]VMResponse, error) { + if s, err := d.GetSession(username, password); err != nil { return []VMResponse{}, err } else { - logrus.Debugf("Fetching all VMs from %s for %s", c.Resty.BaseURL, username) + logrus.Debugf("Fetching all VMs from %s for %s", d.Resty.BaseURL, username) var vmsResponse []VMResponse - if r, err := c.Resty. + if r, err := d.Resty. R(). SetHeader("vmware-api-session-id", s). SetResult(&vmsResponse). @@ -37,28 +37,6 @@ func GetVMs(c internal.Config, username string, password string) ([]VMResponse, } } -// TagResponse Holds the value of TagResponse describing a tag from the tag endpoint -type TagResponse struct { - CategoryID string `json:"category_id"` - Name string `json:"name"` -} - -// CategoryResponse holds the value of CategoryResponse describing a category from the category endpoint -type CategoryResponse struct { - Name string `json:"name"` -} - -// VMIDBody holds the object id from IDBody pointing to a vm -type VMIDBody struct { - Type string `json:"type"` - ID string `json:"id"` -} - -// IDBody holds the information required to search for a specific VM -type IDBody struct { - ObjectID VMIDBody `json:"object_id"` -} - // VMTag holds a tag from vSphere type VMTag struct { Value string `json:"value"` @@ -66,20 +44,21 @@ type VMTag struct { } // GetVMTags retrieves a list of tags associated with the given vm -func GetVMTags(c internal.Config, username string, password string, VMID string) ([]VMTag, error) { +func (d DefaultVSphereProxyApi) GetVMTags(username string, password string, VMID string) ([]VMTag, error) { var tags []VMTag - if s, err := GetSession(c, username, password); err != nil { + if s, err := d.GetSession(username, password); err != nil { return tags, err } else { - logrus.Debugf("Loading the attached tags for vm %s from %s for %s", VMID, c.Resty.BaseURL, username) + logrus.Debugf("Loading the attached tags for vm %s from %s for %s", VMID, d.Resty.BaseURL, username) + var attachedTagsResponse []string - if r, err := c.Resty. + if r, err := d.Resty. R(). SetHeader("vmware-api-session-id", s). SetResult(&attachedTagsResponse). - SetBody(IDBody{ObjectID: VMIDBody{ - Type: "VirtualMachine", - ID: VMID, + SetBody(gin.H{"object_id": gin.H{ + "type": "VirtualMachine", + "id": VMID, }}). SetQueryParam("action", "list-attached-tags"). Post("/api/cis/tagging/tag-association"); err != nil { @@ -93,8 +72,11 @@ func GetVMTags(c internal.Config, username string, password string, VMID string) } for _, tagID := range attachedTagsResponse { logrus.Debugf("Loading tag information for tag id %s from vm %s", tagID, VMID) - var tagResponse TagResponse - if r, err := c.Resty. + var tagResponse struct { + CategoryID string `json:"category_id"` + Name string `json:"name"` + } + if r, err := d.Resty. R(). SetHeader("vmware-api-session-id", s). SetResult(&tagResponse). @@ -108,12 +90,14 @@ func GetVMTags(c internal.Config, username string, password string, VMID string) logrus.Error(err) return tags, err } else if r.StatusCode() == 404 || tagResponse.CategoryID == "" { - logrus.Warn("Invalid tag %s. Either not found or has no category", tagID) + logrus.Warnf("Invalid tag %s. Either not found or has no category", tagID) continue } logrus.Debugf("Loading category information for tag %s from vm %s", tagID, VMID) - var categoryResponse CategoryResponse - if r, err := c.Resty. + var categoryResponse struct { + Name string `json:"name"` + } + if r, err := d.Resty. R(). SetHeader("vmware-api-session-id", s). SetResult(&categoryResponse). @@ -140,27 +124,23 @@ func GetVMTags(c internal.Config, username string, password string, VMID string) } } -type GuestNetworkingResponseDNSValues struct { - DomainName string `json:"domain_name"` - HostName string `json:"host_name"` -} - -type GuestNetworkingResponse struct { - DNSValues GuestNetworkingResponseDNSValues `json:"dns_values"` -} - // GetFQDN uses the VMware guest tools to get the fqdn of a VM (if possible) -func GetFQDN(c internal.Config, username string, password string, VMID string) (string, error) { - if s, err := GetSession(c, username, password); err != nil { +func (d DefaultVSphereProxyApi) GetFQDN(username string, password string, VMID string) (string, error) { + if s, err := d.GetSession(username, password); err != nil { return "", err } else { - logrus.Debugf("Trying to figure out the fqdn for vm %s from %s for %s", VMID, c.Resty.BaseURL, username) + logrus.Debugf("Trying to figure out the fqdn for vm %s from %s for %s", VMID, d.Resty.BaseURL, username) - var guestNetworkingResponse GuestNetworkingResponse - if r, err := c.Resty. + var gR struct { + DNSValues struct { + DomainName string `json:"domain_name"` + HostName string `json:"host_name"` + } `json:"dns_values"` + } + if r, err := d.Resty. R(). SetHeader("vmware-api-session-id", s). - SetResult(&guestNetworkingResponse). + SetResult(&gR). SetPathParam("vm", VMID). Get("/api/vcenter/vm/{vm}/guest/networking"); err != nil { logrus.Error(err) @@ -171,8 +151,8 @@ func GetFQDN(c internal.Config, username string, password string, VMID string) ( } return fmt.Sprintf( "%s.%s", - guestNetworkingResponse.DNSValues.HostName, - guestNetworkingResponse.DNSValues.DomainName, + gR.DNSValues.HostName, + gR.DNSValues.DomainName, ), nil } } diff --git a/internal/config.go b/internal/config.go deleted file mode 100644 index 6a7d730..0000000 --- a/internal/config.go +++ /dev/null @@ -1,11 +0,0 @@ -package internal - -import ( - "github.com/go-resty/resty/v2" -) - -// Config holds shared configuration data -type Config struct { - // The prepared resty client to use - Resty *resty.Client -} diff --git a/internal/endpoints/endpoint.go b/internal/endpoints/endpoint.go index fe92f5e..c5dd334 100644 --- a/internal/endpoints/endpoint.go +++ b/internal/endpoints/endpoint.go @@ -13,7 +13,7 @@ import ( // The Endpoint interface is used to have a common API for all available endpoints type Endpoint interface { // Register needs to be available to register the endpoint with the engine - Register(engine *gin.Engine, config internal.Config) + Register(engine *gin.Engine) } // RequestData holds username and password from a request diff --git a/internal/endpoints/status.go b/internal/endpoints/status.go index 56cadfa..0bef567 100644 --- a/internal/endpoints/status.go +++ b/internal/endpoints/status.go @@ -2,17 +2,14 @@ package endpoints import ( "github.com/gin-gonic/gin" - "vmware-rest-proxy/internal" ) // The StatusEndpoint returns information about VMs in vSphere -type StatusEndpoint struct { - config internal.Config -} +type StatusEndpoint struct{} var _ Endpoint = &StatusEndpoint{} -func (StatusEndpoint) Register(engine *gin.Engine, _ internal.Config) { +func (StatusEndpoint) Register(engine *gin.Engine) { engine.GET("/status", func(context *gin.Context) { context.JSON(200, gin.H{ "status": "running", diff --git a/internal/endpoints/status_test.go b/internal/endpoints/status_test.go new file mode 100644 index 0000000..42ceb1d --- /dev/null +++ b/internal/endpoints/status_test.go @@ -0,0 +1,24 @@ +package endpoints + +import ( + "github.com/gin-gonic/gin" + "github.com/go-playground/assert/v2" + "io" + "net/http" + "net/http/httptest" + "testing" +) + +// TestStatusEndpoint tests the status endpoint +func TestStatusEndpoint(t *testing.T) { + r := gin.Default() + s := StatusEndpoint{} + s.Register(r) + req, _ := http.NewRequest("GET", "/status", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + responseData, _ := io.ReadAll(w.Body) + assert.Equal(t, `{"status":"running"}`, string(responseData)) + assert.Equal(t, http.StatusOK, w.Code) +} diff --git a/internal/endpoints/vms.go b/internal/endpoints/vms.go index 4243534..8dd6090 100644 --- a/internal/endpoints/vms.go +++ b/internal/endpoints/vms.go @@ -3,13 +3,12 @@ package endpoints import ( "fmt" "github.com/gin-gonic/gin" - "vmware-rest-proxy/internal" "vmware-rest-proxy/internal/api" ) // The VMSEndpoint returns information about VMs in vSphere type VMSEndpoint struct { - config internal.Config + API api.VSphereProxyApi } var _ Endpoint = &VMSEndpoint{} @@ -19,17 +18,16 @@ type VMBinding struct { ID string `uri:"vm" binding:"required"` } -func (V *VMSEndpoint) Register(engine *gin.Engine, config internal.Config) { - V.config = config +func (V *VMSEndpoint) Register(engine *gin.Engine) { engine.GET("/vms", V.getVMS) engine.GET("/vms/:vm/tags", V.getVMTags) engine.GET("/vms/:vm/fqdn", V.getFQDN) } -// getVMS exposes all vms of the vCenter at /vms +// getVMS exposes all VMS of the vCenter at /VMS func (V *VMSEndpoint) getVMS(context *gin.Context) { if r, ok := HandleRequest(context); ok { - if vms, err := api.GetVMs(V.config, r.Username, r.Password); err != nil { + if vms, err := V.API.GetVMs(r.Username, r.Password); err != nil { context.AbortWithStatusJSON(500, gin.H{ "error": fmt.Sprintf("Error getting VMs: %s", err), }) @@ -44,7 +42,7 @@ func (V *VMSEndpoint) getVMS(context *gin.Context) { } } -// getVMTags exposes a list of tags associated with a vm at /vms/:vm/tags +// getVMTags exposes a list of tags associated with a vm at /VMS/:vm/tags func (V *VMSEndpoint) getVMTags(context *gin.Context) { if r, ok := HandleRequest(context); ok { var vm VMBinding @@ -54,14 +52,14 @@ func (V *VMSEndpoint) getVMTags(context *gin.Context) { }) return } - if tags, err := api.GetVMTags(V.config, r.Username, r.Password, vm.ID); err != nil { + if tags, err := V.API.GetVMTags(r.Username, r.Password, vm.ID); err != nil { context.AbortWithStatusJSON(500, gin.H{ "error": fmt.Sprintf("Error getting tags: %s", err), }) } else { context.JSON(200, gin.H{ "tags": gin.H{ - "count": len(tags), + "Count": len(tags), "tags": tags, }, }) @@ -78,7 +76,7 @@ func (V *VMSEndpoint) getFQDN(context *gin.Context) { }) return } - if fqdn, err := api.GetFQDN(V.config, r.Username, r.Password, vm.ID); err != nil { + if fqdn, err := V.API.GetFQDN(r.Username, r.Password, vm.ID); err != nil { context.AbortWithStatusJSON(500, gin.H{ "error": fmt.Sprintf("Error getting tags: %s", err), }) diff --git a/internal/endpoints/vms_test.go b/internal/endpoints/vms_test.go new file mode 100644 index 0000000..3b482a9 --- /dev/null +++ b/internal/endpoints/vms_test.go @@ -0,0 +1,222 @@ +package endpoints + +import ( + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + "github.com/go-playground/assert/v2" + "github.com/go-resty/resty/v2" + "net/http" + "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")). + ReturnBodyFromFunction(func(r *http.Request) string { + if r.Method == "POST" { + return fmt.Sprintf(`"%s"`, AUTHTOKEN) + } else { + return `{"user": "test"}` + } + }). + ReturnHeader("Content-Type", "application/json"). + Build() + +// testRequests is a short helper function to call requests on the build-up endpoints and mock server +// requires a http.Handler and a list of http.Request objects +func testRequests(handler http.Handler, requests []*http.Request) *httptest.ResponseRecorder { + s := httptest.NewServer(handler) + defer s.Close() + + r := resty.New().SetBaseURL(s.URL).SetBasicAuth("test", "test") + a := api.DefaultVSphereProxyApi{Resty: r} + sub := VMSEndpoint{API: a} + g := gin.Default() + sub.Register(g) + + w := httptest.NewRecorder() + + for _, request := range requests { + g.ServeHTTP(w, request) + + } + return w +} + +// TestVMSEndpoint_GetSession checks if the session endpoint is called +func TestVMSEndpoint_GetSession(t *testing.T) { + b := handlerinspector.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)). + ReturnBody("[]"). + ReturnHeader("Content-Type", "application/json"). + Build(), + ) + + req, _ := http.NewRequest("GET", "/vms", nil) + req.SetBasicAuth("test", "test") + w := testRequests(b.Build(), []*http.Request{req}) + + i := handlerinspector.NewInspector(b) + assert.Equal(t, i.Failed(), false) + assert.Equal(t, i.AllWereCalled(), true) + assert.Equal(t, http.StatusOK, w.Code) +} + +// TestVMSEndpoint_GetVMS checks the vms endpoint +func TestVMSEndpoint_GetVMS(t *testing.T) { + b := handlerinspector.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)). + ReturnBody(`[{"VM": "1", "Name": "test1"}, {"VM": "2", "Name": "test2"}]`). + ReturnHeader("Content-Type", "application/json"). + Build(), + ) + req, _ := http.NewRequest("GET", "/vms", nil) + req.SetBasicAuth("test", "test") + w := testRequests(b.Build(), []*http.Request{req}) + + type resp struct { + VMS struct { + Count int `json:"count"` + VMS []api.VMResponse `json:"vms"` + } `json:"vms"` + } + var r resp + err := json.NewDecoder(w.Body).Decode(&r) + assert.Equal(t, err, nil) + + i := handlerinspector.NewInspector(b) + assert.Equal(t, i.Failed(), false) + assert.Equal(t, i.AllWereCalled(), true) + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, r.VMS.Count, 2) + assert.Equal(t, len(r.VMS.VMS), 2) + assert.Equal(t, r.VMS.VMS[0].VM, "1") + assert.Equal(t, r.VMS.VMS[0].Name, "test1") + assert.Equal(t, r.VMS.VMS[1].VM, "2") + assert.Equal(t, r.VMS.VMS[1].Name, "test2") +} + +// TestVMSEndpoint_GetVMTags checks the /vms/tags endpoint +func TestVMSEndpoint_GetVMTags(t *testing.T) { + b := handlerinspector.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"}}`)). + 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)). + 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)). + 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)). + 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)). + ReturnBody(`{"name": "testcategory2"}`). + ReturnHeader("Content-Type", "application/json"). + Build(), + ) + req, _ := http.NewRequest("GET", "/vms/1/tags", nil) + req.SetBasicAuth("test", "test") + w := testRequests(b.Build(), []*http.Request{req}) + + type resp struct { + Tags struct { + Count int `json:"count"` + Tags []api.VMTag `json:"tags"` + } `json:"tags"` + } + var r resp + err := json.NewDecoder(w.Body).Decode(&r) + assert.Equal(t, err, nil) + + i := handlerinspector.NewInspector(b) + assert.Equal(t, i.Failed(), false) + assert.Equal(t, i.AllWereCalled(), true) + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, r.Tags.Count, 2) + assert.Equal(t, len(r.Tags.Tags), 2) + assert.Equal(t, r.Tags.Tags[0].Category, "testcategory1") + assert.Equal(t, r.Tags.Tags[0].Value, "testtag1") + assert.Equal(t, r.Tags.Tags[1].Category, "testcategory2") + assert.Equal(t, r.Tags.Tags[1].Value, "testtag2") +} + +// TestVMSEndpoint_GetFQDN checks the vm/fqdn endpoint +func TestVMSEndpoint_GetFQDN(t *testing.T) { + b := handlerinspector.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)). + ReturnBody(`{"dns_values":{"domain_name":"example.com","host_name":"test"}}`). + ReturnHeader("Content-Type", "application/json"). + Build(), + ) + req, _ := http.NewRequest("GET", "/vms/1/fqdn", nil) + req.SetBasicAuth("test", "test") + w := testRequests(b.Build(), []*http.Request{req}) + + type resp struct { + FQDN string + } + var r resp + err := json.NewDecoder(w.Body).Decode(&r) + assert.Equal(t, err, nil) + + i := handlerinspector.NewInspector(b) + assert.Equal(t, i.Failed(), false) + assert.Equal(t, i.AllWereCalled(), true) + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, r.FQDN, "test.example.com") +} diff --git a/pkg/handlerinspector/builder.go b/pkg/handlerinspector/builder.go new file mode 100644 index 0000000..305da74 --- /dev/null +++ b/pkg/handlerinspector/builder.go @@ -0,0 +1,73 @@ +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 new file mode 100644 index 0000000..ce8eb2e --- /dev/null +++ b/pkg/handlerinspector/conditions.go @@ -0,0 +1,99 @@ +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 new file mode 100644 index 0000000..75529b3 --- /dev/null +++ b/pkg/handlerinspector/inspector.go @@ -0,0 +1,36 @@ +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 new file mode 100644 index 0000000..4f90880 --- /dev/null +++ b/pkg/handlerinspector/rule.go @@ -0,0 +1,78 @@ +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 +} From b777e61aeebf5e8fdd64585706b32106e8e336d8 Mon Sep 17 00:00:00 2001 From: Dennis Ploeger Date: Fri, 22 Dec 2023 08:55:03 +0100 Subject: [PATCH 2/4] chore: Correct hooks for workflows --- .github/workflows/chart.yaml | 3 ++- .github/workflows/image.yaml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/chart.yaml b/.github/workflows/chart.yaml index 21e975f..3edaca0 100644 --- a/.github/workflows/chart.yaml +++ b/.github/workflows/chart.yaml @@ -2,9 +2,10 @@ name: Create and publish the Helm chart on: push: + branches: + - main paths: - "charts/**/*" - - ".github/**/*" jobs: build-and-push-chart: diff --git a/.github/workflows/image.yaml b/.github/workflows/image.yaml index 477b3e3..11dac85 100644 --- a/.github/workflows/image.yaml +++ b/.github/workflows/image.yaml @@ -6,6 +6,7 @@ on: - main paths-ignore: - "charts/*" + - ".github/**/*" tags: - '*.*.*' From 6931fe920fa042507e73c89dd653ede052394aa1 Mon Sep 17 00:00:00 2001 From: Dennis Ploeger Date: Fri, 22 Dec 2023 08:58:29 +0100 Subject: [PATCH 3/4] feat: Coverage report --- .github/workflows/test.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4744018..5d34d0b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,4 +15,12 @@ jobs: go-version: '1.21' - name: Test - run: go test ./... -cover \ No newline at end of file + run: | + go test ./... -coverprofile cover.out + go tool cover -html=cover.out -o=cover.html + + - name: Archive code coverage results + uses: actions/upload-artifact@v3 + with: + name: code-coverage-report + path: cover.html \ No newline at end of file From a848a50d34e75be676c38df0fdbcbe7fe42c3341 Mon Sep 17 00:00:00 2001 From: Dennis Ploeger Date: Fri, 22 Dec 2023 10:29:13 +0100 Subject: [PATCH 4/4] feat: Switch to public handlerinspector module --- go.mod | 3 +- go.sum | 2 + internal/endpoints/vms_test.go | 93 ++++++++++++++-------------- pkg/handlerinspector/builder.go | 73 ---------------------- pkg/handlerinspector/conditions.go | 99 ------------------------------ pkg/handlerinspector/inspector.go | 36 ----------- pkg/handlerinspector/rule.go | 78 ----------------------- 7 files changed, 50 insertions(+), 334 deletions(-) delete mode 100644 pkg/handlerinspector/builder.go delete mode 100644 pkg/handlerinspector/conditions.go delete mode 100644 pkg/handlerinspector/inspector.go delete mode 100644 pkg/handlerinspector/rule.go 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 -}