Files
micro-client-http/client_unary_call_test.go
pugnack 24801750a7
Some checks failed
coverage / build (push) Successful in 2m19s
test / test (push) Failing after 17m15s
integrate request builder into HTTP client for googleapis support (#157)
2025-09-23 13:30:15 +03:00

1285 lines
36 KiB
Go

package http_test
import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/require"
jsoncodec "go.unistack.org/micro-codec-json/v4"
"go.unistack.org/micro/v4/client"
"go.unistack.org/micro/v4/metadata"
"google.golang.org/protobuf/proto"
httpcli "go.unistack.org/micro-client-http/v4"
pb "go.unistack.org/micro-client-http/v4/builder/proto"
)
func TestClient_Call_Get(t *testing.T) {
type (
request = pb.Test_Client_Call_Request
response = pb.Test_Client_Call_Response
)
tests := []struct {
name string
method string
path string
req *request
options []client.CallOption
wantPath string
wantReqBody []byte
wantRsp *response
wantErr bool
}{
{
name: "GET request (query)",
method: http.MethodGet,
path: "/user/products",
req: &request{UserId: "123", OrderId: 456},
wantPath: "/user/products?order_id=456&user_id=123",
wantReqBody: []byte{},
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "GET request (path)",
method: http.MethodGet,
path: "/user/{user_id}/order/{order_id}/products",
req: &request{UserId: "123", OrderId: 456},
wantPath: "/user/123/order/456/products",
wantReqBody: []byte{},
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "GET request (path + query)",
method: http.MethodGet,
path: "/user/{user_id}/products",
req: &request{UserId: "123", OrderId: 456},
wantPath: "/user/123/products?order_id=456",
wantReqBody: []byte{},
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "GET request (zero-value query)",
method: http.MethodGet,
path: "/user/products",
req: &request{},
wantPath: "/user/products",
wantReqBody: []byte{},
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "GET request (zero-value path)",
method: http.MethodGet,
path: "/user/{user_id}/products",
req: &request{OrderId: 456},
wantRsp: nil,
wantErr: true,
},
{
name: "GET request (with body)",
method: http.MethodGet,
path: "/user/products",
req: &request{UserId: "123", OrderId: 456},
options: []client.CallOption{httpcli.Body("*")},
wantRsp: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, tt.method, r.Method)
require.Equal(t, tt.wantPath, r.URL.RequestURI())
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
require.Equal(t, "Bearer token", r.Header.Get("Authorization"))
require.Equal(t, "My-Header-Value", r.Header.Get("My-Header"))
buf, err := io.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()
require.Equal(t, tt.wantReqBody, buf)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("My-Header", "My-Header-Value")
w.WriteHeader(http.StatusOK)
if tt.wantRsp != nil {
c := jsoncodec.NewCodec()
buf, err = c.Marshal(tt.wantRsp)
require.NoError(t, err)
_, err = w.Write(buf)
require.NoError(t, err)
}
}))
defer server.Close()
httpClient := httpcli.NewClient(
client.Codec("application/json", jsoncodec.NewCodec()),
)
var (
ctx = metadata.NewOutgoingContext(
context.Background(),
metadata.Pairs("Authorization", "Bearer token", "My-Header", "My-Header-Value"),
)
rsp = &response{}
respMetadata = metadata.Metadata{}
)
opts := []client.CallOption{
client.WithAddress(server.URL),
client.WithResponseMetadata(&respMetadata),
httpcli.Method(tt.method),
httpcli.Path(tt.path),
}
if len(tt.options) > 0 {
opts = append(opts, tt.options...)
}
err := httpClient.Call(
ctx,
httpClient.NewRequest("test.service", "Test.Call", tt.req),
rsp,
opts...,
)
if tt.wantErr {
require.Error(t, err)
require.Empty(t, rsp)
} else {
require.NoError(t, err)
require.True(t, proto.Equal(tt.wantRsp, rsp))
require.Equal(t, "application/json", respMetadata.GetJoined("Content-Type"))
require.Equal(t, "My-Header-Value", respMetadata.GetJoined("My-Header"))
}
})
}
}
func TestClient_Call_Head(t *testing.T) {
type (
request = pb.Test_Client_Call_Request
response = pb.Test_Client_Call_Response
)
tests := []struct {
name string
method string
path string
req *request
options []client.CallOption
wantPath string
wantReqBody []byte
wantRsp *response
wantErr bool
}{
{
name: "HEAD request (query)",
method: http.MethodHead,
path: "/user/products",
req: &request{UserId: "123", OrderId: 456},
wantPath: "/user/products?order_id=456&user_id=123",
wantReqBody: []byte{},
wantRsp: &response{},
wantErr: false,
},
{
name: "HEAD request (path)",
method: http.MethodHead,
path: "/user/{user_id}/order/{order_id}/products",
req: &request{UserId: "123", OrderId: 456},
wantPath: "/user/123/order/456/products",
wantReqBody: []byte{},
wantRsp: &response{},
wantErr: false,
},
{
name: "HEAD request (path + query)",
method: http.MethodHead,
path: "/user/{user_id}/products",
req: &request{UserId: "123", OrderId: 456},
wantPath: "/user/123/products?order_id=456",
wantReqBody: []byte{},
wantRsp: &response{},
wantErr: false,
},
{
name: "HEAD request (zero-value query)",
method: http.MethodHead,
path: "/user/products",
req: &request{},
wantPath: "/user/products",
wantReqBody: []byte{},
wantRsp: &response{},
wantErr: false,
},
{
name: "HEAD request (zero-value path)",
method: http.MethodHead,
path: "/user/{user_id}/products",
req: &request{OrderId: 456},
wantRsp: nil,
wantErr: true,
},
{
name: "HEAD request (with body)",
method: http.MethodHead,
path: "/user/products",
req: &request{UserId: "123", OrderId: 456},
options: []client.CallOption{httpcli.Body("*")},
wantRsp: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, tt.method, r.Method)
require.Equal(t, tt.wantPath, r.URL.RequestURI())
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
require.Equal(t, "Bearer token", r.Header.Get("Authorization"))
require.Equal(t, "My-Header-Value", r.Header.Get("My-Header"))
buf, err := io.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()
require.Equal(t, tt.wantReqBody, buf)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("My-Header", "My-Header-Value")
w.WriteHeader(http.StatusOK)
// used to verify that the HTTP client skips the response body for HEAD method
c := jsoncodec.NewCodec()
buf, err = c.Marshal(map[string]any{"id": "product-id", "name": "product-name"})
require.NoError(t, err)
_, err = w.Write(buf)
require.NoError(t, err)
}))
defer server.Close()
httpClient := httpcli.NewClient(
client.Codec("application/json", jsoncodec.NewCodec()),
)
var (
ctx = metadata.NewOutgoingContext(
context.Background(),
metadata.Pairs("Authorization", "Bearer token", "My-Header", "My-Header-Value"),
)
rsp = &response{}
respMetadata = metadata.Metadata{}
)
opts := []client.CallOption{
client.WithAddress(server.URL),
client.WithResponseMetadata(&respMetadata),
httpcli.Method(tt.method),
httpcli.Path(tt.path),
}
if len(tt.options) > 0 {
opts = append(opts, tt.options...)
}
err := httpClient.Call(
ctx,
httpClient.NewRequest("test.service", "Test.Call", tt.req),
rsp,
opts...,
)
if tt.wantErr {
require.Error(t, err)
require.Empty(t, rsp)
} else {
require.NoError(t, err)
require.True(t, proto.Equal(tt.wantRsp, rsp))
require.Equal(t, "application/json", respMetadata.GetJoined("Content-Type"))
require.Equal(t, "My-Header-Value", respMetadata.GetJoined("My-Header"))
}
})
}
}
func TestClient_Call_Post(t *testing.T) {
type (
request = pb.Test_Client_Call_Request
response = pb.Test_Client_Call_Response
)
tests := []struct {
name string
method string
path string
req *request
options []client.CallOption
wantPath string
wantReqBody []byte
wantRsp *response
wantErr bool
}{
{
name: "POST request (query)",
method: http.MethodPost,
path: "/user/products",
req: &request{UserId: "123", OrderId: 456},
wantPath: "/user/products?order_id=456&user_id=123",
wantReqBody: []byte{},
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "POST request (path)",
method: http.MethodPost,
path: "/user/{user_id}/order/{order_id}/products",
req: &request{UserId: "123", OrderId: 456},
wantPath: "/user/123/order/456/products",
wantReqBody: []byte{},
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "POST request (body)",
method: http.MethodPost,
path: "/user/products",
req: &request{UserId: "123", OrderId: 456},
options: []client.CallOption{httpcli.Body("*")},
wantPath: "/user/products",
wantReqBody: []byte(`{"userId":"123","orderId":456}`),
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "POST request (path + query)",
method: http.MethodPost,
path: "/user/{user_id}/products",
req: &request{UserId: "123", OrderId: 456},
wantPath: "/user/123/products?order_id=456",
wantReqBody: []byte{},
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "POST request (path + body)",
method: http.MethodPost,
path: "/user/{user_id}/products",
req: &request{UserId: "123", OrderId: 456},
options: []client.CallOption{httpcli.Body("*")},
wantPath: "/user/123/products",
wantReqBody: []byte(`{"orderId":456}`),
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "POST request (query + body)",
method: http.MethodPost,
path: "/user/products",
req: &request{UserId: "123", OrderId: 456},
options: []client.CallOption{httpcli.Body("order_id")},
wantPath: "/user/products?user_id=123",
wantReqBody: []byte(`{"orderId":456}`),
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "POST request (zero-value query)",
method: http.MethodPost,
path: "/user/products",
req: &request{},
wantPath: "/user/products",
wantReqBody: []byte{},
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "POST request (zero-value body)",
method: http.MethodPost,
path: "/user/products",
req: &request{},
options: []client.CallOption{httpcli.Body("*")},
wantPath: "/user/products",
wantReqBody: []byte{},
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "POST request (zero-value path)",
method: http.MethodPost,
path: "/user/{user_id}/products",
req: &request{OrderId: 456},
wantRsp: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, tt.method, r.Method)
require.Equal(t, tt.wantPath, r.URL.RequestURI())
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
require.Equal(t, "Bearer token", r.Header.Get("Authorization"))
require.Equal(t, "My-Header-Value", r.Header.Get("My-Header"))
buf, err := io.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()
require.Equal(t, tt.wantReqBody, buf)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("My-Header", "My-Header-Value")
w.WriteHeader(http.StatusOK)
if tt.wantRsp != nil {
c := jsoncodec.NewCodec()
buf, err = c.Marshal(tt.wantRsp)
require.NoError(t, err)
_, err = w.Write(buf)
require.NoError(t, err)
}
}))
defer server.Close()
httpClient := httpcli.NewClient(
client.Codec("application/json", jsoncodec.NewCodec()),
)
var (
ctx = metadata.NewOutgoingContext(
context.Background(),
metadata.Pairs("Authorization", "Bearer token", "My-Header", "My-Header-Value"),
)
rsp = &response{}
respMetadata = metadata.Metadata{}
)
opts := []client.CallOption{
client.WithAddress(server.URL),
client.WithResponseMetadata(&respMetadata),
httpcli.Method(tt.method),
httpcli.Path(tt.path),
}
if len(tt.options) > 0 {
opts = append(opts, tt.options...)
}
err := httpClient.Call(
ctx,
httpClient.NewRequest("test.service", "Test.Call", tt.req),
rsp,
opts...,
)
if tt.wantErr {
require.Error(t, err)
require.Empty(t, rsp)
} else {
require.NoError(t, err)
require.True(t, proto.Equal(tt.wantRsp, rsp))
require.Equal(t, "application/json", respMetadata.GetJoined("Content-Type"))
require.Equal(t, "My-Header-Value", respMetadata.GetJoined("My-Header"))
}
})
}
}
func TestClient_Call_Delete(t *testing.T) {
type (
request = pb.Test_Client_Call_Request
response = pb.Test_Client_Call_Response
)
tests := []struct {
name string
method string
path string
req *request
options []client.CallOption
wantPath string
wantReqBody []byte
wantRsp *response
wantErr bool
}{
{
name: "DELETE request (query)",
method: http.MethodDelete,
path: "/user/products",
req: &request{UserId: "123", OrderId: 456},
wantPath: "/user/products?order_id=456&user_id=123",
wantReqBody: []byte{},
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "DELETE request (path)",
method: http.MethodDelete,
path: "/user/{user_id}/order/{order_id}/products",
req: &request{UserId: "123", OrderId: 456},
wantPath: "/user/123/order/456/products",
wantReqBody: []byte{},
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "DELETE request (path + query)",
method: http.MethodDelete,
path: "/user/{user_id}/products",
req: &request{UserId: "123", OrderId: 456},
wantPath: "/user/123/products?order_id=456",
wantReqBody: []byte{},
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "DELETE request (zero-value query)",
method: http.MethodDelete,
path: "/user/products",
req: &request{},
wantPath: "/user/products",
wantReqBody: []byte{},
wantRsp: &response{Id: "product-id", Name: "product-name"},
wantErr: false,
},
{
name: "DELETE request (zero-value path)",
method: http.MethodDelete,
path: "/user/{user_id}/products",
req: &request{OrderId: 456},
wantRsp: nil,
wantErr: true,
},
{
name: "DELETE request (with body)",
method: http.MethodDelete,
path: "/user/products",
req: &request{UserId: "123", OrderId: 456},
options: []client.CallOption{httpcli.Body("*")},
wantRsp: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, tt.method, r.Method)
require.Equal(t, tt.wantPath, r.URL.RequestURI())
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
require.Equal(t, "Bearer token", r.Header.Get("Authorization"))
require.Equal(t, "My-Header-Value", r.Header.Get("My-Header"))
buf, err := io.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()
require.Equal(t, tt.wantReqBody, buf)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("My-Header", "My-Header-Value")
w.WriteHeader(http.StatusOK)
if tt.wantRsp != nil {
c := jsoncodec.NewCodec()
buf, err = c.Marshal(tt.wantRsp)
require.NoError(t, err)
_, err = w.Write(buf)
require.NoError(t, err)
}
}))
defer server.Close()
httpClient := httpcli.NewClient(
client.Codec("application/json", jsoncodec.NewCodec()),
)
var (
ctx = metadata.NewOutgoingContext(
context.Background(),
metadata.Pairs("Authorization", "Bearer token", "My-Header", "My-Header-Value"),
)
rsp = &response{}
respMetadata = metadata.Metadata{}
)
opts := []client.CallOption{
client.WithAddress(server.URL),
client.WithResponseMetadata(&respMetadata),
httpcli.Method(tt.method),
httpcli.Path(tt.path),
}
if len(tt.options) > 0 {
opts = append(opts, tt.options...)
}
err := httpClient.Call(
ctx,
httpClient.NewRequest("test.service", "Test.Call", tt.req),
rsp,
opts...,
)
if tt.wantErr {
require.Error(t, err)
require.Empty(t, rsp)
} else {
require.NoError(t, err)
require.True(t, proto.Equal(tt.wantRsp, rsp))
require.Equal(t, "application/json", respMetadata.GetJoined("Content-Type"))
require.Equal(t, "My-Header-Value", respMetadata.GetJoined("My-Header"))
}
})
}
}
func TestClient_Call_ErrorsMap(t *testing.T) {
type (
request = pb.Test_Client_Call_Request
response = pb.Test_Client_Call_Response
defaultError = pb.Test_Client_Call_DefaultError
specialError = pb.Test_Client_Call_SpecialError
)
tests := []struct {
name string
serverMock func() *httptest.Server
expectedErr error
}{
{
name: "default error",
serverMock: func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validate request
require.Equal(t, "POST", r.Method)
require.Equal(t, "/user/products", r.URL.RequestURI())
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
require.Equal(t, "Bearer token", r.Header.Get("Authorization"))
require.Equal(t, "My-Header-Value", r.Header.Get("My-Header"))
buf, err := io.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()
c := jsoncodec.NewCodec()
req := &request{}
err = c.Unmarshal(buf, req)
require.NoError(t, err)
require.True(t, proto.Equal(&request{UserId: "123", OrderId: 456}, req))
// Return response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("My-Header", "My-Header-Value")
w.WriteHeader(http.StatusBadRequest)
resp := map[string]interface{}{
"code": "default-error-code",
"msg": "default-error-message",
}
buf, err = c.Marshal(resp)
require.NoError(t, err)
_, err = w.Write(buf)
require.NoError(t, err)
}))
},
expectedErr: &defaultError{Code: "default-error-code", Msg: "default-error-message"},
},
{
name: "special error",
serverMock: func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validate request
require.Equal(t, "POST", r.Method)
require.Equal(t, "/user/products", r.URL.RequestURI())
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
require.Equal(t, "Bearer token", r.Header.Get("Authorization"))
require.Equal(t, "My-Header-Value", r.Header.Get("My-Header"))
buf, err := io.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()
c := jsoncodec.NewCodec()
req := &request{}
err = c.Unmarshal(buf, req)
require.NoError(t, err)
require.True(t, proto.Equal(&request{UserId: "123", OrderId: 456}, req))
// Return response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("My-Header", "My-Header-Value")
w.WriteHeader(http.StatusForbidden)
resp := map[string]interface{}{
"code": "special-error-code",
"msg": "special-error-message",
"warning": "special-error-warning",
}
buf, err = c.Marshal(resp)
require.NoError(t, err)
_, err = w.Write(buf)
require.NoError(t, err)
}))
},
expectedErr: &specialError{Code: "special-error-code", Msg: "special-error-message", Warning: "special-error-warning"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := tt.serverMock()
defer server.Close()
httpClient := httpcli.NewClient(
client.Codec("application/json", jsoncodec.NewCodec()),
)
var (
ctx = metadata.NewOutgoingContext(
context.Background(),
metadata.Pairs("Authorization", "Bearer token", "My-Header", "My-Header-Value"),
)
req = &request{UserId: "123", OrderId: 456}
rsp = &response{}
respMetadata = metadata.Metadata{}
)
opts := []client.CallOption{
client.WithAddress(server.URL),
client.WithResponseMetadata(&respMetadata),
httpcli.Method(http.MethodPost),
httpcli.Path("/user/products"),
httpcli.Body("*"),
httpcli.ErrorMap(map[string]any{
"default": &defaultError{},
"403": &specialError{},
}),
}
err := httpClient.Call(
ctx,
httpClient.NewRequest("test.service", "Test.Call", req),
rsp,
opts...,
)
require.Equal(t, tt.expectedErr.Error(), err.Error())
require.Empty(t, rsp)
require.Equal(t, "application/json", respMetadata.GetJoined("Content-Type"))
require.Equal(t, "My-Header-Value", respMetadata.GetJoined("My-Header"))
})
}
}
func TestClient_Call_HeadersAndCookies(t *testing.T) {
type (
request = pb.Test_Client_Call_Request
response = pb.Test_Client_Call_Response
)
tests := []struct {
name string
serverMock func() *httptest.Server
prepareMetadata func() metadata.Metadata
headerOption client.CallOption
cookieOption client.CallOption
expectedRsp *response
wantErr bool
}{
{
name: "with required headers",
serverMock: func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validate request
require.Equal(t, "POST", r.Method)
require.Equal(t, "/user/products", r.URL.RequestURI())
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
require.Equal(t, "Bearer token", r.Header.Get("Authorization"))
require.Equal(t, "My-Header-Value", r.Header.Get("My-Header"))
buf, err := io.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()
c := jsoncodec.NewCodec()
req := &request{}
err = c.Unmarshal(buf, req)
require.NoError(t, err)
require.True(t, proto.Equal(&request{UserId: "123", OrderId: 456}, req))
// Return response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("My-Header", "My-Header-Value")
w.WriteHeader(http.StatusOK)
resp := map[string]interface{}{
"id": "product-id",
"name": "product-name",
}
buf, err = c.Marshal(resp)
require.NoError(t, err)
_, err = w.Write(buf)
require.NoError(t, err)
}))
},
prepareMetadata: func() metadata.Metadata {
return metadata.Pairs("Authorization", "Bearer token", "My-Header", "My-Header-Value")
},
headerOption: httpcli.Header("Authorization", "true", "My-Header", "true"),
expectedRsp: &response{Id: "product-id", Name: "product-name"},
},
{
name: "without required headers",
serverMock: func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validate request
require.Equal(t, "POST", r.Method)
require.Equal(t, "/user/products", r.URL.RequestURI())
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
require.Equal(t, "Bearer token", r.Header.Get("Authorization"))
require.Equal(t, "My-Header-Value", r.Header.Get("My-Header"))
buf, err := io.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()
c := jsoncodec.NewCodec()
req := &request{}
err = c.Unmarshal(buf, req)
require.NoError(t, err)
require.True(t, proto.Equal(&request{UserId: "123", OrderId: 456}, req))
// Return response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("My-Header", "My-Header-Value")
w.WriteHeader(http.StatusOK)
resp := map[string]interface{}{
"id": "product-id",
"name": "product-name",
}
buf, err = c.Marshal(resp)
require.NoError(t, err)
_, err = w.Write(buf)
require.NoError(t, err)
}))
},
prepareMetadata: func() metadata.Metadata {
return metadata.Pairs("Authorization", "Bearer token")
},
headerOption: httpcli.Header("Authorization", "true", "My-Header", "true"),
wantErr: true,
},
{
name: "with required cookies",
serverMock: func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validate request
require.Equal(t, "POST", r.Method)
require.Equal(t, "/user/products", r.URL.RequestURI())
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
require.Equal(t, "session_id=abc123; theme=dark", r.Header.Get("Cookie"))
buf, err := io.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()
c := jsoncodec.NewCodec()
req := &request{}
err = c.Unmarshal(buf, req)
require.NoError(t, err)
require.True(t, proto.Equal(&request{UserId: "123", OrderId: 456}, req))
// Return response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("My-Header", "My-Header-Value")
w.WriteHeader(http.StatusOK)
resp := map[string]interface{}{
"id": "product-id",
"name": "product-name",
}
buf, err = c.Marshal(resp)
require.NoError(t, err)
_, err = w.Write(buf)
require.NoError(t, err)
}))
},
prepareMetadata: func() metadata.Metadata {
return metadata.Pairs("Cookie", "session_id=abc123; theme=dark")
},
cookieOption: httpcli.Cookie("session_id", "true", "theme", "true"),
expectedRsp: &response{Id: "product-id", Name: "product-name"},
},
{
name: "without required cookies",
serverMock: func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validate request
require.Equal(t, "POST", r.Method)
require.Equal(t, "/user/products", r.URL.RequestURI())
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
require.Equal(t, "session_id=abc123; theme=dark", r.Header.Get("Cookie"))
buf, err := io.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()
c := jsoncodec.NewCodec()
req := &request{}
err = c.Unmarshal(buf, req)
require.NoError(t, err)
require.True(t, proto.Equal(&request{UserId: "123", OrderId: 456}, req))
// Return response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("My-Header", "My-Header-Value")
w.WriteHeader(http.StatusOK)
resp := map[string]interface{}{
"id": "product-id",
"name": "product-name",
}
buf, err = c.Marshal(resp)
require.NoError(t, err)
_, err = w.Write(buf)
require.NoError(t, err)
}))
},
prepareMetadata: func() metadata.Metadata {
return metadata.Pairs("Cookie", "session_id=abc123")
},
cookieOption: httpcli.Cookie("session_id", "true", "theme", "true"),
wantErr: true,
},
{
name: "with headers and cookies",
serverMock: func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validate request
require.Equal(t, "POST", r.Method)
require.Equal(t, "/user/products", r.URL.RequestURI())
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
require.Equal(t, "Bearer token", r.Header.Get("Authorization"))
require.Equal(t, "My-Header-Value", r.Header.Get("My-Header"))
require.Equal(t, "session_id=abc123; theme=dark", r.Header.Get("Cookie"))
buf, err := io.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()
c := jsoncodec.NewCodec()
req := &request{}
err = c.Unmarshal(buf, req)
require.NoError(t, err)
require.True(t, proto.Equal(&request{UserId: "123", OrderId: 456}, req))
// Return response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("My-Header", "My-Header-Value")
w.WriteHeader(http.StatusOK)
resp := map[string]interface{}{
"id": "product-id",
"name": "product-name",
}
buf, err = c.Marshal(resp)
require.NoError(t, err)
_, err = w.Write(buf)
require.NoError(t, err)
}))
},
prepareMetadata: func() metadata.Metadata {
return metadata.Pairs(
"Authorization", "Bearer token",
"My-Header", "My-Header-Value",
"Cookie", "session_id=abc123; theme=dark",
)
},
headerOption: httpcli.Header("Authorization", "true", "My-Header", "true"),
cookieOption: httpcli.Cookie("session_id", "true", "theme", "true"),
expectedRsp: &response{Id: "product-id", Name: "product-name"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := tt.serverMock()
defer server.Close()
httpClient := httpcli.NewClient(
client.Codec("application/json", jsoncodec.NewCodec()),
)
var (
ctx = metadata.NewOutgoingContext(context.Background(), tt.prepareMetadata())
req = &request{UserId: "123", OrderId: 456}
rsp = &response{}
respMetadata = metadata.Metadata{}
)
opts := []client.CallOption{
client.WithAddress(server.URL),
client.WithResponseMetadata(&respMetadata),
httpcli.Method(http.MethodPost),
httpcli.Path("/user/products"),
httpcli.Body("*"),
}
if tt.headerOption != nil {
opts = append(opts, tt.headerOption)
}
if tt.cookieOption != nil {
opts = append(opts, tt.cookieOption)
}
err := httpClient.Call(
ctx,
httpClient.NewRequest("test.service", "Test.Call", req),
rsp,
opts...,
)
if tt.wantErr {
require.Error(t, err)
require.Empty(t, rsp)
} else {
require.NoError(t, err)
require.True(t, proto.Equal(tt.expectedRsp, rsp))
require.Equal(t, "application/json", respMetadata.GetJoined("Content-Type"))
require.Equal(t, "My-Header-Value", respMetadata.GetJoined("My-Header"))
}
})
}
}
func TestClient_Call_NoContent(t *testing.T) {
type (
request = pb.Test_Client_Call_Request
response = pb.Test_Client_Call_Response
)
serverMock := func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validate request
require.Equal(t, "POST", r.Method)
require.Equal(t, "/user/products", r.URL.RequestURI())
require.Equal(t, "application/json", r.Header.Get("Content-Type"))
require.Equal(t, "Bearer token", r.Header.Get("Authorization"))
require.Equal(t, "My-Header-Value", r.Header.Get("My-Header"))
buf, err := io.ReadAll(r.Body)
require.NoError(t, err)
defer r.Body.Close()
c := jsoncodec.NewCodec()
req := &request{}
err = c.Unmarshal(buf, req)
require.NoError(t, err)
require.True(t, proto.Equal(&request{UserId: "123", OrderId: 456}, req))
// Return response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("My-Header", "My-Header-Value")
w.WriteHeader(http.StatusNoContent)
}))
}
server := serverMock()
defer server.Close()
httpClient := httpcli.NewClient(
client.Codec("application/json", jsoncodec.NewCodec()),
)
var (
ctx = metadata.NewOutgoingContext(
context.Background(),
metadata.Pairs("Authorization", "Bearer token", "My-Header", "My-Header-Value"),
)
req = &request{UserId: "123", OrderId: 456}
rsp = &response{}
respMetadata = metadata.Metadata{}
)
opts := []client.CallOption{
client.WithAddress(server.URL),
client.WithResponseMetadata(&respMetadata),
httpcli.Method(http.MethodPost),
httpcli.Path("/user/products"),
httpcli.Body("*"),
}
err := httpClient.Call(
ctx,
httpClient.NewRequest("test.service", "Test.Call", req),
rsp,
opts...,
)
require.NoError(t, err)
require.Empty(t, rsp)
require.Equal(t, "application/json", respMetadata.GetJoined("Content-Type"))
require.Equal(t, "My-Header-Value", respMetadata.GetJoined("My-Header"))
}
func TestClient_Call_RequestTimeoutError(t *testing.T) {
type (
request = pb.Test_Client_Call_Request
response = pb.Test_Client_Call_Response
)
serverMock := func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Millisecond)
}))
}
server := serverMock()
defer server.Close()
httpClient := httpcli.NewClient(
client.Codec("application/json", jsoncodec.NewCodec()),
)
var (
ctx = context.Background()
req = &request{UserId: "123", OrderId: 456}
rsp = &response{}
)
opts := []client.CallOption{
client.WithAddress(server.URL),
client.WithRequestTimeout(time.Millisecond),
httpcli.Method(http.MethodPost),
httpcli.Path("/user/products"),
httpcli.Body("*"),
}
err := httpClient.Call(
ctx,
httpClient.NewRequest("test.service", "Test.Call", req),
rsp,
opts...,
)
require.Error(t, err)
}
func TestClient_Call_ContextDeadlineError(t *testing.T) {
type (
request = pb.Test_Client_Call_Request
response = pb.Test_Client_Call_Response
)
serverMock := func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Millisecond)
}))
}
server := serverMock()
defer server.Close()
httpClient := httpcli.NewClient(
client.Codec("application/json", jsoncodec.NewCodec()),
)
var (
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond))
req = &request{UserId: "123", OrderId: 456}
rsp = &response{}
)
defer cancel()
opts := []client.CallOption{
client.WithAddress(server.URL),
httpcli.Method(http.MethodPost),
httpcli.Path("/user/products"),
httpcli.Body("*"),
}
err := httpClient.Call(
ctx,
httpClient.NewRequest("test.service", "Test.Call", req),
rsp,
opts...,
)
require.Error(t, err)
}
func TestClient_Call_ContextCanceled(t *testing.T) {
type (
request = pb.Test_Client_Call_Request
response = pb.Test_Client_Call_Response
)
serverMock := func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Millisecond)
}))
}
server := serverMock()
defer server.Close()
httpClient := httpcli.NewClient(
client.Codec("application/json", jsoncodec.NewCodec()),
)
var (
ctx, cancel = context.WithCancel(context.Background())
req = &request{UserId: "123", OrderId: 456}
rsp = &response{}
)
cancel()
opts := []client.CallOption{
client.WithAddress(server.URL),
httpcli.Method(http.MethodPost),
httpcli.Path("/user/products"),
httpcli.Body("*"),
}
err := httpClient.Call(
ctx,
httpClient.NewRequest("test.service", "Test.Call", req),
rsp,
opts...,
)
require.Error(t, err)
}