Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ 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
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: allure-results
path: ./examples/allure-results
16 changes: 16 additions & 0 deletions builder_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,22 @@ func (qt *cute) RequestRetryBroken(broken bool) RequestHTTPBuilder {
return qt
}

// RequestSanitizerHook assigns the provided RequestSanitizerHook to the test,
// allowing URL sanitization before logging or reporting.
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
}

func (qt *cute) Request(r *http.Request) ExpectHTTPBuilder {
qt.tests[qt.countTests].Request.Base = r

Expand Down
1 change: 1 addition & 0 deletions examples/inside_step_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down
64 changes: 64 additions & 0 deletions examples/masked_data_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
})

}
14 changes: 14 additions & 0 deletions interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,20 @@ type RequestParams interface {
// Deprecated: use RequestRetryBroken instead
RequestRepeatBroken(broken bool) RequestHTTPBuilder
RequestRetryBroken(broken bool) RequestHTTPBuilder

// 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) { ... }).
// 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
Expand Down
35 changes: 31 additions & 4 deletions roundtripper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -34,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 {
Expand Down Expand Up @@ -148,6 +149,12 @@ func (it *Test) addInformationRequest(t T, req *http.Request) error {
err error
)

if it.RequestSanitizer != nil {
Comment thread
siller174 marked this conversation as resolved.
it.RequestSanitizer(req)
}

it.lastRequestURL = req.URL.String()

curl, err := http2curl.GetCurlCommand(req)
if err != nil {
return err
Expand Down Expand Up @@ -215,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)
Expand Down Expand Up @@ -265,8 +276,24 @@ 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 {
clone, err := copyRequest(req.Context(), req)

// ignore error, because we want to log request
// and it does not matter if we can copy request
if err == nil {
it.RequestSanitizer(clone)

toProcess = clone
}
}

title := toProcess.Method + " " + toProcess.URL.String()

if countRepeat == 1 {
return title
Expand Down
17 changes: 15 additions & 2 deletions test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,21 @@ var (
errorRequestURLEmpty = errors.New("url request must be not empty")
)

// 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 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.
// 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
Expand All @@ -44,6 +53,9 @@ type Test struct {
Middleware *Middleware
Request *Request
Expect *Expect

RequestSanitizer RequestSanitizerHook
ResponseSanitizer ResponseSanitizerHook
}

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

// createRequest builds the final *http.Request to be executed by the test.
func (it *Test) createRequest(ctx context.Context) (*http.Request, error) {
var (
req = it.Request.Base
Expand Down
111 changes: 110 additions & 1 deletion test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import (
"errors"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"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) {
Expand Down Expand Up @@ -163,3 +166,109 @@ 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,
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("****"),
}

req, err := test.createRequest(context.Background())
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)
}

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"),
},
},
RequestSanitizer: 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) RequestSanitizerHook {
return func(req *http.Request) {
q := req.URL.Query()
q.Set("key", mask)
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)
}
Loading