Skip to content

Commit 9f4583b

Browse files
V0.1.24 (#90)
* add Sanitizer hook for safe request/response masking before logging (#89) Co-authored-by: Danil S. <[email protected]>
1 parent 852c9ca commit 9f4583b

File tree

8 files changed

+253
-9
lines changed

8 files changed

+253
-9
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ jobs:
4141
name: examples
4242
runs-on: ubuntu-latest
4343
steps:
44-
- uses: actions/checkout@v3
44+
- uses: actions/checkout@v4
4545
- name: Run examples
4646
run: make examples
4747
- name: Archive code coverage results
48-
uses: actions/upload-artifact@v3
48+
uses: actions/upload-artifact@v4
4949
with:
5050
name: allure-results
5151
path: ./examples/allure-results

builder_request.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,22 @@ func (qt *cute) RequestRetryBroken(broken bool) RequestHTTPBuilder {
111111
return qt
112112
}
113113

114+
// RequestSanitizerHook assigns the provided RequestSanitizerHook to the test,
115+
// allowing URL sanitization before logging or reporting.
116+
func (qt *cute) RequestSanitizerHook(hook RequestSanitizerHook) RequestHTTPBuilder {
117+
qt.tests[qt.countTests].RequestSanitizer = hook
118+
119+
return qt
120+
}
121+
122+
// ResponseSanitizerHook assigns the provided ResponseSanitizerHook to the test,
123+
// allowing URL sanitization before logging or reporting.
124+
func (qt *cute) ResponseSanitizerHook(hook ResponseSanitizerHook) RequestHTTPBuilder {
125+
qt.tests[qt.countTests].ResponseSanitizer = hook
126+
127+
return qt
128+
}
129+
114130
func (qt *cute) Request(r *http.Request) ExpectHTTPBuilder {
115131
qt.tests[qt.countTests].Request.Base = r
116132

examples/inside_step_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/ozontech/allure-go/pkg/framework/provider"
1414
"github.com/ozontech/allure-go/pkg/framework/runner"
15+
1516
"github.com/ozontech/cute"
1617
)
1718

examples/masked_data_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//go:build example
2+
// +build example
3+
4+
package examples
5+
6+
import (
7+
"context"
8+
"net/http"
9+
"net/url"
10+
"testing"
11+
"time"
12+
13+
"github.com/ozontech/allure-go/pkg/framework/provider"
14+
"github.com/ozontech/allure-go/pkg/framework/runner"
15+
16+
"github.com/ozontech/cute"
17+
)
18+
19+
func TestSanitizer(t *testing.T) {
20+
runner.Run(t, "Single test with request and response sanitizer", func(t provider.T) {
21+
22+
t.WithNewStep("First step", func(sCtx provider.StepCtx) {
23+
sCtx.NewStep("Inside first step")
24+
})
25+
26+
t.WithNewStep("Step name", func(sCtx provider.StepCtx) {
27+
u, _ := url.Parse("https://jsonplaceholder.typicode.com/posts/1/comments?example=11")
28+
query := u.Query()
29+
query.Set("name", "Vasya")
30+
u.RawQuery = query.Encode()
31+
32+
cute.NewTestBuilder().
33+
Title("Super simple test").
34+
Tags("simple", "suite", "some_local_tag", "json").
35+
Parallel().
36+
Create().
37+
RequestSanitizerHook(func(req *http.Request) {
38+
req.URL.Path = "/path/masked"
39+
40+
values := req.URL.Query()
41+
values.Set("example", "masked")
42+
43+
req.URL.RawQuery = values.Encode()
44+
45+
req.Header["some_header"] = []string{"masked"}
46+
}).
47+
ResponseSanitizerHook(func(resp *http.Response) {
48+
resp.Header["some_header"] = []string{"masked"}
49+
resp.Header["Content-Type"] = []string{"masked"}
50+
}).
51+
RequestBuilder(
52+
cute.WithHeaders(map[string][]string{
53+
"some_header": []string{"something"},
54+
}),
55+
cute.WithURL(u),
56+
cute.WithMethod(http.MethodPost),
57+
).
58+
ExpectExecuteTimeout(10*time.Second).
59+
ExpectStatus(http.StatusCreated).
60+
ExecuteTest(context.Background(), sCtx)
61+
})
62+
})
63+
64+
}

interface.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,20 @@ type RequestParams interface {
218218
// Deprecated: use RequestRetryBroken instead
219219
RequestRepeatBroken(broken bool) RequestHTTPBuilder
220220
RequestRetryBroken(broken bool) RequestHTTPBuilder
221+
222+
// RequestSanitizerHook sets a RequestSanitizerHook function for the request.
223+
// This hook allows you to modify or mask parts of the request URL (e.g., hide sensitive data)
224+
// before it is logged or added to the test report (Allure).
225+
// Example usage: RequestWithSanitizeHook(func(req *http.Request) { ... }).
226+
// Example: RequestWithSanitizeHook(func(req *http.Request) { req.URL.Path = "/masked" }).
227+
// Example: RequestWithSanitizeHook(func(req *http.Request) { req.Header["some_header"] = []string{"masked"} }).
228+
RequestSanitizerHook(hook RequestSanitizerHook) RequestHTTPBuilder
229+
230+
// ResponseSanitizerHook sets a ResponseSanitizerHook function for the request.
231+
// This hook allows you to modify or mask parts of the response body (e.g., hide sensitive data)
232+
// before it is logged or added to the test report (Allure).
233+
// Example usage: ResponseWithSanitizeHook(func(resp *http.Response) { ... }).
234+
ResponseSanitizerHook(hook ResponseSanitizerHook) RequestHTTPBuilder
221235
}
222236

223237
// ExpectHTTPBuilder is a scope of methods for validate http response

roundtripper.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import (
1010
"time"
1111

1212
"github.com/ozontech/allure-go/pkg/allure"
13+
"moul.io/http2curl/v2"
14+
1315
cuteErrors "github.com/ozontech/cute/errors"
1416
"github.com/ozontech/cute/internal/utils"
15-
"moul.io/http2curl/v2"
1617
)
1718

1819
func (it *Test) makeRequest(t internalT, req *http.Request) (*http.Response, []error) {
@@ -34,7 +35,7 @@ func (it *Test) makeRequest(t internalT, req *http.Request) (*http.Response, []e
3435
}
3536

3637
for i := 1; i <= countRepeat; i++ {
37-
it.executeWithStep(t, createTitle(i, countRepeat, req), func(t T) []error {
38+
it.executeWithStep(t, it.createTitle(i, countRepeat, req), func(t T) []error {
3839
resp, err = it.doRequest(t, req)
3940
if err != nil {
4041
if it.Request.Retry.Broken {
@@ -148,6 +149,12 @@ func (it *Test) addInformationRequest(t T, req *http.Request) error {
148149
err error
149150
)
150151

152+
if it.RequestSanitizer != nil {
153+
it.RequestSanitizer(req)
154+
}
155+
156+
it.lastRequestURL = req.URL.String()
157+
151158
curl, err := http2curl.GetCurlCommand(req)
152159
if err != nil {
153160
return err
@@ -215,6 +222,10 @@ func (it *Test) addInformationResponse(t T, response *http.Response) error {
215222
err error
216223
)
217224

225+
if it.ResponseSanitizer != nil {
226+
it.ResponseSanitizer(response)
227+
}
228+
218229
headers, _ := utils.ToJSON(response.Header)
219230
if headers != "" {
220231
t.WithNewParameters("response_headers", headers)
@@ -265,8 +276,24 @@ func (it *Test) addInformationResponse(t T, response *http.Response) error {
265276
return nil
266277
}
267278

268-
func createTitle(try, countRepeat int, req *http.Request) string {
269-
title := req.Method + " " + req.URL.String()
279+
func (it *Test) createTitle(try, countRepeat int, req *http.Request) string {
280+
toProcess := req
281+
282+
// We have to execute sanitizer hook because
283+
// we need to log it and it can contain sensitive data
284+
if it.RequestSanitizer != nil {
285+
clone, err := copyRequest(req.Context(), req)
286+
287+
// ignore error, because we want to log request
288+
// and it does not matter if we can copy request
289+
if err == nil {
290+
it.RequestSanitizer(clone)
291+
292+
toProcess = clone
293+
}
294+
}
295+
296+
title := toProcess.Method + " " + toProcess.URL.String()
270297

271298
if countRepeat == 1 {
272299
return title

test.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,21 @@ var (
2929
errorRequestURLEmpty = errors.New("url request must be not empty")
3030
)
3131

32+
// RequestSanitizerHook is a function used to modify the request URL
33+
// before it is logged or attached to test reports (e.g., for hiding secrets).
34+
type RequestSanitizerHook func(req *http.Request)
35+
36+
// ResponseSanitizerHook is a function used to modify the response
37+
// before it is logged or attached to test reports (e.g., for hiding secrets).
38+
type ResponseSanitizerHook func(resp *http.Response)
39+
3240
// Test is a main struct of test.
3341
// You may field Request and Expect for create simple test
3442
// Parallel can be used to control the parallelism of a Test
3543
type Test struct {
36-
httpClient *http.Client
37-
jsonMarshaler JSONMarshaler
44+
httpClient *http.Client
45+
jsonMarshaler JSONMarshaler
46+
lastRequestURL string
3847

3948
Name string
4049
Parallel bool
@@ -44,6 +53,9 @@ type Test struct {
4453
Middleware *Middleware
4554
Request *Request
4655
Expect *Expect
56+
57+
RequestSanitizer RequestSanitizerHook
58+
ResponseSanitizer ResponseSanitizerHook
4759
}
4860

4961
// Retry is a struct to control the retry of a whole single test (not only the request)
@@ -474,6 +486,7 @@ func (it *Test) beforeTest(t internalT, req *http.Request) []error {
474486
})
475487
}
476488

489+
// createRequest builds the final *http.Request to be executed by the test.
477490
func (it *Test) createRequest(ctx context.Context) (*http.Request, error) {
478491
var (
479492
req = it.Request.Base

test_test.go

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import (
66
"errors"
77
"io"
88
"net/http"
9+
"net/http/httptest"
910
"net/url"
11+
"strings"
1012
"testing"
1113

1214
"github.com/ozontech/allure-go/pkg/framework/core/common"
13-
"github.com/ozontech/cute/internal/utils"
1415
"github.com/stretchr/testify/require"
16+
17+
"github.com/ozontech/cute/internal/utils"
1518
)
1619

1720
func TestCreateRequest(t *testing.T) {
@@ -163,3 +166,109 @@ func TestValidateResponseWithErrors(t *testing.T) {
163166

164167
require.Len(t, errs, 2)
165168
}
169+
170+
type mockRoundTripper struct{}
171+
172+
func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
173+
return &http.Response{
174+
StatusCode: 200,
175+
Request: req,
176+
Body: io.NopCloser(strings.NewReader("mock response")),
177+
}, nil
178+
}
179+
180+
func TestSanitizeURLHook(t *testing.T) {
181+
client := &http.Client{
182+
Transport: &mockRoundTripper{},
183+
}
184+
185+
test := &Test{
186+
httpClient: client,
187+
Retry: &Retry{
188+
currentCount: 0,
189+
MaxAttempts: 0,
190+
Delay: 0,
191+
},
192+
Request: &Request{
193+
Builders: []RequestBuilder{
194+
WithMethod(http.MethodGet),
195+
WithURI("http://localhost/api?key=123"),
196+
},
197+
Retry: &RequestRetryPolitic{
198+
Count: 1,
199+
Delay: 2,
200+
},
201+
},
202+
RequestSanitizer: sanitizeKeyParam("****"),
203+
}
204+
205+
req, err := test.createRequest(context.Background())
206+
require.NoError(t, err)
207+
require.NotNil(t, req)
208+
209+
newT := createAllureT(t)
210+
211+
err = test.addInformationRequest(newT, req)
212+
require.NoError(t, err)
213+
214+
decodedQuery, err := url.QueryUnescape(req.URL.RawQuery)
215+
require.NoError(t, err)
216+
require.Equal(t, "key=****", decodedQuery)
217+
}
218+
219+
func TestSanitizeURL_LastRequestURL(t *testing.T) {
220+
client := &http.Client{
221+
Transport: &mockRoundTripper{},
222+
}
223+
224+
test := &Test{
225+
httpClient: client,
226+
Request: &Request{
227+
Builders: []RequestBuilder{
228+
WithMethod(http.MethodGet),
229+
WithURI("http://localhost/api?key=123"),
230+
},
231+
},
232+
RequestSanitizer: sanitizeKeyParam("****"),
233+
}
234+
235+
allureT := createAllureT(t)
236+
test.Execute(context.Background(), allureT)
237+
238+
decodedURL, err := url.QueryUnescape(test.lastRequestURL)
239+
require.NoError(t, err)
240+
require.Contains(t, decodedURL, "key=****", "Expected masked key in lastRequestURL")
241+
}
242+
243+
func sanitizeKeyParam(mask string) RequestSanitizerHook {
244+
return func(req *http.Request) {
245+
q := req.URL.Query()
246+
q.Set("key", mask)
247+
req.URL.RawQuery = q.Encode()
248+
}
249+
}
250+
251+
func TestSanitizeURL_RealRequest(t *testing.T) {
252+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
253+
body, _ := io.ReadAll(r.Body)
254+
t.Logf("Server received URL: %s, Body: %s", r.URL.String(), string(body))
255+
require.Contains(t, r.URL.String(), "key=123", "Sanitizer must not change real request")
256+
w.WriteHeader(200)
257+
}))
258+
defer ts.Close()
259+
260+
client := &http.Client{}
261+
test := &Test{
262+
httpClient: client,
263+
Request: &Request{
264+
Builders: []RequestBuilder{
265+
WithMethod(http.MethodGet),
266+
WithURI(ts.URL + "/api?key=123"),
267+
},
268+
},
269+
RequestSanitizer: sanitizeKeyParam("****"),
270+
}
271+
272+
allureT := createAllureT(t)
273+
test.Execute(context.Background(), allureT)
274+
}

0 commit comments

Comments
 (0)