From fd5473f9aae8826c3ff02d06b0af209b183f98af Mon Sep 17 00:00:00 2001 From: Federico Maggi Date: Tue, 5 Nov 2024 15:03:00 +0100 Subject: [PATCH 1/3] test: add integration test with concurrent request load --- go.mod | 5 +- go.sum | 2 + integration_test.go | 186 +++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 180 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 3ae240b6..c22bf5a7 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/rond-authz/rond -go 1.21 +go 1.22 + +toolchain go1.23.2 require ( github.com/davidebianchi/gswagger v0.10.0 @@ -30,6 +32,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidebianchi/go-jsonclient v1.5.0 // indirect + github.com/fredmaggiowski/gowq v0.5.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-ini/ini v1.67.0 // indirect diff --git a/go.sum b/go.sum index 7d86f13e..8b8600af 100644 --- a/go.sum +++ b/go.sum @@ -85,6 +85,8 @@ github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7Dlme github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fredmaggiowski/gowq v0.5.0 h1:cUVA/u9pNr6qrGh8GQPHunZyRZNiTFStOKLwofABVCY= +github.com/fredmaggiowski/gowq v0.5.0/go.mod h1:yutQQ5WPK33bokWzB6jlkRoGRo5zw2zX+aML1TvNnTg= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= diff --git a/integration_test.go b/integration_test.go index 63a336ce..b26cdc12 100644 --- a/integration_test.go +++ b/integration_test.go @@ -1,14 +1,17 @@ package main import ( + "context" "encoding/json" "fmt" "net/http" + "net/http/httptest" "os" "strings" "testing" "github.com/caarlos0/env/v11" + "github.com/fredmaggiowski/gowq" "github.com/rond-authz/rond/core" "github.com/rond-authz/rond/internal/config" "github.com/rond-authz/rond/internal/testutils" @@ -80,8 +83,176 @@ func BenchmarkStartup(b *testing.B) { } } +func TestStartupAndLoadWithConcurrentRequests(t *testing.T) { + log, _ := test.NewNullLogger() + + tmpdir, err := os.MkdirTemp("", "rond-startup-test-") + require.NoError(t, err) + + policies := []string{`package policies`} + policies = append(policies, `allow_get { + verb := input.request.method + verb == "GET" +}`) + policies = append(policies, `allow_post { + verb := input.request.method + verb == "POST" +}`) + policies = append(policies, generateFilterPolicy("something")) // filter_something + policies = append(policies, generateProjectionPolicy("data")) // proj_data + + oas := &openapi.OpenAPISpec{ + Paths: map[string]openapi.PathVerbs{ + "/allow-get": { + http.MethodGet: { + PermissionV2: &core.RondConfig{ + RequestFlow: core.RequestFlow{PolicyName: "allow_get"}, + }, + }, + http.MethodPost: { + PermissionV2: &core.RondConfig{ + RequestFlow: core.RequestFlow{PolicyName: "allow_get"}, + }, + }, + }, + "/filter-something": { + http.MethodGet: { + PermissionV2: &core.RondConfig{ + RequestFlow: core.RequestFlow{PolicyName: "filter_something", GenerateQuery: true}, + }, + }, + }, + "/project-data": { + http.MethodPost: { + PermissionV2: &core.RondConfig{ + RequestFlow: core.RequestFlow{PolicyName: "allow_post"}, + ResponseFlow: core.ResponseFlow{PolicyName: "proj_data"}, + }, + }, + }, + }, + } + oasFileName := writeOAS(t, tmpdir, oas) + policiesFileName := writePolicies(t, tmpdir, policies) + + defer gock.Off() + defer gock.DisableNetworkingFilters() + defer gock.DisableNetworking() + + gock.EnableNetworking() + gock.NetworkingFilter(func(r *http.Request) bool { + if r.URL.Host == "localhost:3050" { + return false + } + if r.URL.Path == "/documentation/json" && r.URL.Host == "localhost:3050" { + return false + } + return true + }) + + gock.New("http://localhost:3050"). + Persist(). + Get("/documentation/json"). + Reply(200). + File(oasFileName) + + gock.New("http://localhost:3050/"). + Persist(). + Get("/allow-get"). + Reply(200) + gock.New("http://localhost:3050/"). + Persist(). + Get("/filter-something"). + Reply(200) + gock.New("http://localhost:3050/"). + Persist(). + Post("/project-data"). + Reply(200). + JSON([]string{}) + + mongoHost := os.Getenv("MONGO_HOST_CI") + if mongoHost == "" { + mongoHost = testutils.LocalhostMongoDB + t.Logf("Connection to localhost MongoDB, on CI env this is a problem!") + } + randomizedDBNamePart := testutils.GetRandomName(10) + mongoDBName := fmt.Sprintf("test-%s", randomizedDBNamePart) + envs, err := env.ParseAsWithOptions[config.EnvironmentVariables](env.Options{ + Environment: map[string]string{ + "TARGET_SERVICE_HOST": "localhost:3050", + "TARGET_SERVICE_OAS_PATH": "/documentation/json", + "OPA_MODULES_DIRECTORY": policiesFileName, + "LOG_LEVEL": "fatal", + "MONGODB_URL": fmt.Sprintf("mongodb://%s/%s", mongoHost, mongoDBName), + "BINDINGS_COLLECTION_NAME": "bindings", + "ROLES_COLLECTION_NAME": "roles", + }, + }) + require.NoError(t, err) + + app, err := setupService(envs, log) + require.NoError(t, err) + require.True(t, <-app.sdkBootState.IsReadyChan()) + defer app.close() + + // everything is up and running, now start bombarding the webserver + type RequestConf struct { + Verb string + Path string + ExpectedStatus int + } + dictinoary := []RequestConf{ + {Verb: http.MethodGet, Path: "/allow-get", ExpectedStatus: http.StatusOK}, + {Verb: http.MethodPost, Path: "/allow-get", ExpectedStatus: http.StatusForbidden}, + {Verb: http.MethodGet, Path: "/filter-something", ExpectedStatus: http.StatusOK}, + {Verb: http.MethodPost, Path: "/filter-something", ExpectedStatus: http.StatusNotFound}, + {Verb: http.MethodPost, Path: "/project-data", ExpectedStatus: http.StatusOK}, + {Verb: http.MethodGet, Path: "/project-data", ExpectedStatus: http.StatusNotFound}, + } + + queue := gowq.New[RequestConf](100) + + fireQuest := func(requestConf RequestConf) { + w := httptest.NewRecorder() + req := httptest.NewRequest(requestConf.Verb, requestConf.Path, nil) + app.router.ServeHTTP(w, req) + require.Equal(t, requestConf.ExpectedStatus, w.Result().StatusCode) + } + + i := 0 + for i < 100_000 { + d := dictinoary[i%len(dictinoary)] + i++ + queue.Push(func(ctx context.Context) (RequestConf, error) { + fireQuest(d) + return d, nil + }) + } + + _, errors := queue.RunAll(context.TODO()) + require.Len(t, errors, 0) +} + +func writeOAS(t require.TestingT, tmpdir string, oas *openapi.OpenAPISpec) string { + oasContent, err := json.Marshal(oas) + require.NoError(t, err) + + oasFileName := fmt.Sprintf("%s/oas.json", tmpdir) + err = os.WriteFile(oasFileName, oasContent, 0644) + require.NoError(t, err) + return oasFileName +} + +func writePolicies(t require.TestingT, tmpdir string, policies []string) string { + policyFileName := fmt.Sprintf("%s/policies.rego", tmpdir) + policiesContent := []byte(strings.Join(policies, "\n")) + err := os.WriteFile(policyFileName, policiesContent, 0644) + require.NoError(t, err) + return policyFileName +} + func generateAndSaveConfig(t require.TestingT, tmpdir string, numberOfPaths int) (string, string) { - oas := openapi.OpenAPISpec{ + oas := &openapi.OpenAPISpec{ Paths: make(map[string]openapi.PathVerbs), } policies := []string{"package policies"} @@ -135,17 +306,8 @@ func generateAndSaveConfig(t require.TestingT, tmpdir string, numberOfPaths int) } } - oasContent, err := json.Marshal(oas) - require.NoError(t, err) - - oasFileName := fmt.Sprintf("%s/oas.json", tmpdir) - err = os.WriteFile(oasFileName, oasContent, 0644) - require.NoError(t, err) - - policyFileName := fmt.Sprintf("%s/policies.rego", tmpdir) - policiesContent := []byte(strings.Join(policies, "\n")) - err = os.WriteFile(policyFileName, policiesContent, 0644) - require.NoError(t, err) + oasFileName := writeOAS(t, tmpdir, oas) + policyFileName := writePolicies(t, tmpdir, policies) return oasFileName, policyFileName } From eece7af92a5ebff2cbafe43600f88e12260cc43f Mon Sep 17 00:00:00 2001 From: Federico Maggi Date: Tue, 5 Nov 2024 15:57:44 +0100 Subject: [PATCH 2/3] refactor: typo and imports --- integration_test.go | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/integration_test.go b/integration_test.go index b26cdc12..b83c1eb0 100644 --- a/integration_test.go +++ b/integration_test.go @@ -10,12 +10,13 @@ import ( "strings" "testing" - "github.com/caarlos0/env/v11" - "github.com/fredmaggiowski/gowq" "github.com/rond-authz/rond/core" "github.com/rond-authz/rond/internal/config" "github.com/rond-authz/rond/internal/testutils" "github.com/rond-authz/rond/openapi" + + "github.com/caarlos0/env/v11" + "github.com/fredmaggiowski/gowq" "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/require" "gopkg.in/h2non/gock.v1" @@ -201,7 +202,7 @@ func TestStartupAndLoadWithConcurrentRequests(t *testing.T) { Path string ExpectedStatus int } - dictinoary := []RequestConf{ + dictionary := []RequestConf{ {Verb: http.MethodGet, Path: "/allow-get", ExpectedStatus: http.StatusOK}, {Verb: http.MethodPost, Path: "/allow-get", ExpectedStatus: http.StatusForbidden}, {Verb: http.MethodGet, Path: "/filter-something", ExpectedStatus: http.StatusOK}, @@ -212,19 +213,17 @@ func TestStartupAndLoadWithConcurrentRequests(t *testing.T) { queue := gowq.New[RequestConf](100) - fireQuest := func(requestConf RequestConf) { - w := httptest.NewRecorder() - req := httptest.NewRequest(requestConf.Verb, requestConf.Path, nil) - app.router.ServeHTTP(w, req) - require.Equal(t, requestConf.ExpectedStatus, w.Result().StatusCode) - } - i := 0 for i < 100_000 { - d := dictinoary[i%len(dictinoary)] + d := dictionary[i%len(dictionary)] i++ queue.Push(func(ctx context.Context) (RequestConf, error) { - fireQuest(d) + w := httptest.NewRecorder() + + req := httptest.NewRequest(d.Verb, d.Path, nil) + app.router.ServeHTTP(w, req) + require.Equal(t, d.ExpectedStatus, w.Result().StatusCode) + return d, nil }) } From 0ce870c68e7a7d21a352bcf212e1db50c068953a Mon Sep 17 00:00:00 2001 From: Federico Maggi Date: Tue, 5 Nov 2024 21:14:47 +0100 Subject: [PATCH 3/3] fix: better test checks --- integration_test.go | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/integration_test.go b/integration_test.go index b83c1eb0..29c684e9 100644 --- a/integration_test.go +++ b/integration_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "net/http/httptest" "os" @@ -164,12 +165,18 @@ func TestStartupAndLoadWithConcurrentRequests(t *testing.T) { gock.New("http://localhost:3050/"). Persist(). Get("/filter-something"). + MatchHeader("acl_rows", `{"$or":[{"$and":[{"key":{"$eq":42}}]}]}`). Reply(200) gock.New("http://localhost:3050/"). Persist(). Post("/project-data"). Reply(200). - JSON([]string{}) + JSON([]struct { + Key string `json:"key"` + }{ + {"k1"}, + {"k2"}, + }) mongoHost := os.Getenv("MONGO_HOST_CI") if mongoHost == "" { @@ -201,13 +208,14 @@ func TestStartupAndLoadWithConcurrentRequests(t *testing.T) { Verb string Path string ExpectedStatus int + ExpectedBody string } dictionary := []RequestConf{ {Verb: http.MethodGet, Path: "/allow-get", ExpectedStatus: http.StatusOK}, {Verb: http.MethodPost, Path: "/allow-get", ExpectedStatus: http.StatusForbidden}, {Verb: http.MethodGet, Path: "/filter-something", ExpectedStatus: http.StatusOK}, {Verb: http.MethodPost, Path: "/filter-something", ExpectedStatus: http.StatusNotFound}, - {Verb: http.MethodPost, Path: "/project-data", ExpectedStatus: http.StatusOK}, + {Verb: http.MethodPost, Path: "/project-data", ExpectedBody: `["k1","k2"]`, ExpectedStatus: http.StatusOK}, {Verb: http.MethodGet, Path: "/project-data", ExpectedStatus: http.StatusNotFound}, } @@ -224,6 +232,12 @@ func TestStartupAndLoadWithConcurrentRequests(t *testing.T) { app.router.ServeHTTP(w, req) require.Equal(t, d.ExpectedStatus, w.Result().StatusCode) + if d.ExpectedBody != "" { + data, err := io.ReadAll(w.Body) + require.NoError(t, err) + require.Equal(t, d.ExpectedBody, string(data)) + } + return d, nil }) } @@ -320,11 +334,11 @@ func generateFilterPolicy(name string) string { func generateProjectionPolicy(name string) string { return fmt.Sprintf(`proj_%s [projects] { - projects := [projects_with_envs_filtered | + projects := [kept | project := input.response.body[_] - projects_with_envs_filtered := project + kept := project.key ] - }`, name) +}`, name) } func generateAllowPolicy(name string) string {