From bb3e50af9960c4e83f4ee3234ee98b2406230a35 Mon Sep 17 00:00:00 2001 From: "Danil S." <55463070+ExcpOccured@users.noreply.github.com> Date: Wed, 7 May 2025 15:55:38 +0300 Subject: [PATCH 1/9] add SanitizeURL hook for safe request masking before logging (#89) * Add and test Sanitizer hook for request masking before logging --- builder_request.go | 8 ++++++ interface.go | 6 ++++ roundtripper.go | 2 ++ test.go | 18 ++++++++++-- test_test.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 2 deletions(-) diff --git a/builder_request.go b/builder_request.go index 67657f4..16f1a31 100644 --- a/builder_request.go +++ b/builder_request.go @@ -111,6 +111,14 @@ func (qt *cute) RequestRetryBroken(broken bool) RequestHTTPBuilder { return qt } +// RequestWithSanitizeHook assigns the provided SanitizeHook to the test, +// allowing URL sanitization before logging or reporting. +func (qt *cute) RequestWithSanitizeHook(hook SanitizeHook) RequestHTTPBuilder { + qt.tests[qt.countTests].Sanitizer = hook + + return qt +} + func (qt *cute) Request(r *http.Request) ExpectHTTPBuilder { qt.tests[qt.countTests].Request.Base = r diff --git a/interface.go b/interface.go index 5cac53a..36cdf32 100644 --- a/interface.go +++ b/interface.go @@ -218,6 +218,12 @@ type RequestParams interface { // Deprecated: use RequestRetryBroken instead RequestRepeatBroken(broken bool) RequestHTTPBuilder RequestRetryBroken(broken bool) RequestHTTPBuilder + + // RequestWithSanitizeHook sets a SanitizeHook function for the request. + // This hook allows you to modify or mask parts of the request URL (e.g., hide sensitive data) + // before it is logged or added to the test report (Allure). + // Example usage: RequestWithSanitizeHook(func(req *http.Request) { ... }). + RequestWithSanitizeHook(hook SanitizeHook) RequestHTTPBuilder } // ExpectHTTPBuilder is a scope of methods for validate http response diff --git a/roundtripper.go b/roundtripper.go index bb971c6..0d2a3af 100644 --- a/roundtripper.go +++ b/roundtripper.go @@ -148,6 +148,8 @@ func (it *Test) addInformationRequest(t T, req *http.Request) error { err error ) + it.lastRequestURL = req.URL.String() + curl, err := http2curl.GetCurlCommand(req) if err != nil { return err diff --git a/test.go b/test.go index 873eec0..69fc6f8 100644 --- a/test.go +++ b/test.go @@ -29,12 +29,17 @@ var ( errorRequestURLEmpty = errors.New("url request must be not empty") ) +// SanitizeHook is a function used to modify the request URL +// before it is logged or attached to test reports (e.g., for hiding secrets). +type SanitizeHook func(req *http.Request) + // Test is a main struct of test. // You may field Request and Expect for create simple test // Parallel can be used to control the parallelism of a Test type Test struct { - httpClient *http.Client - jsonMarshaler JSONMarshaler + httpClient *http.Client + jsonMarshaler JSONMarshaler + lastRequestURL string Name string Parallel bool @@ -44,6 +49,8 @@ type Test struct { Middleware *Middleware Request *Request Expect *Expect + + Sanitizer SanitizeHook } // Retry is a struct to control the retry of a whole single test (not only the request) @@ -474,6 +481,9 @@ func (it *Test) beforeTest(t internalT, req *http.Request) []error { }) } +// createRequest builds the final *http.Request to be executed by the test. +// If the Test.Sanitizer hook is defined, it will be called after validation +// to allow safe modification of the request before logging or execution. func (it *Test) createRequest(ctx context.Context) (*http.Request, error) { var ( req = it.Request.Base @@ -492,6 +502,10 @@ func (it *Test) createRequest(ctx context.Context) (*http.Request, error) { return nil, err } + if it.Sanitizer != nil { + it.Sanitizer(req) + } + return req, nil } diff --git a/test_test.go b/test_test.go index ca34f77..a22f0f4 100644 --- a/test_test.go +++ b/test_test.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "net/url" + "strings" "testing" "github.com/ozontech/allure-go/pkg/framework/core/common" @@ -163,3 +164,70 @@ func TestValidateResponseWithErrors(t *testing.T) { require.Len(t, errs, 2) } + +type mockRoundTripper struct{} + +func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Request: req, + Body: io.NopCloser(strings.NewReader("mock response")), + }, nil +} + +func TestSanitizeURLHook(t *testing.T) { + client := &http.Client{ + Transport: &mockRoundTripper{}, + } + + test := &Test{ + httpClient: client, + Request: &Request{ + Builders: []RequestBuilder{ + WithMethod(http.MethodGet), + WithURI("http://localhost/api?key=123"), + }, + }, + Sanitizer: sanitizeKeyParam("****"), + } + + req, err := test.createRequest(context.Background()) + require.NoError(t, err) + require.NotNil(t, req) + + decodedQuery, err := url.QueryUnescape(req.URL.RawQuery) + require.NoError(t, err) + require.Equal(t, "key=****", decodedQuery) +} + +func TestSanitizeURL_LastRequestURL(t *testing.T) { + client := &http.Client{ + Transport: &mockRoundTripper{}, + } + + test := &Test{ + httpClient: client, + Request: &Request{ + Builders: []RequestBuilder{ + WithMethod(http.MethodGet), + WithURI("http://localhost/api?key=123"), + }, + }, + Sanitizer: sanitizeKeyParam("****"), + } + + allureT := createAllureT(t) + test.Execute(context.Background(), allureT) + + decodedURL, err := url.QueryUnescape(test.lastRequestURL) + require.NoError(t, err) + require.Contains(t, decodedURL, "key=****", "Expected masked key in lastRequestURL") +} + +func sanitizeKeyParam(mask string) SanitizeHook { + return func(req *http.Request) { + q := req.URL.Query() + q.Set("key", mask) + req.URL.RawQuery = q.Encode() + } +} From c750017549b18b8d1ad2cd30f6d8ba4a71a1afcf Mon Sep 17 00:00:00 2001 From: Sergey Makarov Date: Wed, 7 May 2025 15:06:54 +0200 Subject: [PATCH 2/9] add response sanitazer --- builder_request.go | 14 +++++++++++--- examples/inside_step_test.go | 6 ++++++ interface.go | 12 ++++++++++-- roundtripper.go | 11 ++++++++++- test.go | 15 +++++++-------- test_test.go | 9 +++++---- 6 files changed, 49 insertions(+), 18 deletions(-) diff --git a/builder_request.go b/builder_request.go index 16f1a31..32d575b 100644 --- a/builder_request.go +++ b/builder_request.go @@ -111,10 +111,18 @@ func (qt *cute) RequestRetryBroken(broken bool) RequestHTTPBuilder { return qt } -// RequestWithSanitizeHook assigns the provided SanitizeHook to the test, +// RequestSanitizerHook assigns the provided RequestSanitizerHook to the test, // allowing URL sanitization before logging or reporting. -func (qt *cute) RequestWithSanitizeHook(hook SanitizeHook) RequestHTTPBuilder { - qt.tests[qt.countTests].Sanitizer = hook +func (qt *cute) RequestSanitizerHook(hook RequestSanitizerHook) RequestHTTPBuilder { + qt.tests[qt.countTests].RequestSanitizer = hook + + return qt +} + +// ResponseSanitizerHook assigns the provided ResponseSanitizerHook to the test, +// allowing URL sanitization before logging or reporting. +func (qt *cute) ResponseSanitizerHook(hook ResponseSanitizerHook) RequestHTTPBuilder { + qt.tests[qt.countTests].ResponseSanitizer = hook return qt } diff --git a/examples/inside_step_test.go b/examples/inside_step_test.go index d980589..98f39f1 100644 --- a/examples/inside_step_test.go +++ b/examples/inside_step_test.go @@ -12,6 +12,7 @@ import ( "github.com/ozontech/allure-go/pkg/framework/provider" "github.com/ozontech/allure-go/pkg/framework/runner" + "github.com/ozontech/cute" ) @@ -30,6 +31,11 @@ func TestInsideStep(t *testing.T) { Tags("simple", "suite", "some_local_tag", "json"). Parallel(). Create(). + RequestSanitizerHook(func(req *http.Request) { + req.URL.Path = "/path/masked" + + req.Header["some_header"] = []string{"masked"} + }). RequestBuilder( cute.WithHeaders(map[string][]string{ "some_header": []string{"something"}, diff --git a/interface.go b/interface.go index 36cdf32..0f514ee 100644 --- a/interface.go +++ b/interface.go @@ -219,11 +219,19 @@ type RequestParams interface { RequestRepeatBroken(broken bool) RequestHTTPBuilder RequestRetryBroken(broken bool) RequestHTTPBuilder - // RequestWithSanitizeHook sets a SanitizeHook function for the request. + // RequestSanitizerHook sets a RequestSanitizerHook function for the request. // This hook allows you to modify or mask parts of the request URL (e.g., hide sensitive data) // before it is logged or added to the test report (Allure). // Example usage: RequestWithSanitizeHook(func(req *http.Request) { ... }). - RequestWithSanitizeHook(hook SanitizeHook) RequestHTTPBuilder + // Example: RequestWithSanitizeHook(func(req *http.Request) { req.URL.Path = "/masked" }). + // Example: RequestWithSanitizeHook(func(req *http.Request) { req.Header["some_header"] = []string{"masked"} }). + RequestSanitizerHook(hook RequestSanitizerHook) RequestHTTPBuilder + + // ResponseSanitizerHook sets a ResponseSanitizerHook function for the request. + // This hook allows you to modify or mask parts of the response body (e.g., hide sensitive data) + // before it is logged or added to the test report (Allure). + // Example usage: ResponseWithSanitizeHook(func(resp *http.Response) { ... }). + ResponseSanitizerHook(hook ResponseSanitizerHook) RequestHTTPBuilder } // ExpectHTTPBuilder is a scope of methods for validate http response diff --git a/roundtripper.go b/roundtripper.go index 0d2a3af..9fe4418 100644 --- a/roundtripper.go +++ b/roundtripper.go @@ -10,9 +10,10 @@ import ( "time" "github.com/ozontech/allure-go/pkg/allure" + "moul.io/http2curl/v2" + cuteErrors "github.com/ozontech/cute/errors" "github.com/ozontech/cute/internal/utils" - "moul.io/http2curl/v2" ) func (it *Test) makeRequest(t internalT, req *http.Request) (*http.Response, []error) { @@ -148,6 +149,10 @@ func (it *Test) addInformationRequest(t T, req *http.Request) error { err error ) + if it.RequestSanitizer != nil { + it.RequestSanitizer(req) + } + it.lastRequestURL = req.URL.String() curl, err := http2curl.GetCurlCommand(req) @@ -217,6 +222,10 @@ func (it *Test) addInformationResponse(t T, response *http.Response) error { err error ) + if it.ResponseSanitizer != nil { + it.ResponseSanitizer(response) + } + headers, _ := utils.ToJSON(response.Header) if headers != "" { t.WithNewParameters("response_headers", headers) diff --git a/test.go b/test.go index 69fc6f8..8210762 100644 --- a/test.go +++ b/test.go @@ -29,9 +29,11 @@ var ( errorRequestURLEmpty = errors.New("url request must be not empty") ) -// SanitizeHook is a function used to modify the request URL +// RequestSanitizerHook is a function used to modify the request URL // before it is logged or attached to test reports (e.g., for hiding secrets). -type SanitizeHook func(req *http.Request) +type RequestSanitizerHook func(req *http.Request) + +type ResponseSanitizerHook func(resp *http.Response) // Test is a main struct of test. // You may field Request and Expect for create simple test @@ -50,7 +52,8 @@ type Test struct { Request *Request Expect *Expect - Sanitizer SanitizeHook + RequestSanitizer RequestSanitizerHook + ResponseSanitizer ResponseSanitizerHook } // Retry is a struct to control the retry of a whole single test (not only the request) @@ -482,7 +485,7 @@ func (it *Test) beforeTest(t internalT, req *http.Request) []error { } // createRequest builds the final *http.Request to be executed by the test. -// If the Test.Sanitizer hook is defined, it will be called after validation +// If the Test.RequestSanitizer hook is defined, it will be called after validation // to allow safe modification of the request before logging or execution. func (it *Test) createRequest(ctx context.Context) (*http.Request, error) { var ( @@ -502,10 +505,6 @@ func (it *Test) createRequest(ctx context.Context) (*http.Request, error) { return nil, err } - if it.Sanitizer != nil { - it.Sanitizer(req) - } - return req, nil } diff --git a/test_test.go b/test_test.go index a22f0f4..92d74ca 100644 --- a/test_test.go +++ b/test_test.go @@ -11,8 +11,9 @@ import ( "testing" "github.com/ozontech/allure-go/pkg/framework/core/common" - "github.com/ozontech/cute/internal/utils" "github.com/stretchr/testify/require" + + "github.com/ozontech/cute/internal/utils" ) func TestCreateRequest(t *testing.T) { @@ -188,7 +189,7 @@ func TestSanitizeURLHook(t *testing.T) { WithURI("http://localhost/api?key=123"), }, }, - Sanitizer: sanitizeKeyParam("****"), + RequestSanitizer: sanitizeKeyParam("****"), } req, err := test.createRequest(context.Background()) @@ -213,7 +214,7 @@ func TestSanitizeURL_LastRequestURL(t *testing.T) { WithURI("http://localhost/api?key=123"), }, }, - Sanitizer: sanitizeKeyParam("****"), + RequestSanitizer: sanitizeKeyParam("****"), } allureT := createAllureT(t) @@ -224,7 +225,7 @@ func TestSanitizeURL_LastRequestURL(t *testing.T) { require.Contains(t, decodedURL, "key=****", "Expected masked key in lastRequestURL") } -func sanitizeKeyParam(mask string) SanitizeHook { +func sanitizeKeyParam(mask string) RequestSanitizerHook { return func(req *http.Request) { q := req.URL.Query() q.Set("key", mask) From c8383662a0f0c11968a808069f17c853cac35c1d Mon Sep 17 00:00:00 2001 From: Sergey Makarov Date: Wed, 7 May 2025 15:13:08 +0200 Subject: [PATCH 3/9] fix test --- test_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test_test.go b/test_test.go index 92d74ca..5e1c831 100644 --- a/test_test.go +++ b/test_test.go @@ -183,11 +183,20 @@ func TestSanitizeURLHook(t *testing.T) { test := &Test{ httpClient: client, + Retry: &Retry{ + currentCount: 0, + MaxAttempts: 0, + Delay: 0, + }, Request: &Request{ Builders: []RequestBuilder{ WithMethod(http.MethodGet), WithURI("http://localhost/api?key=123"), }, + Retry: &RequestRetryPolitic{ + Count: 1, + Delay: 2, + }, }, RequestSanitizer: sanitizeKeyParam("****"), } @@ -196,6 +205,11 @@ func TestSanitizeURLHook(t *testing.T) { require.NoError(t, err) require.NotNil(t, req) + newT := createAllureT(t) + + err = test.addInformationRequest(newT, req) + require.NoError(t, err) + decodedQuery, err := url.QueryUnescape(req.URL.RawQuery) require.NoError(t, err) require.Equal(t, "key=****", decodedQuery) From 94c1751f45b4e8d3df09e992865387b2d24f5ba0 Mon Sep 17 00:00:00 2001 From: Sergey Makarov Date: Wed, 7 May 2025 15:14:07 +0200 Subject: [PATCH 4/9] fix comments --- test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test.go b/test.go index 8210762..6b7e951 100644 --- a/test.go +++ b/test.go @@ -33,6 +33,8 @@ var ( // before it is logged or attached to test reports (e.g., for hiding secrets). type RequestSanitizerHook func(req *http.Request) +// ResponseSanitizerHook is a function used to modify the response +// before it is logged or attached to test reports (e.g., for hiding secrets). type ResponseSanitizerHook func(resp *http.Response) // Test is a main struct of test. From a4cabd5da3dac9487e9d1b86f538e6b2ad5cbb4e Mon Sep 17 00:00:00 2001 From: Sergey Makarov Date: Wed, 7 May 2025 16:04:27 +0200 Subject: [PATCH 5/9] fix ci/cd --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 878c957..fd27101 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,7 +41,7 @@ jobs: name: examples runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run examples run: make examples - name: Archive code coverage results From 25bf1e76986fa21dbc2998031df61d05d02ee41d Mon Sep 17 00:00:00 2001 From: Sergey Makarov Date: Wed, 7 May 2025 16:07:02 +0200 Subject: [PATCH 6/9] fix ci/cd --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fd27101..ea50a09 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,7 +45,7 @@ jobs: - name: Run examples run: make examples - name: Archive code coverage results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: allure-results path: ./examples/allure-results \ No newline at end of file From f14b10a700ce3dd1df4ea8ca48153a3db4dc183e Mon Sep 17 00:00:00 2001 From: Sergey Makarov Date: Wed, 7 May 2025 21:51:08 +0200 Subject: [PATCH 7/9] fix request sanitizer --- examples/inside_step_test.go | 5 --- examples/masked_data_test.go | 64 ++++++++++++++++++++++++++++++++++++ roundtripper.go | 20 +++++++++-- test.go | 2 -- 4 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 examples/masked_data_test.go diff --git a/examples/inside_step_test.go b/examples/inside_step_test.go index 98f39f1..61c15a8 100644 --- a/examples/inside_step_test.go +++ b/examples/inside_step_test.go @@ -31,11 +31,6 @@ func TestInsideStep(t *testing.T) { Tags("simple", "suite", "some_local_tag", "json"). Parallel(). Create(). - RequestSanitizerHook(func(req *http.Request) { - req.URL.Path = "/path/masked" - - req.Header["some_header"] = []string{"masked"} - }). RequestBuilder( cute.WithHeaders(map[string][]string{ "some_header": []string{"something"}, diff --git a/examples/masked_data_test.go b/examples/masked_data_test.go new file mode 100644 index 0000000..af9a1e6 --- /dev/null +++ b/examples/masked_data_test.go @@ -0,0 +1,64 @@ +//go:build example +// +build example + +package examples + +import ( + "context" + "net/http" + "net/url" + "testing" + "time" + + "github.com/ozontech/allure-go/pkg/framework/provider" + "github.com/ozontech/allure-go/pkg/framework/runner" + + "github.com/ozontech/cute" +) + +func TestSanitizer(t *testing.T) { + runner.Run(t, "Single test with request and response sanitizer", func(t provider.T) { + + t.WithNewStep("First step", func(sCtx provider.StepCtx) { + sCtx.NewStep("Inside first step") + }) + + t.WithNewStep("Step name", func(sCtx provider.StepCtx) { + u, _ := url.Parse("https://jsonplaceholder.typicode.com/posts/1/comments?example=11") + query := u.Query() + query.Set("name", "Vasya") + u.RawQuery = query.Encode() + + cute.NewTestBuilder(). + Title("Super simple test"). + Tags("simple", "suite", "some_local_tag", "json"). + Parallel(). + Create(). + RequestSanitizerHook(func(req *http.Request) { + req.URL.Path = "/path/masked" + + values := req.URL.Query() + values.Set("example", "masked") + + req.URL.RawQuery = values.Encode() + + req.Header["some_header"] = []string{"masked"} + }). + ResponseSanitizerHook(func(resp *http.Response) { + resp.Header["some_header"] = []string{"masked"} + resp.Header["Content-Type"] = []string{"masked"} + }). + RequestBuilder( + cute.WithHeaders(map[string][]string{ + "some_header": []string{"something"}, + }), + cute.WithURL(u), + cute.WithMethod(http.MethodPost), + ). + ExpectExecuteTimeout(10*time.Second). + ExpectStatus(http.StatusCreated). + ExecuteTest(context.Background(), sCtx) + }) + }) + +} diff --git a/roundtripper.go b/roundtripper.go index 9fe4418..a66a246 100644 --- a/roundtripper.go +++ b/roundtripper.go @@ -35,7 +35,7 @@ func (it *Test) makeRequest(t internalT, req *http.Request) (*http.Response, []e } for i := 1; i <= countRepeat; i++ { - it.executeWithStep(t, createTitle(i, countRepeat, req), func(t T) []error { + it.executeWithStep(t, it.createTitle(i, countRepeat, req), func(t T) []error { resp, err = it.doRequest(t, req) if err != nil { if it.Request.Retry.Broken { @@ -276,8 +276,22 @@ func (it *Test) addInformationResponse(t T, response *http.Response) error { return nil } -func createTitle(try, countRepeat int, req *http.Request) string { - title := req.Method + " " + req.URL.String() +func (it *Test) createTitle(try, countRepeat int, req *http.Request) string { + toProcess := req + + // We have to execute sanitizer hook because + // we need to log it and it can contain sensitive data + if it.RequestSanitizer != nil { + // ignore error, because we want to log request + // and it does not matter if we can copy request + clone, _ := copyRequest(req.Context(), req) + + it.RequestSanitizer(clone) + + toProcess = clone + } + + title := toProcess.Method + " " + toProcess.URL.String() if countRepeat == 1 { return title diff --git a/test.go b/test.go index 6b7e951..5fca33d 100644 --- a/test.go +++ b/test.go @@ -487,8 +487,6 @@ func (it *Test) beforeTest(t internalT, req *http.Request) []error { } // createRequest builds the final *http.Request to be executed by the test. -// If the Test.RequestSanitizer hook is defined, it will be called after validation -// to allow safe modification of the request before logging or execution. func (it *Test) createRequest(ctx context.Context) (*http.Request, error) { var ( req = it.Request.Base From 38e035d86f23294c38877e11a32ff18ed1dc5822 Mon Sep 17 00:00:00 2001 From: Sergey Makarov Date: Wed, 7 May 2025 21:52:46 +0200 Subject: [PATCH 8/9] fix logic --- roundtripper.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/roundtripper.go b/roundtripper.go index a66a246..989f251 100644 --- a/roundtripper.go +++ b/roundtripper.go @@ -282,13 +282,15 @@ func (it *Test) createTitle(try, countRepeat int, req *http.Request) string { // We have to execute sanitizer hook because // we need to log it and it can contain sensitive data if it.RequestSanitizer != nil { + clone, err := copyRequest(req.Context(), req) + // ignore error, because we want to log request // and it does not matter if we can copy request - clone, _ := copyRequest(req.Context(), req) - - it.RequestSanitizer(clone) + if err == nil { + it.RequestSanitizer(clone) - toProcess = clone + toProcess = clone + } } title := toProcess.Method + " " + toProcess.URL.String() From 8062bdf7c73543986280749d56582a0474be2c80 Mon Sep 17 00:00:00 2001 From: Sergey Makarov Date: Thu, 8 May 2025 18:45:29 +0200 Subject: [PATCH 9/9] add test --- test_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test_test.go b/test_test.go index 5e1c831..4074030 100644 --- a/test_test.go +++ b/test_test.go @@ -6,6 +6,7 @@ import ( "errors" "io" "net/http" + "net/http/httptest" "net/url" "strings" "testing" @@ -246,3 +247,28 @@ func sanitizeKeyParam(mask string) RequestSanitizerHook { req.URL.RawQuery = q.Encode() } } + +func TestSanitizeURL_RealRequest(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + t.Logf("Server received URL: %s, Body: %s", r.URL.String(), string(body)) + require.Contains(t, r.URL.String(), "key=123", "Sanitizer must not change real request") + w.WriteHeader(200) + })) + defer ts.Close() + + client := &http.Client{} + test := &Test{ + httpClient: client, + Request: &Request{ + Builders: []RequestBuilder{ + WithMethod(http.MethodGet), + WithURI(ts.URL + "/api?key=123"), + }, + }, + RequestSanitizer: sanitizeKeyParam("****"), + } + + allureT := createAllureT(t) + test.Execute(context.Background(), allureT) +}