diff --git a/v2/client/service_test.go b/v2/client/service_test.go index 85843f5..6e80a7d 100644 --- a/v2/client/service_test.go +++ b/v2/client/service_test.go @@ -1361,12 +1361,12 @@ var _ = Describe("ConnectionController", func() { mockServer *httptest.Server //mockToken string mockRequest InvokeConnectionRequest - mockResponse map[string]interface{} + //mockResponse map[string]interface{} ) BeforeEach(func() { //mockToken = "mock-valid-token" - mockResponse = map[string]interface{}{"key": "value"} + //mockResponse = map[string]interface{}{"key": "value"} mockRequest = InvokeConnectionRequest{ Headers: map[string]string{ "Content-Type": "application/json", @@ -1405,7 +1405,7 @@ var _ = Describe("ConnectionController", func() { service, err := client.Connection("failed") response, err := service.Invoke(ctx, mockRequest) Expect(err).To(BeNil()) - Expect(response.Data).To(Equal(mockResponse)) + Expect(response.Data).To(Equal(fmt.Sprintf("%v", `{"key": "value"}`))) }) }) Context("Handling query parameters", func() { diff --git a/v2/internal/validation/validations.go b/v2/internal/validation/validations.go index 5de8ac8..96269db 100644 --- a/v2/internal/validation/validations.go +++ b/v2/internal/validation/validations.go @@ -645,9 +645,12 @@ func ValidateInvokeConnectionRequest(request common.InvokeConnectionRequest) *sk } // Validate body if request.Body != nil { - if len(request.Body) == 0 { - logger.Error(fmt.Sprintf(logs.EMPTY_REQUEST_BODY, constants.REQUEST_INVOKE_CONNECTION)) - return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_REQUEST_BODY) + // Check if body is a map and if it's empty + if bodyMap, ok := request.Body.(map[string]interface{}); ok { + if len(bodyMap) == 0 { + logger.Error(fmt.Sprintf(logs.EMPTY_REQUEST_BODY, constants.REQUEST_INVOKE_CONNECTION)) + return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_REQUEST_BODY) + } } } if request.Method != "" { diff --git a/v2/internal/vault/controller/connection_controller.go b/v2/internal/vault/controller/connection_controller.go index 1f9c2d1..552b8f7 100644 --- a/v2/internal/vault/controller/connection_controller.go +++ b/v2/internal/vault/controller/connection_controller.go @@ -6,9 +6,9 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "mime/multipart" "net/http" + "net/url" "os" "reflect" "strconv" @@ -22,7 +22,10 @@ import ( "github.com/skyflowapi/skyflow-go/v2/utils/logger" logs "github.com/skyflowapi/skyflow-go/v2/utils/messages" - "github.com/hetiansu5/urlquery" +) + +const ( + formatValue = "%v" ) type ConnectionController struct { @@ -119,6 +122,11 @@ func (v *ConnectionController) Invoke(ctx context.Context, request common.Invoke logger.Error(logs.INVOKE_CONNECTION_REQUEST_REJECTED) return nil, errors.NewSkyflowError(errors.INVALID_INPUT_CODE, fmt.Sprintf(errors.UNKNOWN_ERROR, invokeErr.Error())) } + // Ensure response body is closed to prevent resource leaks + if res.Body != nil { + defer res.Body.Close() + } + metaData := map[string]interface{}{ constants.REQUEST_ID_KEY: requestId, } @@ -126,11 +134,58 @@ func (v *ConnectionController) Invoke(ctx context.Context, request common.Invoke logger.Info(logs.INVOKE_CONNECTION_REQUEST_RESOLVED) // Step 7: Parse Response if res.StatusCode >= http.StatusOK && res.StatusCode < http.StatusMultipleChoices { - parseRes, parseErr := parseResponse(res) - if parseErr != nil { - return nil, parseErr + response := common.InvokeConnectionResponse{Metadata: metaData} + if res.Body != nil { + contentType := res.Header.Get("Content-Type") + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, errors.NewSkyflowError(errors.INVALID_INPUT_CODE, errors.INVALID_RESPONSE) + } + if strings.Contains(contentType, string(common.APPLICATIONXML)) || strings.Contains(contentType, string(common.TEXTORXML)) { + response.Data = string(data) + return &response, nil + } else if strings.Contains(contentType, string(common.APPLICATIONORJSON)) || contentType == "" { + var jsonData interface{} + err = json.Unmarshal(data, &jsonData) + if err != nil { + response.Data = data + return &response, nil + } else { + response.Data = jsonData + return &response, nil + } + + } else if strings.Contains(contentType, string(common.TEXTORPLAIN)) { + response.Data = string(data) + return &response, nil + } else if strings.Contains(contentType, string(common.FORMURLENCODED)) { + // Parse URL-encoded form data + values, err := url.ParseQuery(string(data)) + if err != nil { + return nil, errors.NewSkyflowError(errors.INVALID_INPUT_CODE, errors.INVALID_RESPONSE) + } + // Convert url.Values to map[string]interface{} + result := make(map[string]interface{}) + for key, val := range values { + if len(val) == 1 { + result[key] = val[0] + } else { + result[key] = val + } + } + response.Data = result + return &response, nil + } else if strings.Contains(contentType, string(common.FORMDATA)) { + response.Data = string(data) + } else if strings.Contains(contentType, string(common.TEXTHTML)) { + response.Data = string(data) + return &response, nil + } else { + response.Data = string(data) + return &response, nil + } } - return &common.InvokeConnectionResponse{Data: parseRes, Metadata: metaData}, nil + return &response, nil } return nil, errors.SkyflowApiError(*res) } @@ -145,44 +200,185 @@ func buildRequestURL(baseURL string, pathParams map[string]string) string { func prepareRequest(request common.InvokeConnectionRequest, url string) (*http.Request, error) { var body io.Reader var writer *multipart.Writer - contentType := detectContentType(request.Headers) + var contentType string + shouldSetContentType := true + + contentType = detectContentType(request.Headers) + + // If no content-type and body is an object, default to JSON + if contentType == string(common.APPLICATIONORJSON) && request.Body != nil { + if _, ok := request.Body.(map[string]interface{}); ok { + contentType = string(common.APPLICATIONORJSON) + } + } + + switch contentType { + case string(common.APPLICATIONORJSON): + if strBody, ok := request.Body.(string); ok { + body = strings.NewReader(strBody) + } else if bodyMap, ok := request.Body.(map[string]interface{}); ok { + data, err := json.Marshal(bodyMap) + if err != nil { + return nil, err + } + body = strings.NewReader(string(data)) + } else if request.Body != nil { + if strBody, ok := request.Body.(string); ok { + body = strings.NewReader(strBody) + } else { + body = strings.NewReader(fmt.Sprintf(formatValue, request.Body)) + } + } - switch contentType { case string(common.FORMURLENCODED): - data, err := urlquery.Marshal(request.Body) - if err != nil { - return nil, err + if bodyMap, ok := request.Body.(map[string]interface{}); ok { + urlParams := buildURLEncodedParams(bodyMap) + body = strings.NewReader(urlParams.Encode()) + } else { //need to check here + body = strings.NewReader("") } - body = strings.NewReader(string(data)) case string(common.FORMDATA): buffer := new(bytes.Buffer) writer = multipart.NewWriter(buffer) - if err := writeFormData(writer, request.Body); err != nil { - return nil, err + + if bodyMap, ok := request.Body.(map[string]interface{}); ok { + for key, value := range bodyMap { + if value == nil { + continue + } + + // Check if value is *os.File or io.Reader for file uploads + if file, ok := value.(*os.File); ok { + // Handle *os.File - create form file + part, err := writer.CreateFormFile(key, file.Name()) + if err != nil { + return nil, err + } + if _, err := io.Copy(part, file); err != nil { + return nil, err + } + } else if reader, ok := value.(io.Reader); ok { + // Handle io.Reader - create form file with generic name + part, err := writer.CreateFormFile(key, key) + if err != nil { + return nil, err + } + if _, err := io.Copy(part, reader); err != nil { + return nil, err + } + } else if nestedMap, ok := value.(map[string]interface{}); ok { + // Check if value is a map/object - stringify it as JSON + jsonData, err := json.Marshal(nestedMap) + if err != nil { + return nil, err + } + if err := writer.WriteField(key, string(jsonData)); err != nil { + return nil, err + } + } else if arr, ok := value.([]interface{}); ok { + // Handle arrays - stringify as JSON + jsonData, err := json.Marshal(arr) + if err != nil { + return nil, err + } + if err := writer.WriteField(key, string(jsonData)); err != nil { + return nil, err + } + } else { + // Handle primitive values - convert to string + if err := writer.WriteField(key, fmt.Sprintf(formatValue, value)); err != nil { + return nil, err + } + } + } + } else if strBody, ok := request.Body.(string); ok { + // If body is already a string, use it as-is (though this is unusual for multipart) + body = strings.NewReader(strBody) + writer = nil // Don't use multipart writer for string body + shouldSetContentType = false // Keep user's content-type + } else if request.Body != nil { + // For other types, convert to string + body = strings.NewReader(fmt.Sprintf(formatValue, request.Body)) + writer = nil + shouldSetContentType = false + } + + if writer != nil { + writer.Close() + body = buffer + contentType = writer.FormDataContentType() // set with boundary + shouldSetContentType = true // Force set with boundary } - writer.Close() - body = buffer + case string(common.APPLICATIONXML), string(common.TEXTORXML): + if strBody, ok := request.Body.(string); ok { + // Body is already a string (raw XML) + body = strings.NewReader(strBody) + } else if bodyMap, ok := request.Body.(map[string]interface{}); ok { + // Convert map to XML + data, err := mapToXML(bodyMap) + if err != nil { + return nil, err + } + body = bytes.NewReader(data) + } else { + // throw error for unsupported body type + return nil, errors.NewSkyflowError(errors.INVALID_INPUT_CODE, errors.INVALID_XML_FORMAT) + } + + case string(common.TEXTORPLAIN): + if strBody, ok := request.Body.(string); ok { + body = strings.NewReader(strBody) + } else if request.Body != nil { + body = strings.NewReader(fmt.Sprintf(formatValue, request.Body)) + } + case string(common.TEXTHTML): + if strBody, ok := request.Body.(string); ok { + body = strings.NewReader(strBody) + } else if bodyMap, ok := request.Body.(map[string]interface{}); ok { + // send map as json in body + data, err := json.Marshal(bodyMap) + if err != nil { + return nil, err + } + body = strings.NewReader(string(data)) + } else if request.Body != nil { + body = strings.NewReader(fmt.Sprintf(formatValue, request.Body)) + } + default: - data, err := json.Marshal(request.Body) - if err != nil { - return nil, err + if strBody, ok := request.Body.(string); ok { + body = strings.NewReader(strBody) + } else if request.Body != nil { + if bodyMap, ok := request.Body.(map[string]interface{}); ok { + data, err := json.Marshal(bodyMap) + if err != nil { + return nil, err + } + body = strings.NewReader(string(data)) + } else { + body = strings.NewReader(fmt.Sprintf(formatValue, request.Body)) + } } - body = strings.NewReader(string(data)) } if request.Method == "" { request.Method = common.POST } request1, err := http.NewRequest(string(request.Method), url, body) - if err == nil && writer != nil { - request1.Header.Set(constants.HEADER_CONTENT_TYPE, writer.FormDataContentType()) + if err != nil { + return nil, err + } + + // Set content-type header + if shouldSetContentType && contentType != "" { + request1.Header.Set("content-type", contentType) } - return request1, err + return request1, nil } func writeFormData(writer *multipart.Writer, requestBody interface{}) error { - formData := rUrlencode(make([]interface{}, 0), make(map[string]string), requestBody) + formData := RUrlencode(make([]interface{}, 0), make(map[string]string), requestBody) for key, value := range formData { if err := writer.WriteField(key, value); err != nil { return err @@ -190,7 +386,37 @@ func writeFormData(writer *multipart.Writer, requestBody interface{}) error { } return nil } -func rUrlencode(parents []interface{}, pairs map[string]string, data interface{}) map[string]string { + +// buildURLEncodedParams converts a map to URL encoded params matching Node.js URLSearchParams behavior +func buildURLEncodedParams(data map[string]interface{}) *url.Values { + params := url.Values{} + + for key, value := range data { + if value == nil { + continue + } + + // Check if value is a map (nested object) + if nestedMap, ok := value.(map[string]interface{}); ok { + for nestedKey, nestedValue := range nestedMap { + paramKey := fmt.Sprintf("%s[%s]", key, nestedKey) + params.Add(paramKey, fmt.Sprintf(formatValue, nestedValue)) + } + } else if arr, ok := value.([]interface{}); ok { + // Handle arrays + for _, item := range arr { + params.Add(key, fmt.Sprintf(formatValue, item)) + } + } else { + // Handle primitive values + params.Add(key, fmt.Sprintf(formatValue, value)) + } + } + + return ¶ms +} + +func RUrlencode(parents []interface{}, pairs map[string]string, data interface{}) map[string]string { switch reflect.TypeOf(data).Kind() { case reflect.Int: @@ -205,7 +431,7 @@ func rUrlencode(parents []interface{}, pairs map[string]string, data interface{} var mapOfdata = (data).(map[string]interface{}) for index, value := range mapOfdata { parents = append(parents, index) - rUrlencode(parents, pairs, value) + RUrlencode(parents, pairs, value) parents = parents[:len(parents)-1] } default: @@ -219,7 +445,7 @@ func renderKey(parents []interface{}) string { for index := range parents { var typeOfindex = reflect.TypeOf(parents[index]).Kind() if depth > 0 || typeOfindex == reflect.Int { - outputString = outputString + fmt.Sprintf("[%v]", parents[index]) + outputString = outputString + fmt.Sprintf("["+formatValue+"]", parents[index]) } else { outputString = outputString + (parents[index]).(string) } @@ -260,9 +486,17 @@ func setHeaders(request *http.Request, api ConnectionController, invokeRequest c } else { request.Header.Set(constants.HEADER_AUTHORIZATION, api.Token) } - request.Header.Set(constants.HEADER_CONTENT_TYPE, constants.CONTENT_TYPE_JSON) + + // Only set default content-type if not already set (preserve multipart boundary) + if request.Header.Get(constants.HEADER_CONTENT_TYPE) == "" { + request.Header.Set(constants.HEADER_CONTENT_TYPE, constants.CONTENT_TYPE_JSON) + } for key, value := range invokeRequest.Headers { + // Skip content-type from user headers to preserve the one set in prepareRequest (especially multipart boundaries) + if strings.ToLower(key) == constants.HEADER_CONTENT_TYPE { + continue + } request.Header.Set(key, value) } } @@ -277,14 +511,52 @@ func sendRequest(request *http.Request) (*http.Response, string, error) { } return response, requestId, nil } -func parseResponse(response *http.Response) (map[string]interface{}, *errors.SkyflowError) { - data, err := ioutil.ReadAll(response.Body) - if err != nil { - return nil, errors.NewSkyflowError(errors.INVALID_INPUT_CODE, errors.INVALID_RESPONSE) + +// mapToXML converts a map[string]interface{} to XML format +func mapToXML(data map[string]interface{}) ([]byte, error) { + var buf bytes.Buffer + buf.WriteString("") + buf.WriteString("") + + for key, value := range data { + writeXMLElement(&buf, key, value) } - var result map[string]interface{} - if err1 := json.Unmarshal(data, &result); err1 != nil { - return nil, errors.NewSkyflowError(errors.INVALID_INPUT_CODE, errors.INVALID_RESPONSE) + + buf.WriteString("") + return buf.Bytes(), nil +} + +// writeXMLElement recursively writes XML elements with proper escaping +func writeXMLElement(buf *bytes.Buffer, key string, value interface{}) { + if value == nil { + buf.WriteString(fmt.Sprintf("<%s/>", key)) + return } - return result, nil + + switch v := value.(type) { + case map[string]interface{}: + buf.WriteString(fmt.Sprintf("<%s>", key)) + for k, val := range v { + writeXMLElement(buf, k, val) + } + buf.WriteString(fmt.Sprintf("", key)) + case []interface{}: + for _, item := range v { + writeXMLElement(buf, key, item) + } + default: + // Escape special XML characters + escapedValue := escapeXML(fmt.Sprintf(formatValue, v)) + buf.WriteString(fmt.Sprintf("<%s>%s", key, escapedValue, key)) + } +} + +// escapeXML escapes special XML characters +func escapeXML(s string) string { + s = strings.ReplaceAll(s, "&", "&") + s = strings.ReplaceAll(s, "<", "<") + s = strings.ReplaceAll(s, ">", ">") + s = strings.ReplaceAll(s, "\"", """) + s = strings.ReplaceAll(s, "'", "'") + return s } diff --git a/v2/internal/vault/controller/controller_test.go b/v2/internal/vault/controller/controller_test.go index 1f3bae3..f526929 100644 --- a/v2/internal/vault/controller/controller_test.go +++ b/v2/internal/vault/controller/controller_test.go @@ -1,13 +1,18 @@ package controller_test import ( + // "bytes" "context" "encoding/json" + "errors" "fmt" + // "io" + // "mime/multipart" "net/http" "net/http/httptest" "os" "path/filepath" + "strings" "testing" "github.com/skyflowapi/skyflow-go/v2/internal/generated/option" @@ -22,6 +27,13 @@ import ( skyflowError "github.com/skyflowapi/skyflow-go/v2/utils/error" ) +// errorReader is a custom io.Reader that always returns an error +type errorReader struct{} + +func (e *errorReader) Read(p []byte) (n int, err error) { + return 0, errors.New("simulated read error") +} + var ( mockInsertSuccessJSON = `{"vaultID":"id", "responses":[{"Body":{"records":[{"skyflow_id":"skyflowid", "tokens":{"name_on_card":"token1"}}]}, "Status":200}]}` mockInsertContinueFalseSuccessJSON = `{"records":[{"skyflow_id":"skyflowid1", "tokens":{"name":"nameToken1"}}, {"skyflow_id":"skyflowid2", "tokens":{"expiry_month":"monthToken", "name":"nameToken3"}}]}` @@ -1178,6 +1190,7 @@ var _ = Describe("ConnectionController", func() { Context("when making a valid request", func() { BeforeEach(func() { mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"key": "value"}`)) })) @@ -1240,6 +1253,7 @@ var _ = Describe("ConnectionController", func() { }) It("should return an success from api with invalid body", func() { mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Header().Set("Content-Length", "0") _, _ = w.Write([]byte(`67676`)) @@ -1249,13 +1263,15 @@ var _ = Describe("ConnectionController", func() { return nil } response, err := ctrl.Invoke(ctx, mockRequest) - Expect(response).To(BeNil()) - Expect(err).ToNot(BeNil()) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + Expect(response.Data).To(Equal(float64(67676))) }) }) Context("Invoke with different content types", func() { BeforeEach(func() { mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"key": "value"}`)) })) @@ -1432,7 +1448,1989 @@ var _ = Describe("ConnectionController", func() { }) }) - }) + Context("Handling XML content types", func() { + BeforeEach(func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`value`)) + })) + ctrl.Config.ConnectionUrl = mockServer.URL + }) + + AfterEach(func() { + mockServer.Close() + }) + + It("should handle application/xml content type with map body", func() { + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "application/xml", + }, + Body: map[string]interface{}{ + "key": "value", + "nested": map[string]interface{}{ + "inner": "data", + }, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + Expect(response.Data).To(ContainSubstring("value")) + }) + + It("should handle text/xml content type", func() { + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "text/xml", + }, + Body: map[string]interface{}{ + "user": "john", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle XML with special characters requiring escaping", func() { + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "application/xml", + }, + Body: map[string]interface{}{ + "key": "value with & \"characters\" 'here'", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle XML with string body", func() { + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "application/xml", + }, + Body: "test", + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle XML with arrays", func() { + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "application/xml", + }, + Body: map[string]interface{}{ + "items": []interface{}{"item1", "item2", "item3"}, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + }) + + Context("Handling URL-encoded content with nested objects", func() { + BeforeEach(func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/x-www-form-urlencoded") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`key=value&nested=data`)) + })) + ctrl.Config.ConnectionUrl = mockServer.URL + }) + + AfterEach(func() { + mockServer.Close() + }) + + It("should handle nested objects in URL-encoded format", func() { + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + }, + Body: map[string]interface{}{ + "user": map[string]interface{}{ + "name": "john", + "age": 30, + }, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle arrays in URL-encoded format", func() { + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + }, + Body: map[string]interface{}{ + "tags": []interface{}{"tag1", "tag2", "tag3"}, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle mixed nested objects and arrays", func() { + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + }, + Body: map[string]interface{}{ + "user": map[string]interface{}{ + "name": "john", + }, + "tags": []interface{}{"tag1", "tag2"}, + "key": "value", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + }) + + Context("Handling multipart/form-data with file uploads", func() { + BeforeEach(func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"success": true}`)) + })) + ctrl.Config.ConnectionUrl = mockServer.URL + }) + + AfterEach(func() { + mockServer.Close() + }) + + It("should handle multipart/form-data with *os.File", func() { + // Create a temporary file for testing + tmpFile, err := os.CreateTemp("", "test-*.txt") + Expect(err).To(BeNil()) + defer os.Remove(tmpFile.Name()) + _, _ = tmpFile.WriteString("test file content") + tmpFile.Close() + + // Reopen for reading + file, err := os.Open(tmpFile.Name()) + Expect(err).To(BeNil()) + defer file.Close() + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "file": file, + "key": "value", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with io.Reader", func() { + reader := strings.NewReader("test content from reader") + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "upload": reader, + "name": "test", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with nested maps (JSON stringified)", func() { + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "user": map[string]interface{}{ + "name": "john", + "age": 30, + }, + "simple": "value", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with arrays (JSON stringified)", func() { + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "tags": []interface{}{"tag1", "tag2", "tag3"}, + "key": "value", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + }) + + Context("Handling text/plain and text/html content types", func() { + BeforeEach(func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + contentType := r.Header.Get("Content-Type") + w.Header().Set("Content-Type", contentType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("plain text response")) + })) + ctrl.Config.ConnectionUrl = mockServer.URL + }) + + AfterEach(func() { + mockServer.Close() + }) + + It("should handle text/plain content type", func() { + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "text/plain", + }, + Body: "This is plain text content", + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + Expect(response.Data).To(Equal("plain text response")) + }) + + It("should handle text/html content type", func() { + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "text/html", + }, + Body: "Hello", + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle text/html with map body (converted to JSON)", func() { + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "text/html", + }, + Body: map[string]interface{}{ + "key": "value", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + }) + + Context("Handling response parsing for different content types", func() { + It("should parse XML response correctly", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`value`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "GET", + Headers: map[string]string{ + "Content-Type": "application/json", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + Expect(response.Data).To(ContainSubstring("value")) + }) + + It("should parse URL-encoded response correctly", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/x-www-form-urlencoded") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`key1=value1&key2=value2&key3=value3a&key3=value3b`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "GET", + Headers: map[string]string{ + "Content-Type": "application/json", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + // Response should be a map + dataMap, ok := response.Data.(map[string]interface{}) + Expect(ok).To(BeTrue()) + Expect(dataMap).To(HaveKey("key1")) + Expect(dataMap["key1"]).To(Equal("value1")) + // key3 should be an array since it has multiple values + Expect(dataMap).To(HaveKey("key3")) + }) + + It("should parse JSON response correctly", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"key": "value", "number": 42}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "GET", + Headers: map[string]string{ + "Content-Type": "application/json", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + dataMap, ok := response.Data.(map[string]interface{}) + Expect(ok).To(BeTrue()) + Expect(dataMap["key"]).To(Equal("value")) + Expect(dataMap["number"]).To(Equal(float64(42))) + }) + + It("should parse text/plain response correctly", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("Simple plain text")) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "GET", + Headers: map[string]string{ + "Content-Type": "application/json", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + Expect(response.Data).To(Equal("Simple plain text")) + }) + + It("should handle invalid JSON response gracefully", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`invalid json content`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "GET", + Headers: map[string]string{ + "Content-Type": "application/json", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + // Should return as bytes when JSON parsing fails + Expect(response.Data).To(Equal([]byte("invalid json content"))) + }) + + It("should handle invalid URL-encoded response gracefully", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/x-www-form-urlencoded") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`%invalid%`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "GET", + Headers: map[string]string{ + "Content-Type": "application/json", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(response).To(BeNil()) + Expect(err).ToNot(BeNil()) + }) + + It("should handle multipart/form-data response correctly", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "multipart/form-data") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`boundary data`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "GET", + Headers: map[string]string{ + "Content-Type": "application/json", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + Expect(response.Data).To(Equal("boundary data")) + }) + + It("should handle URL-encoded response with multiple values for same key", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/x-www-form-urlencoded") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`color=red&color=blue&color=green`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "GET", + Headers: map[string]string{ + "Content-Type": "application/json", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + dataMap, ok := response.Data.(map[string]interface{}) + Expect(ok).To(BeTrue()) + colors, ok := dataMap["color"].([]string) + Expect(ok).To(BeTrue()) + Expect(len(colors)).To(Equal(3)) + }) + + It("should handle multipart/form-data body as string", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: "raw string body", + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data body as non-map type", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: 12345, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle text/plain body as non-string", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "text/plain", + }, + Body: 98765, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle text/html body as non-string", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "text/html", + }, + Body: 54321, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle unknown content-type with non-map body", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "application/octet-stream", + }, + Body: 99999, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle empty response body", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "GET", + Headers: map[string]string{ + "Content-Type": "application/json", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle JSON body as string for application/json", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "application/json", + }, + Body: `{"test": "value"}`, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle JSON body as non-map and non-string (integer)", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "application/json", + }, + Body: 12345, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle URL-encoded body with non-map type", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + }, + Body: "not a map", + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle XML body with unsupported type (not string or map)", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "application/xml", + }, + Body: 12345, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(response).To(BeNil()) + Expect(err).ToNot(BeNil()) + Expect(err.GetMessage()).To(ContainSubstring("Invalid XML format")) + }) + + It("should handle default method as POST when method is empty", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + Expect(r.Method).To(Equal("POST")) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "", + Headers: map[string]string{ + "Content-Type": "application/json", + }, + Body: map[string]interface{}{"test": "value"}, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle text/html body as map (converted to JSON)", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "text/html", + }, + Body: map[string]interface{}{"html": "

Title

"}, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle default content-type with map body", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "application/custom", + }, + Body: map[string]interface{}{"key": "value"}, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle default content-type with string body", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "application/custom", + }, + Body: "string body", + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with nil value in map", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "field1": "value1", + "field2": nil, + "field3": "value3", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + // Error handling tests for multipart/form-data + It("should handle multipart/form-data with complex nested map containing all valid types", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "nestedMap": map[string]interface{}{ + "key1": "value1", + "key2": 123, + "key3": true, + "key4": 45.67, + }, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with array containing all valid types", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "arrayField": []interface{}{ + "string", + 123, + true, + 45.67, + map[string]interface{}{"nested": "map"}, + }, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with primitive string value", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "simpleString": "test value", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with primitive int value", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "simpleInt": 42, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with primitive bool value", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "simpleBool": true, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with primitive float value", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "simpleFloat": 3.14159, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with map containing unsupported types that json.Marshal handles", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + // json.Marshal can handle most basic types, so this tests the success path + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "complexMap": map[string]interface{}{ + "nullValue": nil, + "emptyString": "", + "zero": 0, + "negativInt": -42, + }, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with empty nested map", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "emptyMap": map[string]interface{}{}, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with empty array", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "emptyArray": []interface{}{}, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with deeply nested structure", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "level1": map[string]interface{}{ + "level2": map[string]interface{}{ + "level3": map[string]interface{}{ + "data": "deep value", + }, + }, + }, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with array of arrays", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "matrix": []interface{}{ + []interface{}{1, 2, 3}, + []interface{}{4, 5, 6}, + []interface{}{7, 8, 9}, + }, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with mixed file and data fields", func() { + tmpFile, err := os.CreateTemp("", "test-mixed-*.txt") + Expect(err).To(BeNil()) + defer os.Remove(tmpFile.Name()) + _, _ = tmpFile.WriteString("file content") + tmpFile.Seek(0, 0) + + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "file": tmpFile, + "name": "test file", + "metadata": map[string]interface{}{ + "size": 12, + "type": "text", + }, + "tags": []interface{}{"test", "sample"}, + "count": 42, + "enabled": true, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + // Error path tests for multipart/form-data operations + It("should handle error when file is closed before reading in multipart/form-data", func() { + tmpFile, err := os.CreateTemp("", "test-closed-*.txt") + Expect(err).To(BeNil()) + fileName := tmpFile.Name() + _, _ = tmpFile.WriteString("file content") + // Close the file to trigger io.Copy error + tmpFile.Close() + defer os.Remove(fileName) + + // Reopen file for deletion but create request with closed file handle + closedFile, _ := os.Open(fileName) + closedFile.Close() // Close immediately to trigger error + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "file": closedFile, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + // Should return error due to closed file + Expect(err).ToNot(BeNil()) + Expect(response).To(BeNil()) + }) + + It("should handle multipart/form-data with types that json.Marshal can handle", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + // Test with all JSON-compatible types in nested map + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "complexData": map[string]interface{}{ + "string": "value", + "number": 42, + "float": 3.14, + "bool": true, + "null": nil, + "array": []interface{}{1, 2, 3}, + "nested": map[string]interface{}{"key": "val"}, + }, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with arrays containing all JSON types", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "mixedArray": []interface{}{ + "string", + 123, + 45.67, + true, + false, + nil, + map[string]interface{}{"nested": "object"}, + []interface{}{1, 2, 3}, + }, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with all primitive value types", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "stringField": "text value", + "intField": 42, + "floatField": 3.14159, + "boolField": true, + "zeroField": 0, + "emptyString": "", + "negativeInt": -100, + "negativeFloat": -99.99, + "falseField": false, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with bytes.Reader as io.Reader", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + fileContent := []byte("This is file content from bytes.Reader") + reader := strings.NewReader(string(fileContent)) + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "fileFromReader": reader, + "description": "File uploaded via io.Reader", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with multiple files of different types", func() { + // Create temp files + txtFile, _ := os.CreateTemp("", "test-*.txt") + _, _ = txtFile.WriteString("text content") + txtFile.Seek(0, 0) + defer os.Remove(txtFile.Name()) + + jsonFile, _ := os.CreateTemp("", "test-*.json") + _, _ = jsonFile.WriteString(`{"key": "value"}`) + jsonFile.Seek(0, 0) + defer os.Remove(jsonFile.Name()) + + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "textFile": txtFile, + "jsonFile": jsonFile, + "readerFile": strings.NewReader("reader content"), + "metadata": map[string]interface{}{"count": 2}, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with large nested structure", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "level1": map[string]interface{}{ + "level2a": map[string]interface{}{ + "level3": []interface{}{ + map[string]interface{}{"id": 1, "name": "item1"}, + map[string]interface{}{"id": 2, "name": "item2"}, + }, + }, + "level2b": []interface{}{ + []interface{}{1, 2, 3}, + []interface{}{4, 5, 6}, + }, + }, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with special characters in primitive values", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "specialChars": "value with spaces & symbols !@#$%^&*()", + "unicode": "Hello δΈ–η•Œ 🌍", + "quotes": `value with "quotes" and 'apostrophes'`, + "newlines": "line1\nline2\nline3", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + // Error path tests for FORMDATA case + It("should return error when io.Reader fails during io.Copy in multipart/form-data", func() { + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "failingReader": &errorReader{}, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + // Should return error due to failing reader + Expect(err).ToNot(BeNil()) + Expect(response).To(BeNil()) + }) + + It("should handle multipart/form-data with channel type causing json.Marshal to work with map", func() { + // Note: json.Marshal will handle most types, but channels, functions, and complex types cause issues + // However, since we're putting them in a map[string]interface{}, Go will handle the conversion + // This test verifies the happy path where json.Marshal succeeds even with edge case types + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "nestedData": map[string]interface{}{ + "validString": "test", + "validNumber": 123, + "validBool": true, + }, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with array containing various valid types", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "arrayData": []interface{}{ + "string", + 123, + 45.67, + true, + map[string]interface{}{"nested": "value"}, + }, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with all primitive types as WriteField values", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "stringPrimitive": "text", + "intPrimitive": 42, + "floatPrimitive": 3.14, + "boolPrimitive": true, + "int64Primitive": int64(9223372036854775807), + "int32Primitive": int32(2147483647), + "float32Primitive": float32(3.14159), + "uint8Primitive": uint8(255), + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with empty string primitive", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "emptyString": "", + "whitespace": " ", + "tab": "\t", + "newline": "\n", + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with zero values for all numeric types", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "zeroInt": 0, + "zeroFloat": 0.0, + "zeroInt64": int64(0), + "zeroFloat32": float32(0.0), + "falseBool": false, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with very long string primitive", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + longString := strings.Repeat("a", 10000) + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "longString": longString, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with nested maps at multiple levels", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "nested1": map[string]interface{}{ + "nested2": map[string]interface{}{ + "nested3": map[string]interface{}{ + "nested4": map[string]interface{}{ + "value": "deeply nested", + }, + }, + }, + }, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with arrays containing nested arrays", func() { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "nestedArrays": []interface{}{ + []interface{}{ + []interface{}{ + []interface{}{1, 2, 3}, + }, + }, + }, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + + It("should handle multipart/form-data with combination of files, maps, arrays, and primitives", func() { + tmpFile, err := os.CreateTemp("", "combo-test-*.txt") + Expect(err).To(BeNil()) + defer os.Remove(tmpFile.Name()) + _, _ = tmpFile.WriteString("combo test content") + tmpFile.Seek(0, 0) + + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"status": "ok"}`)) + })) + defer mockServer.Close() + ctrl.Config.ConnectionUrl = mockServer.URL + + request := InvokeConnectionRequest{ + Method: "POST", + Headers: map[string]string{ + "Content-Type": "multipart/form-data", + }, + Body: map[string]interface{}{ + "file1": tmpFile, + "file2": strings.NewReader("reader content"), + "map1": map[string]interface{}{"key": "value"}, + "array1": []interface{}{1, 2, 3}, + "string1": "text", + "int1": 42, + "bool1": true, + "float1": 3.14, + }, + } + SetBearerTokenForConnectionControllerFunc = func(v *ConnectionController) *skyflowError.SkyflowError { + return nil + } + response, err := ctrl.Invoke(ctx, request) + Expect(err).To(BeNil()) + Expect(response).ToNot(BeNil()) + }) + }) + +}) + +var _ = Describe("Connection Utility Functions", func() { + Describe("RUrlencode and renderKey", func() { + It("should handle int values", func() { + parents := make([]interface{}, 0) + pairs := make(map[string]string) + parents = append(parents, "count") + result := RUrlencode(parents, pairs, 42) + Expect(result["count"]).To(Equal("42")) + }) + + It("should handle float32 values", func() { + parents := make([]interface{}, 0) + pairs := make(map[string]string) + parents = append(parents, "price") + result := RUrlencode(parents, pairs, float32(19.99)) + Expect(result["price"]).To(ContainSubstring("19.99")) + }) + + It("should handle float64 values", func() { + parents := make([]interface{}, 0) + pairs := make(map[string]string) + parents = append(parents, "amount") + result := RUrlencode(parents, pairs, float64(99.95)) + Expect(result["amount"]).To(ContainSubstring("99.95")) + }) + + It("should handle bool values", func() { + parents := make([]interface{}, 0) + pairs := make(map[string]string) + parents = append(parents, "active") + result := RUrlencode(parents, pairs, true) + Expect(result["active"]).To(Equal("true")) + }) + + It("should handle string values", func() { + parents := make([]interface{}, 0) + pairs := make(map[string]string) + parents = append(parents, "name") + result := RUrlencode(parents, pairs, "John Doe") + Expect(result["name"]).To(Equal("John Doe")) + }) + + It("should handle nested map values", func() { + parents := make([]interface{}, 0) + pairs := make(map[string]string) + parents = append(parents, "user") + data := map[string]interface{}{ + "name": "Alice", + "age": 30, + } + result := RUrlencode(parents, pairs, data) + Expect(result["user[name]"]).To(Equal("Alice")) + Expect(result["user[age]"]).To(Equal("30")) + }) + + It("should handle deeply nested map values", func() { + parents := make([]interface{}, 0) + pairs := make(map[string]string) + parents = append(parents, "user") + data := map[string]interface{}{ + "profile": map[string]interface{}{ + "firstName": "Bob", + "age": 25, + }, + } + result := RUrlencode(parents, pairs, data) + Expect(result["user[profile][firstName]"]).To(Equal("Bob")) + Expect(result["user[profile][age]"]).To(Equal("25")) + }) + }) + + // Describe("writeFormData", func() { + // It("should write form data with multiple types", func() { + // buffer := new(bytes.Buffer) + // writer := multipart.NewWriter(buffer) + + // requestBody := map[string]interface{}{ + // "string": "value", + // "number": 123, + // "bool": true, + // "nested": map[string]interface{}{ + // "key": "nested_value", + // }, + // } + + // err := writeFormData(writer, requestBody) + // Expect(err).To(BeNil()) + // writer.Close() + + // content := buffer.String() + // Expect(content).To(ContainSubstring("string")) + // Expect(content).To(ContainSubstring("value")) + // Expect(content).To(ContainSubstring("number")) + // Expect(content).To(ContainSubstring("123")) + // Expect(content).To(ContainSubstring("bool")) + // Expect(content).To(ContainSubstring("true")) + // Expect(content).To(ContainSubstring("nested[key]")) + // Expect(content).To(ContainSubstring("nested_value")) + // }) + + // It("should handle float values in form data", func() { + // buffer := new(bytes.Buffer) + // writer := multipart.NewWriter(buffer) + + // requestBody := map[string]interface{}{ + // "price": float64(19.99), + // } + + // err := writeFormData(writer, requestBody) + // Expect(err).To(BeNil()) + // writer.Close() + + // content := buffer.String() + // Expect(content).To(ContainSubstring("price")) + // Expect(content).To(ContainSubstring("19.99")) + // }) + // }) +}) }) var _ = Describe("VaultController", func() { diff --git a/v2/utils/common/common.go b/v2/utils/common/common.go index c94d243..097dd8f 100644 --- a/v2/utils/common/common.go +++ b/v2/utils/common/common.go @@ -218,7 +218,7 @@ const ( ) type InvokeConnectionResponse struct { - Data map[string]interface{} + Data interface{} Metadata map[string]interface{} Errors map[string]interface{} } @@ -346,7 +346,7 @@ type InvokeConnectionRequest struct { Method RequestMethod QueryParams map[string]interface{} PathParams map[string]string - Body map[string]interface{} + Body interface{} Headers map[string]string } type ContentType string @@ -357,6 +357,8 @@ const ( FORMURLENCODED ContentType = "application/x-www-form-urlencoded" FORMDATA ContentType = "multipart/form-data" TEXTORXML ContentType = "text/xml" + APPLICATIONXML ContentType = "application/xml" + TEXTHTML ContentType = "text/html" ) type OrderByEnum string diff --git a/v2/utils/error/message.go b/v2/utils/error/message.go index 784b86c..e36109b 100644 --- a/v2/utils/error/message.go +++ b/v2/utils/error/message.go @@ -5,6 +5,7 @@ import internal "github.com/skyflowapi/skyflow-go/v2/internal/constants" // TO DO const ( // config + INVALID_XML_FORMAT string = internal.SDK_PREFIX + " Validation error. Invalid XML format. Specify a valid XML format as string." VAULT_ID_ALREADY_IN_CONFIG_LIST string = internal.SDK_PREFIX + " Validation error. VaultId is present in an existing config. Specify a new vaultId in config." VAULT_ID_NOT_IN_CONFIG_LIST string = internal.SDK_PREFIX + " Validation error. VaultId is missing from the config. Specify the vaultIds from configs." CONNECTION_ID_NOT_IN_CONFIG_LIST string = internal.SDK_PREFIX + " Validation error. ConnectionId is missing from the config. Specify the connectionIds from configs." diff --git a/v2/utils/messages/error_logs.go b/v2/utils/messages/error_logs.go index 3244ce4..c493844 100644 --- a/v2/utils/messages/error_logs.go +++ b/v2/utils/messages/error_logs.go @@ -3,6 +3,7 @@ package logs import . "github.com/skyflowapi/skyflow-go/v2/internal/constants" const ( + INVALID_XML_FORMAT = SDK_LOG_PREFIX + " Validation error. Invalid XML format. Specify a valid XML format as string." CLIENT_ID_NOT_FOUND = SDK_LOG_PREFIX + "Invalid credentials. Client ID cannot be empty." TOKEN_URI_NOT_FOUND = SDK_LOG_PREFIX + "Invalid credentials. Token URI cannot be empty." KEY_ID_NOT_FOUND = SDK_LOG_PREFIX + "Invalid credentials. Key ID cannot be empty." diff --git a/v2/utils/messages/info_logs.go b/v2/utils/messages/info_logs.go index 2a67223..fb1ebfc 100644 --- a/v2/utils/messages/info_logs.go +++ b/v2/utils/messages/info_logs.go @@ -6,7 +6,7 @@ import ( const ( EMPTY_BEARER_TOKEN = SDK_LOG_PREFIX + "BearerToken is Empty" - BEARER_TOKEN_EXPIRED = SDK_LOG_PREFIX + "BearerToken is expired" + BEARER_TOKEN_EXPIRED = SDK_LOG_PREFIX + "Bearer Token provided is either invalid or has expired." GENERATE_BEARER_TOKEN_TRIGGERED = SDK_LOG_PREFIX + "GenerateBearerToken is triggered" GENERATE_BEARER_TOKEN_SUCCESS = SDK_LOG_PREFIX + "BearerToken is generated" GENERATE_SIGNED_DATA_TOKEN_SUCCESS = SDK_LOG_PREFIX + "Signed Data tokens are generated"