From 6ceadf40fcb6c482f580fc055efd72a143c426a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Jerman?= Date: Mon, 27 Sep 2021 15:50:18 +0200 Subject: [PATCH] Setup the API GW testing env --- pkg/apigw/service.go | 40 ++-- tests/apigw/main_test.go | 213 ++++++++++++++++++ tests/apigw/prefilter_header_failing_test.go | 20 ++ tests/apigw/prefilter_header_n_routes_test.go | 46 ++++ tests/apigw/prefilter_header_passing_test.go | 20 ++ tests/apigw/prefilter_query_failing_test.go | 20 ++ tests/apigw/prefilter_query_n_routes_test.go | 46 ++++ tests/apigw/prefilter_query_passing_test.go | 20 ++ .../prefilter_header_failing/def.yaml | 9 + .../prefilter_header_n_routes/def.yaml | 18 ++ .../prefilter_header_passing/def.yaml | 9 + .../testdata/prefilter_query_failing/def.yaml | 9 + .../prefilter_query_n_routes/def.yaml | 18 ++ .../testdata/prefilter_query_passing/def.yaml | 9 + 14 files changed, 481 insertions(+), 16 deletions(-) create mode 100644 tests/apigw/main_test.go create mode 100644 tests/apigw/prefilter_header_failing_test.go create mode 100644 tests/apigw/prefilter_header_n_routes_test.go create mode 100644 tests/apigw/prefilter_header_passing_test.go create mode 100644 tests/apigw/prefilter_query_failing_test.go create mode 100644 tests/apigw/prefilter_query_n_routes_test.go create mode 100644 tests/apigw/prefilter_query_passing_test.go create mode 100644 tests/apigw/testdata/prefilter_header_failing/def.yaml create mode 100644 tests/apigw/testdata/prefilter_header_n_routes/def.yaml create mode 100644 tests/apigw/testdata/prefilter_header_passing/def.yaml create mode 100644 tests/apigw/testdata/prefilter_query_failing/def.yaml create mode 100644 tests/apigw/testdata/prefilter_query_n_routes/def.yaml create mode 100644 tests/apigw/testdata/prefilter_query_passing/def.yaml diff --git a/pkg/apigw/service.go b/pkg/apigw/service.go index 3b8bde898..a0ce4bb0f 100644 --- a/pkg/apigw/service.go +++ b/pkg/apigw/service.go @@ -29,6 +29,7 @@ type ( log *zap.Logger reg *registry.Registry routes []*route + router chi.Router storer storer reload chan bool } @@ -75,6 +76,24 @@ func (s *apigw) Reload(ctx context.Context) { }() } +// ReloadHandler is a wrapper for route reloading logic, primarily needed for testing +func (s *apigw) ReloadHandler(ctx context.Context) { + routes, err := s.loadRoutes(ctx) + + if err != nil { + s.log.Error("could not reload API Gateway routes", zap.Error(err)) + return + } + + s.log.Debug("reloading API Gateway routes and functions", zap.Int("count", len(routes))) + + s.Init(ctx, routes...) + + for _, route := range s.routes { + s.router.Handle(route.endpoint, route) + } +} + func (s *apigw) loadRoutes(ctx context.Context) (rr []*route, err error) { routes, _, err := s.storer.SearchApigwRoutes(ctx, st.ApigwRouteFilter{ Enabled: true, @@ -116,7 +135,9 @@ func (s *apigw) Router(r chi.Router) { ctx = context.Background() ) - r.HandleFunc("/", helperDefaultResponse(s.opts)) + s.router = r + + s.router.HandleFunc("/", helperDefaultResponse(s.opts)) routes, err := s.loadRoutes(ctx) @@ -128,27 +149,14 @@ func (s *apigw) Router(r chi.Router) { s.Init(ctx, routes...) for _, route := range s.routes { - r.Handle(route.endpoint, route) + s.router.Handle(route.endpoint, route) } go func() { for { select { case <-s.reload: - routes, err := s.loadRoutes(ctx) - - if err != nil { - s.log.Error("could not reload API Gateway routes", zap.Error(err)) - return - } - - s.log.Debug("reloading API Gateway routes and functions", zap.Int("count", len(routes))) - - s.Init(ctx, routes...) - - for _, route := range s.routes { - r.Handle(route.endpoint, route) - } + s.ReloadHandler(ctx) case <-ctx.Done(): s.log.Debug("shutting down API Gateway service") diff --git a/tests/apigw/main_test.go b/tests/apigw/main_test.go new file mode 100644 index 000000000..27956f91c --- /dev/null +++ b/tests/apigw/main_test.go @@ -0,0 +1,213 @@ +package apigw + +import ( + "context" + "errors" + "os" + "path" + "testing" + + "github.com/cortezaproject/corteza-server/app" + "github.com/cortezaproject/corteza-server/pkg/api/server" + "github.com/cortezaproject/corteza-server/pkg/apigw" + "github.com/cortezaproject/corteza-server/pkg/auth" + "github.com/cortezaproject/corteza-server/pkg/cli" + "github.com/cortezaproject/corteza-server/pkg/envoy" + "github.com/cortezaproject/corteza-server/pkg/envoy/csv" + "github.com/cortezaproject/corteza-server/pkg/envoy/directory" + "github.com/cortezaproject/corteza-server/pkg/envoy/resource" + es "github.com/cortezaproject/corteza-server/pkg/envoy/store" + "github.com/cortezaproject/corteza-server/pkg/envoy/yaml" + "github.com/cortezaproject/corteza-server/pkg/eventbus" + "github.com/cortezaproject/corteza-server/pkg/id" + "github.com/cortezaproject/corteza-server/pkg/label" + ltype "github.com/cortezaproject/corteza-server/pkg/label/types" + "github.com/cortezaproject/corteza-server/pkg/logger" + "github.com/cortezaproject/corteza-server/pkg/options" + "github.com/cortezaproject/corteza-server/store" + "github.com/cortezaproject/corteza-server/store/sqlite3" + "github.com/cortezaproject/corteza-server/system/service" + sysTypes "github.com/cortezaproject/corteza-server/system/types" + "github.com/cortezaproject/corteza-server/tests/helpers" + "github.com/go-chi/chi" + _ "github.com/joho/godotenv/autoload" + "github.com/steinfletcher/apitest" + "github.com/stretchr/testify/require" +) + +type ( + helper struct { + t *testing.T + a *require.Assertions + + cUser *sysTypes.User + roleID uint64 + token string + } +) + +var ( + testApp *app.CortezaApp + r chi.Router + + eventBus = eventbus.New() +) + +func init() { + helpers.RecursiveDotEnvLoad() +} + +func InitTestApp() { + if testApp == nil { + ctx := cli.Context() + + testApp = helpers.NewIntegrationTestApp(ctx, func(app *app.CortezaApp) (err error) { + service.DefaultStore, err = sqlite3.ConnectInMemory(ctx) + if err != nil { + return err + } + + eventbus.Set(eventBus) + return nil + }) + } + + if r == nil { + r = chi.NewRouter() + r.Use(server.BaseMiddleware(false, logger.Default())...) + helpers.BindAuthMiddleware(r) + + // setup API GW routes + apigw.Setup(options.Apigw(), service.DefaultLogger, service.DefaultStore) + r.Route("/", apigw.Service().Router) + } +} + +func TestMain(m *testing.M) { + InitTestApp() + os.Exit(m.Run()) +} + +func newHelper(t *testing.T) helper { + h := helper{ + t: t, + a: require.New(t), + roleID: id.Next(), + cUser: &sysTypes.User{ + ID: id.Next(), + }, + } + + var err error + h.token, err = auth.DefaultJwtHandler.Generate(context.Background(), h.cUser) + if err != nil { + panic(err) + } + + h.cUser.SetRoles(h.roleID) + helpers.UpdateRBAC(h.roleID) + + return h +} + +func (h helper) MyRole() uint64 { + return h.roleID +} + +// Returns context w/ security details +func (h helper) secCtx() context.Context { + return auth.SetIdentityToContext(context.Background(), h.cUser) +} + +// apitest basics, initialize, set handler, add auth +func (h helper) apiInit() *apitest.APITest { + InitTestApp() + + return apitest. + New(). + Handler(r). + Intercept(helpers.ReqHeaderRawAuthBearer(h.token)) + +} + +func setupScenario(t *testing.T) (context.Context, helper, store.Storer) { + ctx, h, s := setup(t) + loadScenario(ctx, s, t, h) + reloadRoutes(ctx) + + return ctx, h, s +} + +func setup(t *testing.T) (context.Context, helper, store.Storer) { + h := newHelper(t) + s := service.DefaultStore + + u := &sysTypes.User{ + ID: id.Next(), + } + u.SetRoles(auth.BypassRoles().IDs()...) + + ctx := auth.SetIdentityToContext(context.Background(), u) + + return ctx, h, s +} + +func reloadRoutes(ctx context.Context) { + apigw.Service().ReloadHandler(ctx) +} + +// Unwraps error before it passes it to the tester +func (h helper) noError(err error) { + for errors.Unwrap(err) != nil { + err = errors.Unwrap(err) + } + + h.a.NoError(err) +} + +func (h helper) setLabel(res label.LabeledResource, name, value string) { + h.a.NoError(store.UpsertLabel(h.secCtx(), service.DefaultStore, <ype.Label{ + Kind: res.LabelResourceKind(), + ResourceID: res.LabelResourceID(), + Name: name, + Value: value, + })) +} + +func loadScenario(ctx context.Context, s store.Storer, t *testing.T, h helper) { + loadScenarioWithName(ctx, s, t, h, t.Name()[5:]) +} + +func loadScenarioWithName(ctx context.Context, s store.Storer, t *testing.T, h helper, scenario string) { + cleanup(ctx, h, s) + parseEnvoy(ctx, s, h, path.Join("testdata", scenario)) +} + +func cleanup(ctx context.Context, h helper, s store.Storer) { + h.noError(s.TruncateApigwFilters(ctx)) + h.noError(s.TruncateApigwRoutes(ctx)) +} + +func parseEnvoy(ctx context.Context, s store.Storer, h helper, path string) { + nn, err := directory.Decode( + ctx, + path, + yaml.Decoder(), + csv.Decoder(), + ) + if err != nil { + h.t.Fatalf("failed to decode scenario data: %v", err) + } + + crs := resource.ComposeRecordShaper() + nn, err = resource.Shape(nn, crs) + h.a.NoError(err) + + // import into the store + se := es.NewStoreEncoder(s, nil) + bld := envoy.NewBuilder(se) + g, err := bld.Build(ctx, nn...) + h.a.NoError(err) + err = envoy.Encode(ctx, g, se) + h.a.NoError(err) +} diff --git a/tests/apigw/prefilter_header_failing_test.go b/tests/apigw/prefilter_header_failing_test.go new file mode 100644 index 000000000..fe9a578a7 --- /dev/null +++ b/tests/apigw/prefilter_header_failing_test.go @@ -0,0 +1,20 @@ +package apigw + +import ( + "net/http" + "testing" +) + +func Test_prefilter_header_failing(t *testing.T) { + var ( + _, h, _ = setupScenario(t) + ) + + h.apiInit(). + Get("/test"). + Header("Accept", "application/json"). + Header("Token", "brute-force-guess"). + Expect(t). + Status(http.StatusBadRequest). + End() +} diff --git a/tests/apigw/prefilter_header_n_routes_test.go b/tests/apigw/prefilter_header_n_routes_test.go new file mode 100644 index 000000000..daff75bb4 --- /dev/null +++ b/tests/apigw/prefilter_header_n_routes_test.go @@ -0,0 +1,46 @@ +package apigw + +import ( + "net/http" + "testing" +) + +func Test_prefilter_header_n_routes(t *testing.T) { + var ( + _, h, _ = setupScenario(t) + ) + + // First (a) route query validation + h.apiInit(). + Get("/a"). + Header("P", "a"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + End() + + h.apiInit(). + Get("/a"). + Header("P", "b"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusBadRequest). + End() + + // Second (b) route query validation + h.apiInit(). + Get("/b"). + Header("P", "b"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + End() + + h.apiInit(). + Get("/b"). + Header("P", "a"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusBadRequest). + End() +} diff --git a/tests/apigw/prefilter_header_passing_test.go b/tests/apigw/prefilter_header_passing_test.go new file mode 100644 index 000000000..1111e3fcf --- /dev/null +++ b/tests/apigw/prefilter_header_passing_test.go @@ -0,0 +1,20 @@ +package apigw + +import ( + "net/http" + "testing" +) + +func Test_prefilter_header_passing(t *testing.T) { + var ( + _, h, _ = setupScenario(t) + ) + + h.apiInit(). + Get("/test"). + Header("Accept", "application/json"). + Header("Token", "super-secret-token"). + Expect(t). + Status(http.StatusOK). + End() +} diff --git a/tests/apigw/prefilter_query_failing_test.go b/tests/apigw/prefilter_query_failing_test.go new file mode 100644 index 000000000..1b1a60922 --- /dev/null +++ b/tests/apigw/prefilter_query_failing_test.go @@ -0,0 +1,20 @@ +package apigw + +import ( + "net/http" + "testing" +) + +func Test_prefilter_query_failing(t *testing.T) { + var ( + _, h, _ = setupScenario(t) + ) + + h.apiInit(). + Get("/test"). + Query("token", "brute-force-guess"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusBadRequest). + End() +} diff --git a/tests/apigw/prefilter_query_n_routes_test.go b/tests/apigw/prefilter_query_n_routes_test.go new file mode 100644 index 000000000..68c21d6f1 --- /dev/null +++ b/tests/apigw/prefilter_query_n_routes_test.go @@ -0,0 +1,46 @@ +package apigw + +import ( + "net/http" + "testing" +) + +func Test_prefilter_query_n_routes(t *testing.T) { + var ( + _, h, _ = setupScenario(t) + ) + + // First (a) route query validation + h.apiInit(). + Get("/a"). + Query("p", "a"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + End() + + h.apiInit(). + Get("/a"). + Query("p", "b"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusBadRequest). + End() + + // Second (b) route query validation + h.apiInit(). + Get("/b"). + Query("p", "b"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + End() + + h.apiInit(). + Get("/b"). + Query("p", "a"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusBadRequest). + End() +} diff --git a/tests/apigw/prefilter_query_passing_test.go b/tests/apigw/prefilter_query_passing_test.go new file mode 100644 index 000000000..532d0ac46 --- /dev/null +++ b/tests/apigw/prefilter_query_passing_test.go @@ -0,0 +1,20 @@ +package apigw + +import ( + "net/http" + "testing" +) + +func Test_prefilter_query_passing(t *testing.T) { + var ( + _, h, _ = setupScenario(t) + ) + + h.apiInit(). + Get("/test"). + Query("token", "super-secret-token"). + Header("Accept", "application/json"). + Expect(t). + Status(http.StatusOK). + End() +} diff --git a/tests/apigw/testdata/prefilter_header_failing/def.yaml b/tests/apigw/testdata/prefilter_header_failing/def.yaml new file mode 100644 index 000000000..e8b48a415 --- /dev/null +++ b/tests/apigw/testdata/prefilter_header_failing/def.yaml @@ -0,0 +1,9 @@ +apigateway: + - endpoint: /test + method: GET + enabled: true + filters: + - ref: "header" + kind: "prefilter" + params: + expr: "Token == \"super-secret-token\"" diff --git a/tests/apigw/testdata/prefilter_header_n_routes/def.yaml b/tests/apigw/testdata/prefilter_header_n_routes/def.yaml new file mode 100644 index 000000000..60d1c4856 --- /dev/null +++ b/tests/apigw/testdata/prefilter_header_n_routes/def.yaml @@ -0,0 +1,18 @@ +apigateway: + - endpoint: /a + method: GET + enabled: true + filters: + - ref: "header" + kind: "prefilter" + params: + expr: "P == \"a\"" + + - endpoint: /b + method: GET + enabled: true + filters: + - ref: "header" + kind: "prefilter" + params: + expr: "P == \"b\"" diff --git a/tests/apigw/testdata/prefilter_header_passing/def.yaml b/tests/apigw/testdata/prefilter_header_passing/def.yaml new file mode 100644 index 000000000..e8b48a415 --- /dev/null +++ b/tests/apigw/testdata/prefilter_header_passing/def.yaml @@ -0,0 +1,9 @@ +apigateway: + - endpoint: /test + method: GET + enabled: true + filters: + - ref: "header" + kind: "prefilter" + params: + expr: "Token == \"super-secret-token\"" diff --git a/tests/apigw/testdata/prefilter_query_failing/def.yaml b/tests/apigw/testdata/prefilter_query_failing/def.yaml new file mode 100644 index 000000000..f8e965013 --- /dev/null +++ b/tests/apigw/testdata/prefilter_query_failing/def.yaml @@ -0,0 +1,9 @@ +apigateway: + - endpoint: /test + method: GET + enabled: true + filters: + - ref: "queryParam" + kind: "prefilter" + params: + expr: "token == \"super-secret-token\"" diff --git a/tests/apigw/testdata/prefilter_query_n_routes/def.yaml b/tests/apigw/testdata/prefilter_query_n_routes/def.yaml new file mode 100644 index 000000000..2aac8c921 --- /dev/null +++ b/tests/apigw/testdata/prefilter_query_n_routes/def.yaml @@ -0,0 +1,18 @@ +apigateway: + - endpoint: /a + method: GET + enabled: true + filters: + - ref: "queryParam" + kind: "prefilter" + params: + expr: "p == \"a\"" + + - endpoint: /b + method: GET + enabled: true + filters: + - ref: "queryParam" + kind: "prefilter" + params: + expr: "p == \"b\"" diff --git a/tests/apigw/testdata/prefilter_query_passing/def.yaml b/tests/apigw/testdata/prefilter_query_passing/def.yaml new file mode 100644 index 000000000..f8e965013 --- /dev/null +++ b/tests/apigw/testdata/prefilter_query_passing/def.yaml @@ -0,0 +1,9 @@ +apigateway: + - endpoint: /test + method: GET + enabled: true + filters: + - ref: "queryParam" + kind: "prefilter" + params: + expr: "token == \"super-secret-token\""