Files
shop-crm-agent/internal/client/client_test.go
T

157 lines
6.0 KiB
Go
Raw Normal View History

package client
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
api "code.zhecent.com/open/shop-crm-agent/internal/api"
"google.golang.org/protobuf/proto"
)
func TestClientSendsBearerProtobufAndChecksBusinessHeader(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path != apiPath+"/projects/get" {
t.Errorf("path = %s", request.URL.Path)
}
if request.Header.Get("Authorization") != "Bearer test-token" {
t.Errorf("authorization = %q", request.Header.Get("Authorization"))
}
if request.Header.Get("Content-Type") != "application/x-protobuf" {
t.Errorf("content type = %q", request.Header.Get("Content-Type"))
}
data, _ := proto.Marshal(&api.ProjectResponse{Header: &api.ResponseHeader{Code: 200, Message: "success"}, Project: &api.Project{Id: 9}})
writer.Header().Set("Content-Type", "application/x-protobuf")
_, _ = writer.Write(data)
}))
defer server.Close()
client, err := New(server.URL, "test-token", server.Client())
if err != nil {
t.Fatalf("new client: %v", err)
}
response, err := client.ProjectGet(context.Background(), &api.ProjectGetRequest{ProjectId: 9})
if err != nil {
t.Fatalf("project get: %v", err)
}
if response.Project.GetId() != 9 {
t.Fatalf("project id = %d", response.Project.GetId())
}
}
func TestClientReportsHTTPProtocolAndSizeFailures(t *testing.T) {
t.Run("http error preserves protobuf header", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusUnauthorized)
data, _ := proto.Marshal(&api.ProjectResponse{Header: &api.ResponseHeader{Code: 401, Message: "unauthorized"}})
_, _ = writer.Write(data)
}))
defer server.Close()
client, _ := New(server.URL, "token", server.Client())
response, err := client.ProjectGet(context.Background(), &api.ProjectGetRequest{})
if err == nil || !strings.Contains(err.Error(), "HTTP 401: unauthorized") {
t.Fatalf("error = %v", err)
}
if response.GetHeader().GetCode() != 401 {
t.Fatalf("header = %#v", response.GetHeader())
}
})
t.Run("malformed protobuf", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
_, _ = writer.Write([]byte{0xff})
}))
defer server.Close()
client, _ := New(server.URL, "token", server.Client())
if _, err := client.ProjectGet(context.Background(), &api.ProjectGetRequest{}); err == nil || !strings.Contains(err.Error(), "解析 Protobuf 响应失败") {
t.Fatalf("error = %v", err)
}
})
t.Run("response size limit", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
_, _ = writer.Write(make([]byte, maxResponseSize+1))
}))
defer server.Close()
client, _ := New(server.URL, "token", server.Client())
if _, err := client.ProjectGet(context.Background(), &api.ProjectGetRequest{}); err == nil || !strings.Contains(err.Error(), "16 MiB") {
t.Fatalf("error = %v", err)
}
})
}
func TestClientHonorsHTTPClientTimeout(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
time.Sleep(50 * time.Millisecond)
}))
defer server.Close()
httpClient := server.Client()
httpClient.Timeout = time.Millisecond
client, _ := New(server.URL, "token", httpClient)
_, err := client.ProjectGet(context.Background(), &api.ProjectGetRequest{})
if err == nil || !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("error = %v", err)
}
}
func TestClientRepresentativeOperationRoutes(t *testing.T) {
var paths []string
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
paths = append(paths, request.URL.Path)
var response proto.Message
switch request.URL.Path {
case apiPath + "/projects/create":
response = &api.ProjectResponse{Header: &api.ResponseHeader{Code: 200}}
case apiPath + "/quotations/delete/preview":
response = &api.DeletePreviewResponse{Header: &api.ResponseHeader{Code: 200}}
case apiPath + "/quotation/edit/command":
response = &api.QuotationEditCommandResponse{Header: &api.ResponseHeader{Code: 200}}
default:
t.Fatalf("unexpected path %q", request.URL.Path)
}
data, _ := proto.Marshal(response)
_, _ = writer.Write(data)
}))
defer server.Close()
client, _ := New(server.URL, "token", server.Client())
if _, err := client.ProjectCreate(context.Background(), &api.ProjectCreateRequest{}); err != nil {
t.Fatal(err)
}
if _, err := client.QuotationDeletePreview(context.Background(), &api.QuotationDeletePreviewRequest{}); err != nil {
t.Fatal(err)
}
if _, err := client.EditCommand(context.Background(), &api.QuotationEditCommandRequest{}); err != nil {
t.Fatal(err)
}
want := []string{apiPath + "/projects/create", apiPath + "/quotations/delete/preview", apiPath + "/quotation/edit/command"}
if strings.Join(paths, "|") != strings.Join(want, "|") {
t.Fatalf("paths = %v", paths)
}
}
func TestClientRejectsMissingConfigurationAndBusinessError(t *testing.T) {
if _, err := New("", "token", nil); err == nil {
t.Fatal("empty base URL accepted")
}
if _, err := New("https://example.com", "", nil); err == nil {
t.Fatal("empty token accepted")
}
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
data, _ := proto.Marshal(&api.CommonResponse{Header: &api.ResponseHeader{Code: 400, Message: "bad request", RequestId: "request-123", Replayed: true}})
_, _ = writer.Write(data)
}))
defer server.Close()
client, _ := New(server.URL, "token", server.Client())
response, err := client.ProjectGet(context.Background(), &api.ProjectGetRequest{})
if err == nil {
t.Fatal("business error was accepted")
}
if response.GetHeader() == nil || !response.GetHeader().Replayed || response.GetHeader().RequestId != "request-123" {
t.Fatalf("business error response = %#v", response.GetHeader())
}
}