diff --git a/helpers/tdhttp/request.go b/helpers/tdhttp/request.go index e2fc87c4..59c4325f 100644 --- a/helpers/tdhttp/request.go +++ b/helpers/tdhttp/request.go @@ -24,6 +24,39 @@ import ( ) func newRequest(method string, target string, body io.Reader, headersQueryParams []any) (*http.Request, error) { + header, qp, cookies, err := requestParams(headersQueryParams) + if err != nil { + return nil, err + } + + // Parse path even when no query params to have consistent error + // messages when using query params or not + u, err := url.Parse(target) + if err != nil { + return nil, errors.New(color.Bad("target is not a valid path: %s", err)) + } + if len(qp) > 0 { + if u.RawQuery != "" { + u.RawQuery += "&" + } + u.RawQuery += qp.Encode() + target = u.String() + } + + req := httptest.NewRequest(method, target, body) + + for k, v := range header { + req.Header[k] = append(req.Header[k], v...) + } + + for _, c := range cookies { + req.AddCookie(c) + } + + return req, nil +} + +func requestParams(headersQueryParams []any) (http.Header, url.Values, []*http.Cookie, error) { header := http.Header{} qp := url.Values{} var cookies []*http.Cookie @@ -37,7 +70,7 @@ func newRequest(method string, target string, body io.Reader, headersQueryParams if i < len(headersQueryParams) { var ok bool if val, ok = headersQueryParams[i].(string); !ok { - return nil, errors.New(color.Bad( + return nil, nil, nil, errors.New(color.Bad( `header "%s" should have a string value, not a %T (@ headersQueryParams[%d])`, cur, headersQueryParams[i], i)) } @@ -56,6 +89,9 @@ func newRequest(method string, target string, body io.Reader, headersQueryParams case http.Cookie: cookies = append(cookies, &cur) + case []*http.Cookie: + cookies = append(cookies, cur...) + case url.Values: for k, v := range cur { qp[k] = append(qp[k], v...) @@ -64,43 +100,24 @@ func newRequest(method string, target string, body io.Reader, headersQueryParams case Q: err := cur.AddTo(qp) if err != nil { - return nil, errors.New(color.Bad( + return nil, nil, nil, errors.New(color.Bad( "headersQueryParams... tdhttp.Q bad parameter: %s (@ headersQueryParams[%d])", err, i)) } default: - return nil, errors.New(color.Bad( - "headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not %T (@ headersQueryParams[%d])", + return nil, nil, nil, errors.New(color.Bad( + "headersQueryParams... can only contains string, http.Header, ([]*|*|)http.Cookie, url.Values and tdhttp.Q, not %T (@ headersQueryParams[%d])", cur, i)) } } - - // Parse path even when no query params to have consistent error - // messages when using query params or not - u, err := url.Parse(target) - if err != nil { - return nil, errors.New(color.Bad("target is not a valid path: %s", err)) + if len(header) == 0 { + header = nil } - if len(qp) > 0 { - if u.RawQuery != "" { - u.RawQuery += "&" - } - u.RawQuery += qp.Encode() - target = u.String() - } - - req := httptest.NewRequest(method, target, body) - - for k, v := range header { - req.Header[k] = append(req.Header[k], v...) + if len(qp) == 0 { + qp = nil } - - for _, c := range cookies { - req.AddCookie(c) - } - - return req, nil + return header, qp, cookies, nil } // BasicAuthHeader returns a new [http.Header] with only Authorization diff --git a/helpers/tdhttp/request_test.go b/helpers/tdhttp/request_test.go index d7cd2831..23257e78 100644 --- a/helpers/tdhttp/request_test.go +++ b/helpers/tdhttp/request_test.go @@ -64,9 +64,15 @@ func TestNewRequest(tt *testing.T) { req := tdhttp.NewRequest("GET", "/path", nil, &http.Cookie{Name: "cook1", Value: "val1"}, http.Cookie{Name: "cook2", Value: "val2"}, + []*http.Cookie{ + {Name: "cook3", Value: "val3"}, + {Name: "cook4", Value: "val4"}, + }, ) - t.Cmp(req.Header, http.Header{"Cookie": []string{"cook1=val1; cook2=val2"}}) + t.Cmp(req.Header, http.Header{ + "Cookie": {"cook1=val1; cook2=val2; cook3=val3; cook4=val4"}, + }) }) t.Run("NewRequest header flattened", func(t *td.T) { @@ -135,7 +141,7 @@ func TestNewRequest(tt *testing.T) { t.Run("NewRequest panics", func(t *td.T) { t.CmpPanic( func() { tdhttp.NewRequest("GET", "/path", nil, "H", "V", true) }, - td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[2])")) + td.HasPrefix("headersQueryParams... can only contains string, http.Header, ([]*|*|)http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[2])")) t.CmpPanic( func() { tdhttp.NewRequest("GET", "/path", nil, "H1", true) }, @@ -143,39 +149,39 @@ func TestNewRequest(tt *testing.T) { t.CmpPanic( func() { tdhttp.Get("/path", true) }, - td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) + td.HasPrefix("headersQueryParams... can only contains string, http.Header, ([]*|*|)http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.Head("/path", true) }, - td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) + td.HasPrefix("headersQueryParams... can only contains string, http.Header, ([]*|*|)http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.Options("/path", nil, true) }, - td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) + td.HasPrefix("headersQueryParams... can only contains string, http.Header, ([]*|*|)http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.Post("/path", nil, true) }, - td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) + td.HasPrefix("headersQueryParams... can only contains string, http.Header, ([]*|*|)http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.PostForm("/path", nil, true) }, - td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) + td.HasPrefix("headersQueryParams... can only contains string, http.Header, ([]*|*|)http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.PostMultipartFormData("/path", &tdhttp.MultipartBody{}, true) }, - td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) + td.HasPrefix("headersQueryParams... can only contains string, http.Header, ([]*|*|)http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.Patch("/path", nil, true) }, - td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) + td.HasPrefix("headersQueryParams... can only contains string, http.Header, ([]*|*|)http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.Put("/path", nil, true) }, - td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) + td.HasPrefix("headersQueryParams... can only contains string, http.Header, ([]*|*|)http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) t.CmpPanic( func() { tdhttp.Delete("/path", nil, true) }, - td.HasPrefix("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) + td.HasPrefix("headersQueryParams... can only contains string, http.Header, ([]*|*|)http.Cookie, url.Values and tdhttp.Q, not bool (@ headersQueryParams[0])")) // Bad target t.CmpPanic( diff --git a/helpers/tdhttp/test_api.go b/helpers/tdhttp/test_api.go index 40f608d2..5d697520 100644 --- a/helpers/tdhttp/test_api.go +++ b/helpers/tdhttp/test_api.go @@ -13,6 +13,7 @@ import ( "io" "net/http" "net/http/httptest" + "net/url" "reflect" "runtime" "strings" @@ -52,6 +53,10 @@ type TestAPI struct { // autoDumpResponse dumps the received response when a test fails. autoDumpResponse bool responseDumped bool + + defaultHeader http.Header + defaultQParams url.Values + defaultCookies []*http.Cookie } // NewTestAPI creates a [TestAPI] that can be used to test routes of the @@ -85,7 +90,9 @@ func NewTestAPI(tb testing.TB, handler http.Handler) *TestAPI { // With creates a new [*TestAPI] instance copied from t, but resetting // the [testing.TB] instance the tests are based on to tb. The // returned instance is independent from t, sharing only the same -// handler. +// handler. The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are also copied. // // It is typically used when the [TestAPI] instance is “reused” in // sub-tests, as in: @@ -109,11 +116,13 @@ func NewTestAPI(tb testing.TB, handler http.Handler) *TestAPI { // // See [TestAPI.Run] for another way to handle subtests. func (ta *TestAPI) With(tb testing.TB) *TestAPI { - return &TestAPI{ + nta := &TestAPI{ t: td.NewT(tb), handler: ta.handler, autoDumpResponse: ta.autoDumpResponse, } + return nta.DefaultRequestParams( + ta.defaultHeader, ta.defaultQParams, ta.defaultCookies) } // T returns the internal instance of [*td.T]. @@ -140,6 +149,72 @@ func (ta *TestAPI) AutoDumpResponse(enable ...bool) *TestAPI { return ta } +// DefaultRequestParams allows to define header values, query params +// and cookies to automatically set in each future requests sent by +// [TestAPI.Request], [TestAPI.Get], [TestAPI.Head], +// [TestAPI.Options], [TestAPI.Post], [TestAPI.PostForm], +// [TestAPI.PostMultipartFormData], [TestAPI.Put], [TestAPI.Path], +// [TestAPI.Delete] and all derived JSON and XML methods. +// +// See [NewRequest] for all possible formats accepted in headersQueryParams. +// +// The passed headersQueryParams resets existing default params, so +// calling +// +// ta.DefaultRequestParams() +// +// discards any previously set default params. +// +// See [TestAPI.AddDefaultRequestParams] to add new default params +// instead of entirely replacing them. +func (ta *TestAPI) DefaultRequestParams(headersQueryParams ...any) *TestAPI { + ta.t.Helper() + header, qp, cookies, err := requestParams(headersQueryParams) + if err != nil { + ta.t.Fatal(err) + } + ta.defaultHeader = header + ta.defaultQParams = qp + ta.defaultCookies = cookies + return ta +} + +// AddDefaultRequestParams allows to define header values, query +// params and cookies to automatically set in each future requests +// sent by [TestAPI.Request], [TestAPI.Get], [TestAPI.Head], +// [TestAPI.Options], [TestAPI.Post], [TestAPI.PostForm], +// [TestAPI.PostMultipartFormData], [TestAPI.Put], [TestAPI.Path], +// [TestAPI.Delete] and all derived JSON and XML methods. +// +// See [NewRequest] for all possible formats accepted in headersQueryParams. +// +// The passed headersQueryParams are merged with already existing +// default params, set by a previous call of this method or +// of [TestAPI.DefaultRequestParams], replacing the common ones. +// +// See [TestAPI.DefaultRequestParams] to entirely replace default +// params instead of modifying them. +func (ta *TestAPI) AddDefaultRequestParams(headersQueryParams ...any) *TestAPI { + ta.t.Helper() + header, qp, cookies, err := requestParams(headersQueryParams) + if err != nil { + ta.t.Fatal(err) + } + ta.defaultHeader = mergeHeader(header, ta.defaultHeader) + ta.defaultQParams, _ = mergeQParams(qp, ta.defaultQParams) +default_cookie: + for _, dflt := range ta.defaultCookies { + for _, c := range cookies { + if dflt.Name == c.Name { + continue default_cookie + } + } + cookies = append(cookies, dflt) + } + ta.defaultCookies = cookies + return ta +} + // Name allows to name the series of tests that follow. This name is // used as a prefix for all following tests, in case of failure to // qualify each test. If len(args) > 1 and the first item of args is @@ -153,11 +228,54 @@ func (ta *TestAPI) Name(args ...any) *TestAPI { return ta } +func mergeHeader(cur, dflt http.Header) http.Header { + if cur == nil && len(dflt) > 0 { + cur = make(http.Header, len(dflt)) + } + for k, v := range dflt { + if _, exists := cur[k]; !exists { + cur[k] = append([]string(nil), v...) + } + } + return cur +} + +func mergeQParams(cur, dflt url.Values) (url.Values, bool) { + if cur == nil && len(dflt) > 0 { + cur = make(url.Values, len(dflt)) + } + altered := false + for k, v := range dflt { + if _, exists := cur[k]; !exists { + cur[k] = append([]string(nil), v...) + altered = true + } + } + return cur, altered +} + // Request sends a new HTTP request to the tested API. Any Cmp* or // [TestAPI.NoBody] methods can now be called. // +// It merges header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] only if each of them does not +// already exist in req. +// // Note that [TestAPI.Failed] status is reset just after this call. func (ta *TestAPI) Request(req *http.Request) *TestAPI { + req.Header = mergeHeader(req.Header, ta.defaultHeader) + if len(ta.defaultQParams) > 0 && req.URL != nil { + qv, altered := mergeQParams(req.URL.Query(), ta.defaultQParams) + if altered { + req.URL.RawQuery = qv.Encode() + } + } + for _, c := range ta.defaultCookies { + if _, err := req.Cookie(c.Name); err != nil { + req.AddCookie(c) + } + } + ta.response = httptest.NewRecorder() ta.failed = 0 @@ -196,6 +314,11 @@ func (ta *TestAPI) Failed() bool { // Get sends a HTTP GET to the tested API. Any Cmp* or [TestAPI.NoBody] methods // can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. @@ -208,8 +331,13 @@ func (ta *TestAPI) Get(target string, headersQueryParams ...any) *TestAPI { return ta.Request(req) } -// Head sends a HTTP HEAD to the tested API. Any Cmp* or [TestAPI.NoBody] methods -// can now be called. +// Head sends a HTTP HEAD to the tested API. Any Cmp* or +// [TestAPI.NoBody] methods can now be called. +// +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. // // Note that [TestAPI.Failed] status is reset just after this call. // @@ -226,6 +354,11 @@ func (ta *TestAPI) Head(target string, headersQueryParams ...any) *TestAPI { // Options sends a HTTP OPTIONS to the tested API. Any Cmp* or // [TestAPI.NoBody] methods can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. @@ -241,6 +374,11 @@ func (ta *TestAPI) Options(target string, body io.Reader, headersQueryParams ... // Post sends a HTTP POST to the tested API. Any Cmp* or // [TestAPI.NoBody] methods can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. @@ -258,6 +396,11 @@ func (ta *TestAPI) Post(target string, body io.Reader, headersQueryParams ...any // automatically set to "application/x-www-form-urlencoded". Any Cmp* // or [TestAPI.NoBody] methods can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. @@ -277,6 +420,11 @@ func (ta *TestAPI) PostForm(target string, data URLValuesEncoder, headersQueryPa // data.Boundary (defaults to "go-testdeep-42"). Any Cmp* or // [TestAPI.NoBody] methods can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // ta.PostMultipartFormData("/data", @@ -304,6 +452,11 @@ func (ta *TestAPI) PostMultipartFormData(target string, data *MultipartBody, hea // Put sends a HTTP PUT to the tested API. Any Cmp* or [TestAPI.NoBody] methods // can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. @@ -316,8 +469,13 @@ func (ta *TestAPI) Put(target string, body io.Reader, headersQueryParams ...any) return ta.Request(req) } -// Patch sends a HTTP PATCH to the tested API. Any Cmp* or [TestAPI.NoBody] methods -// can now be called. +// Patch sends a HTTP PATCH to the tested API. Any Cmp* or +// [TestAPI.NoBody] methods can now be called. +// +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. // // Note that [TestAPI.Failed] status is reset just after this call. // @@ -331,8 +489,13 @@ func (ta *TestAPI) Patch(target string, body io.Reader, headersQueryParams ...an return ta.Request(req) } -// Delete sends a HTTP DELETE to the tested API. Any Cmp* or [TestAPI.NoBody] methods -// can now be called. +// Delete sends a HTTP DELETE to the tested API. Any Cmp* or +// [TestAPI.NoBody] methods can now be called. +// +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. // // Note that [TestAPI.Failed] status is reset just after this call. // @@ -350,6 +513,11 @@ func (ta *TestAPI) Delete(target string, body io.Reader, headersQueryParams ...a // JSON. "Content-Type" header is automatically set to // "application/json". Any Cmp* or [TestAPI.NoBody] methods can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. @@ -366,6 +534,11 @@ func (ta *TestAPI) NewJSONRequest(method, target string, body any, headersQueryP // JSON. "Content-Type" header is automatically set to // "application/json". Any Cmp* or [TestAPI.NoBody] methods can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. @@ -382,6 +555,11 @@ func (ta *TestAPI) PostJSON(target string, body any, headersQueryParams ...any) // JSON. "Content-Type" header is automatically set to // "application/json". Any Cmp* or [TestAPI.NoBody] methods can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. @@ -398,6 +576,11 @@ func (ta *TestAPI) PutJSON(target string, body any, headersQueryParams ...any) * // JSON. "Content-Type" header is automatically set to // "application/json". Any Cmp* or [TestAPI.NoBody] methods can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. @@ -414,6 +597,11 @@ func (ta *TestAPI) PatchJSON(target string, body any, headersQueryParams ...any) // JSON. "Content-Type" header is automatically set to // "application/json". Any Cmp* or [TestAPI.NoBody] methods can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. @@ -430,6 +618,11 @@ func (ta *TestAPI) DeleteJSON(target string, body any, headersQueryParams ...any // XML. "Content-Type" header is automatically set to // "application/xml". Any Cmp* or [TestAPI.NoBody] methods can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. @@ -446,6 +639,11 @@ func (ta *TestAPI) NewXMLRequest(method, target string, body any, headersQueryPa // XML. "Content-Type" header is automatically set to // "application/xml". Any Cmp* or [TestAPI.NoBody] methods can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. @@ -462,6 +660,11 @@ func (ta *TestAPI) PostXML(target string, body any, headersQueryParams ...any) * // XML. "Content-Type" header is automatically set to // "application/xml". Any Cmp* or [TestAPI.NoBody] methods can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. @@ -478,6 +681,11 @@ func (ta *TestAPI) PutXML(target string, body any, headersQueryParams ...any) *T // XML. "Content-Type" header is automatically set to // "application/xml". Any Cmp* or [TestAPI.NoBody] methods can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. @@ -494,6 +702,11 @@ func (ta *TestAPI) PatchXML(target string, body any, headersQueryParams ...any) // XML. "Content-Type" header is automatically set to // "application/xml". Any Cmp* or [TestAPI.NoBody] methods can now be called. // +// The header values, query params and cookies defined using +// [TestAPI.DefaultRequestParams] or [TestAPI.AddDefaultRequestParams] +// are merged only if each of them does not already exist in +// headersQueryParams. +// // Note that [TestAPI.Failed] status is reset just after this call. // // See [NewRequest] for all possible formats accepted in headersQueryParams. diff --git a/helpers/tdhttp/test_api_test.go b/helpers/tdhttp/test_api_test.go index bd772dfd..a106b625 100644 --- a/helpers/tdhttp/test_api_test.go +++ b/helpers/tdhttp/test_api_test.go @@ -76,6 +76,20 @@ func server() *http.ServeMux { io.Copy(w, req.Body) //nolint: errcheck }) + mux.HandleFunc("/hq/json", func(w http.ResponseWriter, req *http.Request) { + if req.Method == "HEAD" { + w.WriteHeader(http.StatusOK) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + m := map[string]any{ + "header": req.Header, + "query_params": req.URL.Query(), + } + json.NewEncoder(w).Encode(m) //nolint: errcheck + }) + mux.HandleFunc("/any/xml", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("X-TestDeep-Method", req.Method) if req.Method == "HEAD" { @@ -1070,7 +1084,7 @@ bingo%CR })) td.Cmp(t, mockT.LogBuf(), - td.Contains("headersQueryParams... can only contains string, http.Header, http.Cookie, url.Values and tdhttp.Q, not bool"), + td.Contains("headersQueryParams... can only contains string, http.Header, ([]*|*|)http.Cookie, url.Values and tdhttp.Q, not bool"), ) } @@ -1098,6 +1112,69 @@ bingo%CR checkFatal(func() { ta.PatchXML("/path", nil, true) }) checkFatal(func() { ta.DeleteXML("/path", nil, true) }) }) + + t.Run("Request params", func(t *testing.T) { + ta := tdhttp.NewTestAPI(t, mux) + + ta.Get("/hq/json", + "X-Test", "pipo", + tdhttp.Q{"a": "b"}, + http.Cookie{Name: "cook1", Value: "val1"}). + CmpJSONBody(td.JSON(`{ + "header": SuperMapOf({ + "X-Test": ["pipo"] + }), + "query_params": { + "a": ["b"] + } + }`)) + + ta.DefaultRequestParams( + "X-Zip", "test", + tdhttp.Q{"x": "y"}, + http.Cookie{Name: "cook9", Value: "val9"}) + + ta.Get("/hq/json"). + CmpJSONBody(td.JSON(`{ + "header": SuperMapOf({ + "X-Zip": ["test"], + "Cookie": ["cook9=val9"] + }), + "query_params": { + "x": ["y"] + } + }`)) + + ta.Get("/hq/json", + "X-Test", "pipo", + tdhttp.Q{"a": "b"}, + http.Cookie{Name: "cook1", Value: "val1"}). + CmpJSONBody(td.JSON(`{ + "header": SuperMapOf({ + "X-Test": ["pipo"], + "X-Zip": ["test"], + "Cookie": ["cook1=val1; cook9=val9"] + }), + "query_params": { + "x": ["y"], + "a": ["b"] + } + }`)) + + ta.Get("/hq/json", + "X-Zip", "override", + tdhttp.Q{"x": "override"}, + http.Cookie{Name: "cook9", Value: "override"}). + CmpJSONBody(td.JSON(`{ + "header": SuperMapOf({ + "X-Zip": ["override"], + "Cookie": ["cook9=override"] + }), + "query_params": { + "x": ["override"] + } + }`)) + }) } func TestWith(t *testing.T) { @@ -1130,6 +1207,112 @@ func TestWith(t *testing.T) { td.CmpContains(t, nt.LogBuf(), "X-Testdeep-Method: HEAD") // Header dumped } +func TestDefaultRequestParams(t *testing.T) { + mux := server() + + ta := tdhttp.NewTestAPI(tdutil.NewT("test1"), mux) + + td.Cmp(t, ta, td.Struct(nil, td.StructFields{ + "defaultHeader": nil, + "defaultQParams": nil, + "defaultCookies": nil, + })) + + ta.DefaultRequestParams("X-Test", "pipo") + td.Cmp(t, ta, td.Struct(nil, td.StructFields{ + "defaultHeader": http.Header{"X-Test": {"pipo"}}, + "defaultQParams": nil, + "defaultCookies": nil, + })) + + ta.DefaultRequestParams(tdhttp.Q{"a": "zip"}) + td.Cmp(t, ta, td.Struct(nil, td.StructFields{ + "defaultHeader": nil, + "defaultQParams": url.Values{"a": {"zip"}}, + "defaultCookies": nil, + })) + + ta.DefaultRequestParams(http.Cookie{Name: "cook1", Value: "val1"}) + td.Cmp(t, ta, td.Struct(nil, td.StructFields{ + "defaultHeader": nil, + "defaultQParams": nil, + "defaultCookies": []*http.Cookie{{Name: "cook1", Value: "val1"}}, + })) + + ta.DefaultRequestParams() + td.Cmp(t, ta, td.Struct(nil, td.StructFields{ + "defaultHeader": nil, + "defaultQParams": nil, + "defaultCookies": nil, + })) + + ta.DefaultRequestParams( + "X-Test", "pipo", + tdhttp.Q{"a": "zip"}, + http.Cookie{Name: "cook1", Value: "val1"}) + td.Cmp(t, ta, td.Struct(nil, td.StructFields{ + "defaultHeader": http.Header{"X-Test": {"pipo"}}, + "defaultQParams": url.Values{"a": {"zip"}}, + "defaultCookies": []*http.Cookie{{Name: "cook1", Value: "val1"}}, + })) + + ta.AddDefaultRequestParams() + td.Cmp(t, ta, td.Struct(nil, td.StructFields{ + "defaultHeader": http.Header{"X-Test": {"pipo"}}, + "defaultQParams": url.Values{"a": {"zip"}}, + "defaultCookies": []*http.Cookie{{Name: "cook1", Value: "val1"}}, + })) + + ta.AddDefaultRequestParams( + "X-Zip", "OK", + tdhttp.Q{"b": "kiss"}, + http.Cookie{Name: "cook2", Value: "val2"}) + td.Cmp(t, ta, td.Struct(nil, td.StructFields{ + "defaultHeader": http.Header{"X-Test": {"pipo"}, "X-Zip": {"OK"}}, + "defaultQParams": url.Values{"a": {"zip"}, "b": {"kiss"}}, + "defaultCookies": []*http.Cookie{ + {Name: "cook2", Value: "val2"}, + {Name: "cook1", Value: "val1"}, + }, + })) + + ta.AddDefaultRequestParams( + "X-Test", "bingo", "X-Zip", "OK", + tdhttp.Q{"a": "pizza", "b": "kiss"}, + http.Cookie{Name: "cook1", Value: "VAL1"}, + http.Cookie{Name: "cook2", Value: "val2"}) + td.Cmp(t, ta, td.Struct(nil, td.StructFields{ + "defaultHeader": http.Header{"X-Test": {"bingo"}, "X-Zip": {"OK"}}, + "defaultQParams": url.Values{"a": {"pizza"}, "b": {"kiss"}}, + "defaultCookies": []*http.Cookie{ + {Name: "cook1", Value: "VAL1"}, + {Name: "cook2", Value: "val2"}, + }, + })) + + t.Run("DefaultRequestParams fatal", func(t *testing.T) { + mockT := tdutil.NewT("test") + td.CmpTrue(t, mockT.CatchFailNow(func() { + ta = tdhttp.NewTestAPI(mockT, mux) + ta.DefaultRequestParams(123) + })) + td.Cmp(t, + mockT.LogBuf(), + td.Contains("headersQueryParams... can only contains")) + }) + + t.Run("AddDefaultRequestParams fatal", func(t *testing.T) { + mockT := tdutil.NewT("test") + td.CmpTrue(t, mockT.CatchFailNow(func() { + ta = tdhttp.NewTestAPI(mockT, mux) + ta.AddDefaultRequestParams(123) + })) + td.Cmp(t, + mockT.LogBuf(), + td.Contains("headersQueryParams... can only contains")) + }) +} + func TestOr(t *testing.T) { mux := server()