From 63e1f908616fad0db8e1a8248a8667c0099b3360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gerdau?= Date: Sun, 21 Apr 2024 19:12:51 +0200 Subject: [PATCH] WIP add tenant tests --- tenant/common_test.go | 129 ++++++++++++++++++ tenant/go.mod | 5 + tenant/go.sum | 6 + tenant/middleware_common_test.go | 93 +++++++++++++ .../errors/missing_auth_claims_error.json | 7 + 5 files changed, 240 insertions(+) create mode 100644 tenant/common_test.go create mode 100644 tenant/middleware_common_test.go create mode 100644 tenant/testdata/errors/missing_auth_claims_error.json diff --git a/tenant/common_test.go b/tenant/common_test.go new file mode 100644 index 0000000..3e6d881 --- /dev/null +++ b/tenant/common_test.go @@ -0,0 +1,129 @@ +package tenant_test + +import ( + "github.com/google/uuid" + "github.com/justinas/alice" + "github.com/kernle32dll/turtleware" + "github.com/kernle32dll/turtleware/tenant" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/stretchr/testify/suite" + + "fmt" + "io" + "net/http" + "os" + "path" + "strings" +) + +type CommonSuite struct { + suite.Suite + + entityUUID string + userUUID string + tenantUUID string +} + +func (s *CommonSuite) SetupTest() { + s.entityUUID = uuid.NewString() + s.userUUID = uuid.NewString() + s.tenantUUID = uuid.NewString() +} + +func (s *CommonSuite) buildEntityUUIDChain(h http.Handler) http.Handler { + return turtleware.EntityUUIDMiddleware(func(r *http.Request) (string, error) { + return s.entityUUID, nil + })(h) +} + +func (s *CommonSuite) buildAuthChain(h http.Handler) http.Handler { + s.T().Helper() + + privateKey, err := jwk.FromRaw([]byte("secret-passphrase")) + s.Require().NoError(err) + s.Require().NoError(privateKey.Set(jwk.KeyIDKey, "super-key")) + s.Require().NoError(privateKey.Set(jwk.AlgorithmKey, jwa.HS512)) + + keySet := jwk.NewSet() + s.Require().NoError(keySet.AddKey(privateKey)) + + return alice.New( + func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := s.generateToken( + jwa.HS512, + privateKey, + map[string]interface{}{ + "uuid": s.userUUID, + "tenant_uuid": s.tenantUUID, + }, + map[string]interface{}{jwk.KeyIDKey: privateKey.KeyID()}, + ) + + r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + handler.ServeHTTP(w, r) + }) + }, + turtleware.AuthBearerHeaderMiddleware, + turtleware.AuthClaimsMiddleware(keySet), + tenant.UUIDMiddleware, + ).Then(h) +} + +func (s *CommonSuite) generateToken( + algo jwa.SignatureAlgorithm, + key interface{}, + claims map[string]interface{}, + headers map[string]interface{}, +) string { + t := jwt.New() + + for k, v := range claims { + if err := t.Set(k, v); err != nil { + s.Require().NoError(err) + } + } + + hdr := jws.NewHeaders() + for k, v := range headers { + if err := hdr.Set(k, v); err != nil { + s.Require().NoError(err) + } + } + + signedT, err := jwt.Sign(t, jwt.WithKey(algo, key, jws.WithProtectedHeaders(hdr))) + if err != nil { + s.Require().NoError(err) + } + + return string(signedT) +} + +func (s *CommonSuite) loadTestDataString(name string) string { + bufBytes, err := io.ReadAll(s.loadTestData(name)) + if err != nil { + s.Require().NoError(err) + } + + return string(bufBytes) +} + +func (s *CommonSuite) loadTestData(name string) io.Reader { + filePath := path.Join("testdata", name) + + f, err := os.Open(filePath) + if err != nil { + s.Require().NoError(err) + } + + s.T().Cleanup(func() { + if err := f.Close(); err != nil && !strings.Contains(err.Error(), "file already closed") { + s.T().Logf("Failed to close file handle for test data %q: %s", name, err) + } + }) + + return f +} diff --git a/tenant/go.mod b/tenant/go.mod index d46cff9..29fb043 100644 --- a/tenant/go.mod +++ b/tenant/go.mod @@ -7,14 +7,17 @@ toolchain go1.22.0 replace github.com/kernle32dll/turtleware => ./.. require ( + github.com/google/uuid v1.6.0 github.com/jmoiron/sqlx v1.4.0 github.com/justinas/alice v1.2.0 github.com/kernle32dll/turtleware v0.0.0-20240528134300-d4bd43ff9b2c github.com/lestrrat-go/jwx/v2 v2.1.0 github.com/rs/zerolog v1.33.0 + github.com/stretchr/testify v1.9.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -31,6 +34,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 // indirect go.opentelemetry.io/otel v1.28.0 // indirect @@ -38,4 +42,5 @@ require ( go.opentelemetry.io/otel/trace v1.28.0 // indirect golang.org/x/crypto v0.25.0 // indirect golang.org/x/sys v0.22.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tenant/go.sum b/tenant/go.sum index bd9d668..ca11b85 100644 --- a/tenant/go.sum +++ b/tenant/go.sum @@ -31,6 +31,8 @@ github.com/kernle32dll/emissione-go v1.1.0 h1:ecsp58tVs8sJA0FJftIVHHPv/hagtjQJui github.com/kernle32dll/emissione-go v1.1.0/go.mod h1:h3zrmXUggdVPQW7hHv0WUGHoO4VwTieXvUpC/Go95kE= github.com/kernle32dll/keybox-go v1.2.0 h1:4bfv3uilJi8y971G2m62W2NV+n9OoYryT5Z9ULgzT6Q= github.com/kernle32dll/keybox-go v1.2.0/go.mod h1:+avlBw/jrVKyR/tHaWsA8YMT9zLsbnhPqmZH+a94sRY= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= @@ -58,6 +60,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -88,6 +92,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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= diff --git a/tenant/middleware_common_test.go b/tenant/middleware_common_test.go new file mode 100644 index 0000000..c1e20bf --- /dev/null +++ b/tenant/middleware_common_test.go @@ -0,0 +1,93 @@ +package tenant_test + +import ( + "context" + "github.com/kernle32dll/turtleware/tenant" + "github.com/stretchr/testify/suite" + "strings" + + "net/http" + "net/http/httptest" + "testing" +) + +type MiddlewareCommonSuite struct { + CommonSuite + + response *httptest.ResponseRecorder + request *http.Request +} + +func TestMiddlewareCommonSuite(t *testing.T) { + suite.Run(t, &MiddlewareCommonSuite{}) +} + +func (s *MiddlewareCommonSuite) SetupTest() { + s.CommonSuite.SetupTest() + + s.response = httptest.NewRecorder() + s.request = httptest.NewRequest(http.MethodGet, "https://example.com/foo", http.NoBody) +} + +func (s *MiddlewareCommonSuite) SetupSubTest() { + s.SetupTest() +} + +func (s *MiddlewareCommonSuite) Test_UUIDMiddleware_Success() { + // given + recordedUUID := "" + middlewareVerify := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + tenantUUID, err := tenant.UUIDFromRequestContext(r.Context()) + s.Require().NoError(err) + + recordedUUID = tenantUUID + }) + + // when + s.buildAuthChain(middlewareVerify).ServeHTTP(s.response, s.request) + + // then + s.Empty(s.response.Body.String()) + s.Equal(s.tenantUUID, recordedUUID) +} + +func (s *MiddlewareCommonSuite) Test_UUIDMiddleware_Error() { + // given + middlewareVerify := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + s.Fail("unexpected middleware invocation") + }) + + middleware := tenant.UUIDMiddleware + + // when + middleware(middlewareVerify).ServeHTTP(s.response, s.request) + + // then + s.Equal(http.StatusInternalServerError, s.response.Code) + s.JSONEq(s.loadTestDataString("errors/missing_auth_claims_error.json"), s.response.Body.String()) +} + +func (s *MiddlewareCommonSuite) Test_UUIDFromRequestContext_Error() { + // given + ctx := context.Background() + + // when + tenantUUID, err := tenant.UUIDFromRequestContext(ctx) + + // then + s.Empty(tenantUUID) + s.ErrorIs(err, tenant.ErrContextMissingTenantUUID) +} + +func (s *MiddlewareCommonSuite) Test_UUIDFromRequestContext_ErrTokenMissingTenantUUID() { + // given + s.tenantUUID = "" + + chain := s.buildAuthChain(nil) + + // when + chain.ServeHTTP(s.response, s.request) + + // then + s.True(strings.Contains(s.response.Body.String(), tenant.ErrTokenMissingTenantUUID.Error())) +} diff --git a/tenant/testdata/errors/missing_auth_claims_error.json b/tenant/testdata/errors/missing_auth_claims_error.json new file mode 100644 index 0000000..a923bbe --- /dev/null +++ b/tenant/testdata/errors/missing_auth_claims_error.json @@ -0,0 +1,7 @@ +{ + "status": 500, + "text": "Internal Server Error", + "errors": [ + "missing auth claims in context" + ] +} \ No newline at end of file