Skip to content

Commit ad9bfa2

Browse files
test: add integration test with concurrent request load (#402)
* test: add integration test with concurrent request load * refactor: typo and imports * fix: better test checks
1 parent 6d1eeeb commit ad9bfa2

File tree

3 files changed

+197
-17
lines changed

3 files changed

+197
-17
lines changed

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module github.com/rond-authz/rond
22

3-
go 1.21
3+
go 1.22
4+
5+
toolchain go1.23.2
46

57
require (
68
github.com/davidebianchi/gswagger v0.10.0
@@ -30,6 +32,7 @@ require (
3032
github.com/cespare/xxhash/v2 v2.3.0 // indirect
3133
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
3234
github.com/davidebianchi/go-jsonclient v1.5.0 // indirect
35+
github.com/fredmaggiowski/gowq v0.5.0 // indirect
3336
github.com/fsnotify/fsnotify v1.7.0 // indirect
3437
github.com/ghodss/yaml v1.0.0 // indirect
3538
github.com/go-ini/ini v1.67.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7Dlme
8585
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
8686
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
8787
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
88+
github.com/fredmaggiowski/gowq v0.5.0 h1:cUVA/u9pNr6qrGh8GQPHunZyRZNiTFStOKLwofABVCY=
89+
github.com/fredmaggiowski/gowq v0.5.0/go.mod h1:yutQQ5WPK33bokWzB6jlkRoGRo5zw2zX+aML1TvNnTg=
8890
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
8991
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
9092
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=

integration_test.go

Lines changed: 191 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
package main
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
7+
"io"
68
"net/http"
9+
"net/http/httptest"
710
"os"
811
"strings"
912
"testing"
1013

11-
"github.com/caarlos0/env/v11"
1214
"github.com/rond-authz/rond/core"
1315
"github.com/rond-authz/rond/internal/config"
1416
"github.com/rond-authz/rond/internal/testutils"
1517
"github.com/rond-authz/rond/openapi"
18+
19+
"github.com/caarlos0/env/v11"
20+
"github.com/fredmaggiowski/gowq"
1621
"github.com/sirupsen/logrus/hooks/test"
1722
"github.com/stretchr/testify/require"
1823
"gopkg.in/h2non/gock.v1"
@@ -80,8 +85,187 @@ func BenchmarkStartup(b *testing.B) {
8085
}
8186
}
8287

88+
func TestStartupAndLoadWithConcurrentRequests(t *testing.T) {
89+
log, _ := test.NewNullLogger()
90+
91+
tmpdir, err := os.MkdirTemp("", "rond-startup-test-")
92+
require.NoError(t, err)
93+
94+
policies := []string{`package policies`}
95+
policies = append(policies, `allow_get {
96+
verb := input.request.method
97+
verb == "GET"
98+
}`)
99+
policies = append(policies, `allow_post {
100+
verb := input.request.method
101+
verb == "POST"
102+
}`)
103+
policies = append(policies, generateFilterPolicy("something")) // filter_something
104+
policies = append(policies, generateProjectionPolicy("data")) // proj_data
105+
106+
oas := &openapi.OpenAPISpec{
107+
Paths: map[string]openapi.PathVerbs{
108+
"/allow-get": {
109+
http.MethodGet: {
110+
PermissionV2: &core.RondConfig{
111+
RequestFlow: core.RequestFlow{PolicyName: "allow_get"},
112+
},
113+
},
114+
http.MethodPost: {
115+
PermissionV2: &core.RondConfig{
116+
RequestFlow: core.RequestFlow{PolicyName: "allow_get"},
117+
},
118+
},
119+
},
120+
"/filter-something": {
121+
http.MethodGet: {
122+
PermissionV2: &core.RondConfig{
123+
RequestFlow: core.RequestFlow{PolicyName: "filter_something", GenerateQuery: true},
124+
},
125+
},
126+
},
127+
"/project-data": {
128+
http.MethodPost: {
129+
PermissionV2: &core.RondConfig{
130+
RequestFlow: core.RequestFlow{PolicyName: "allow_post"},
131+
ResponseFlow: core.ResponseFlow{PolicyName: "proj_data"},
132+
},
133+
},
134+
},
135+
},
136+
}
137+
oasFileName := writeOAS(t, tmpdir, oas)
138+
policiesFileName := writePolicies(t, tmpdir, policies)
139+
140+
defer gock.Off()
141+
defer gock.DisableNetworkingFilters()
142+
defer gock.DisableNetworking()
143+
144+
gock.EnableNetworking()
145+
gock.NetworkingFilter(func(r *http.Request) bool {
146+
if r.URL.Host == "localhost:3050" {
147+
return false
148+
}
149+
if r.URL.Path == "/documentation/json" && r.URL.Host == "localhost:3050" {
150+
return false
151+
}
152+
return true
153+
})
154+
155+
gock.New("http://localhost:3050").
156+
Persist().
157+
Get("/documentation/json").
158+
Reply(200).
159+
File(oasFileName)
160+
161+
gock.New("http://localhost:3050/").
162+
Persist().
163+
Get("/allow-get").
164+
Reply(200)
165+
gock.New("http://localhost:3050/").
166+
Persist().
167+
Get("/filter-something").
168+
MatchHeader("acl_rows", `{"$or":[{"$and":[{"key":{"$eq":42}}]}]}`).
169+
Reply(200)
170+
gock.New("http://localhost:3050/").
171+
Persist().
172+
Post("/project-data").
173+
Reply(200).
174+
JSON([]struct {
175+
Key string `json:"key"`
176+
}{
177+
{"k1"},
178+
{"k2"},
179+
})
180+
181+
mongoHost := os.Getenv("MONGO_HOST_CI")
182+
if mongoHost == "" {
183+
mongoHost = testutils.LocalhostMongoDB
184+
t.Logf("Connection to localhost MongoDB, on CI env this is a problem!")
185+
}
186+
randomizedDBNamePart := testutils.GetRandomName(10)
187+
mongoDBName := fmt.Sprintf("test-%s", randomizedDBNamePart)
188+
envs, err := env.ParseAsWithOptions[config.EnvironmentVariables](env.Options{
189+
Environment: map[string]string{
190+
"TARGET_SERVICE_HOST": "localhost:3050",
191+
"TARGET_SERVICE_OAS_PATH": "/documentation/json",
192+
"OPA_MODULES_DIRECTORY": policiesFileName,
193+
"LOG_LEVEL": "fatal",
194+
"MONGODB_URL": fmt.Sprintf("mongodb://%s/%s", mongoHost, mongoDBName),
195+
"BINDINGS_COLLECTION_NAME": "bindings",
196+
"ROLES_COLLECTION_NAME": "roles",
197+
},
198+
})
199+
require.NoError(t, err)
200+
201+
app, err := setupService(envs, log)
202+
require.NoError(t, err)
203+
require.True(t, <-app.sdkBootState.IsReadyChan())
204+
defer app.close()
205+
206+
// everything is up and running, now start bombarding the webserver
207+
type RequestConf struct {
208+
Verb string
209+
Path string
210+
ExpectedStatus int
211+
ExpectedBody string
212+
}
213+
dictionary := []RequestConf{
214+
{Verb: http.MethodGet, Path: "/allow-get", ExpectedStatus: http.StatusOK},
215+
{Verb: http.MethodPost, Path: "/allow-get", ExpectedStatus: http.StatusForbidden},
216+
{Verb: http.MethodGet, Path: "/filter-something", ExpectedStatus: http.StatusOK},
217+
{Verb: http.MethodPost, Path: "/filter-something", ExpectedStatus: http.StatusNotFound},
218+
{Verb: http.MethodPost, Path: "/project-data", ExpectedBody: `["k1","k2"]`, ExpectedStatus: http.StatusOK},
219+
{Verb: http.MethodGet, Path: "/project-data", ExpectedStatus: http.StatusNotFound},
220+
}
221+
222+
queue := gowq.New[RequestConf](100)
223+
224+
i := 0
225+
for i < 100_000 {
226+
d := dictionary[i%len(dictionary)]
227+
i++
228+
queue.Push(func(ctx context.Context) (RequestConf, error) {
229+
w := httptest.NewRecorder()
230+
231+
req := httptest.NewRequest(d.Verb, d.Path, nil)
232+
app.router.ServeHTTP(w, req)
233+
require.Equal(t, d.ExpectedStatus, w.Result().StatusCode)
234+
235+
if d.ExpectedBody != "" {
236+
data, err := io.ReadAll(w.Body)
237+
require.NoError(t, err)
238+
require.Equal(t, d.ExpectedBody, string(data))
239+
}
240+
241+
return d, nil
242+
})
243+
}
244+
245+
_, errors := queue.RunAll(context.TODO())
246+
require.Len(t, errors, 0)
247+
}
248+
249+
func writeOAS(t require.TestingT, tmpdir string, oas *openapi.OpenAPISpec) string {
250+
oasContent, err := json.Marshal(oas)
251+
require.NoError(t, err)
252+
253+
oasFileName := fmt.Sprintf("%s/oas.json", tmpdir)
254+
err = os.WriteFile(oasFileName, oasContent, 0644)
255+
require.NoError(t, err)
256+
return oasFileName
257+
}
258+
259+
func writePolicies(t require.TestingT, tmpdir string, policies []string) string {
260+
policyFileName := fmt.Sprintf("%s/policies.rego", tmpdir)
261+
policiesContent := []byte(strings.Join(policies, "\n"))
262+
err := os.WriteFile(policyFileName, policiesContent, 0644)
263+
require.NoError(t, err)
264+
return policyFileName
265+
}
266+
83267
func generateAndSaveConfig(t require.TestingT, tmpdir string, numberOfPaths int) (string, string) {
84-
oas := openapi.OpenAPISpec{
268+
oas := &openapi.OpenAPISpec{
85269
Paths: make(map[string]openapi.PathVerbs),
86270
}
87271
policies := []string{"package policies"}
@@ -135,17 +319,8 @@ func generateAndSaveConfig(t require.TestingT, tmpdir string, numberOfPaths int)
135319
}
136320
}
137321

138-
oasContent, err := json.Marshal(oas)
139-
require.NoError(t, err)
140-
141-
oasFileName := fmt.Sprintf("%s/oas.json", tmpdir)
142-
err = os.WriteFile(oasFileName, oasContent, 0644)
143-
require.NoError(t, err)
144-
145-
policyFileName := fmt.Sprintf("%s/policies.rego", tmpdir)
146-
policiesContent := []byte(strings.Join(policies, "\n"))
147-
err = os.WriteFile(policyFileName, policiesContent, 0644)
148-
require.NoError(t, err)
322+
oasFileName := writeOAS(t, tmpdir, oas)
323+
policyFileName := writePolicies(t, tmpdir, policies)
149324

150325
return oasFileName, policyFileName
151326
}
@@ -159,11 +334,11 @@ func generateFilterPolicy(name string) string {
159334

160335
func generateProjectionPolicy(name string) string {
161336
return fmt.Sprintf(`proj_%s [projects] {
162-
projects := [projects_with_envs_filtered |
337+
projects := [kept |
163338
project := input.response.body[_]
164-
projects_with_envs_filtered := project
339+
kept := project.key
165340
]
166-
}`, name)
341+
}`, name)
167342
}
168343

169344
func generateAllowPolicy(name string) string {

0 commit comments

Comments
 (0)