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()) } }