diff --git a/v2/.golangci.yml b/v2/.golangci.yml index 9961087..b33b9e7 100644 --- a/v2/.golangci.yml +++ b/v2/.golangci.yml @@ -9,6 +9,7 @@ linters: default: none enable: - revive + - goconst exclusions: paths: - internal/generated @@ -19,6 +20,15 @@ linters: enable-all-rules: false enable-default-rules: false rules: + - name: add-constant + severity: warning + disabled: false + arguments: + - max-lit-count: "1" + allow-strs: '""' + allow-ints: "0,1,2,64,0644" + allow-floats: "0.0,0.,1.0,1.,2.0,2." + - name: var-naming arguments: - ["ID", "URL", "API", "HTTP", "JSON", "UUID"] @@ -26,7 +36,13 @@ linters: - - upper-case-const: true skip-package-name-checks: true - name: receiver-naming - + + - name: unexported-return + disabled: true + - name: unused-parameter + disabled: true + - name: redefines-builtin-id + disabled: true - name: exported disabled: true - name: package-comments @@ -34,4 +50,10 @@ linters: - name: dot-imports disabled: true - name: indent-error-flow - disabled: true \ No newline at end of file + disabled: true + + goconst: + min-len: 1 + min-occurrences: 1 + match-constant: true + numbers: true \ No newline at end of file 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/constants/constants.go b/v2/internal/constants/constants.go index ff21d0e..32a9b7c 100644 --- a/v2/internal/constants/constants.go +++ b/v2/internal/constants/constants.go @@ -16,4 +16,171 @@ const ( ERROR_FROM_CLIENT = "error-from-client" REQUEST_KEY = "X-Request-Id" SKYFLOW_ID = "skyflow_id" + + // File extensions + FILE_EXTENSION_TXT = "txt" + FILE_EXTENSION_PDF = "pdf" + FILE_EXTENSION_JSON = "json" + FILE_EXTENSION_XML = "xml" + FILE_EXTENSION_MP3 = "mp3" + FILE_EXTENSION_WAV = "wav" + FILE_EXTENSION_JPG = "jpg" + FILE_EXTENSION_JPEG = "jpeg" + FILE_EXTENSION_PNG = "png" + FILE_EXTENSION_BMP = "bmp" + FILE_EXTENSION_TIF = "tif" + FILE_EXTENSION_TIFF = "tiff" + FILE_EXTENSION_PPT = "ppt" + FILE_EXTENSION_PPTX = "pptx" + FILE_EXTENSION_CSV = "csv" + FILE_EXTENSION_XLS = "xls" + FILE_EXTENSION_XLSX = "xlsx" + FILE_EXTENSION_DOC = "doc" + FILE_EXTENSION_DOCX = "docx" + + // Encoding types + ENCODING_BASE64 = "base64" + ENCODING_UTF8 = "utf-8" + ENCODING_BINARY = "binary" + + // File type identifiers + FILE_TYPE_TEXT = "text" + FILE_TYPE_IMAGE = "image" + FILE_TYPE_PDF = "pdf" + FILE_TYPE_PPT = "ppt" + FILE_TYPE_SPREAD = "spread" + FILE_TYPE_AUDIO = "audio" + FILE_TYPE_DOCUMENT = "document" + FILE_TYPE_STRUCTURED = "structured" + FILE_TYPE_GENERIC = "generic" + + // Detect status + DETECT_STATUS_IN_PROGRESS = "IN_PROGRESS" + DETECT_STATUS_SUCCESS = "SUCCESS" + DETECT_STATUS_FAILED = "FAILED" + + // HTTP schemes and protocols + HTTPS_PROTOCOL = "https" + HTTP_PROTOCOL = "http" + + // PEM key type + PRIVATE_KEY_PEM_TYPE = "PRIVATE KEY" + + // Entity types + ENTITY_TYPE_REDACTED = "redacted" + ENTITY_TYPE_MASKED = "masked" + ENTITY_TYPE_PLAIN_TEXT = "plain_text" + ENTITY_TYPE_TEXT = "text" + ENTITY_TYPE_ENTITY_ONLY = "entity_only" + ENTITY_TYPE_VAULT_TOKEN = "vault_token" + ENTITY_TYPE_ENTITY_UNIQUE_CTR = "entity_unique_counter" + ENTITY_TYPE_ENTITIES = "entities" + + // Request/API names + REQUEST_DEIDENTIFY_FILE = "DeidentifyFileRequest" + REQUEST_INSERT = "Insert" + REQUEST_INSERT_LOWER = "insert" + REQUEST_DETOKENIZE = "DetokenizeRequest" + REQUEST_GET = "Get" + REQUEST_DELETE = "delete" + REQUEST_UPDATE = "update" + REQUEST_UPLOAD_FILE = "UploadFile" + REQUEST_INVOKE_CONN = "Invoke Connection" + + // HTTP headers + HEADER_CONTENT_TYPE = "content-type" + HEADER_CONTENT_TYPE_CAPITAL = "Content-Type" + + // File type mapping for Detect (removed - use FILE_TYPE_* constants instead) + + // Redaction types for Detect + DETECT_REDACTION_TYPE_REDACTED = "redacted" + DETECT_REDACTION_TYPE_MASKED = "masked" + DETECT_REDACTION_TYPE_PLAINTEXT = "plaintext" + + // File output types for Detect + FILE_OUTPUT_TYPE_REDACTED_FILE = "redacted_file" + DEIDENTIFIED_FILE_PREFIX = "deidentified." + + // File processing + PROCESSED_PREFIX = "processed-" + PERMISSION_CHECK_FILE = ".permission_check" + + // Error and status constants + UNKNOWN_STATUS = "UNKNOWN" + UNKNOWN_ERROR = "Unknown error" + HTTP_STATUS_BAD_REQUEST = "Bad Request" + ERROR_KEY_FROM_CLIENT = "errorFromClient" + + // Environment variables + SKYFLOW_CREDENTIALS_ENV = "SKYFLOW_CREDENTIALS" + + // HTTP headers and content types + HEADER_AUTHORIZATION = "x-skyflow-authorization" + CONTENT_TYPE_JSON = "application/json" + CONTENT_TYPE_TEXT_PLAIN = "text/plain" + CONTENT_TYPE_TEXT_CHARSET = "text/plain; charset=utf-8" + RESPONSE_HEADER_REQUEST_ID = "x-request-id" + + // JSON error response keys + ERROR_KEY_ERROR = "error" + ERROR_KEY_MESSAGE = "message" + ERROR_KEY_HTTP_CODE = "http_code" + ERROR_KEY_GRPC_CODE = "grpc_code" + ERROR_KEY_HTTP_STATUS = "http_status" + ERROR_KEY_DETAILS = "details" + + // JSON response keys + REQUEST_ID_KEY = "request_id" + RESPONSE_KEY_REQUEST_ID = "RequestId" + RESPONSE_KEY_HTTP_CODE = "HttpCode" + RESPONSE_KEY_SKYFLOW_ID = "skyflowId" + + // Other constants + ERROR_FAILED_TO_READ = "Failed to read error" + + // Credentials and JWT keys + CRED_KEY_PRIVATE_KEY = "privateKey" + CRED_KEY_CLIENT_ID = "clientID" + CRED_KEY_TOKEN_URI = "tokenURI" + CRED_KEY_KEY_ID = "keyID" + API_KEY_PREFIX = "sky-" + + // JWT claim keys + JWT_CLAIM_EXP = "exp" + JWT_CLAIM_CTX = "ctx" + JWT_CLAIM_ISS = "iss" + JWT_CLAIM_AUD = "aud" + JWT_CLAIM_KEY = "key" + JWT_CLAIM_IAT = "iat" + JWT_CLAIM_SUB = "sub" + JWT_CLAIM_TOK = "tok" + + // Request validation + REQUEST_INVOKE_CONNECTION = "InvokeConnectionRequest" + REQUEST_ENTITY_ONLY = "entity_only" + REQUEST_DEIDENTIFY_TEXT = "DeidentifyTextRequest" + REQUEST_REIDENTIFY_TEXT = "ReidentifyTextRequest" + REQUEST_GET_DETECT_RUN = "GetDetectRunRequest" + REQUEST_TOKENIZE = "Tokenize" + + // JSON keys for request/response bodies + JSON_KEY_BODY = "Body" + JSON_KEY_RECORDS = "records" + JSON_KEY_TOKENS = "tokens" + JSON_KEY_REQUEST_INDEX = "request_index" + JSON_KEY_TOKENIZED_DATA = "tokenized_data" + + // SDK and token generation + SDK_ISSUER = "sdk" + SIGNED_TOKEN_PREFIX = "signed_token_" + + // SDK metadata keys for CreateJsonMetadata + SDK_METADATA_KEY_NAME_VERSION = "sdk_name_version" + SDK_METADATA_KEY_DEVICE_MODEL = "sdk_client_device_model" + SDK_METADATA_KEY_OS_DETAILS = "sdk_client_os_details" + SDK_METADATA_KEY_RUNTIME_DETAILS = "sdk_runtime_details" + + // Magic numbers + API_KEY_LENGTH = 42 ) diff --git a/v2/internal/helpers/helpers.go b/v2/internal/helpers/helpers.go index 3b7d31c..b3e3c5d 100644 --- a/v2/internal/helpers/helpers.go +++ b/v2/internal/helpers/helpers.go @@ -19,10 +19,10 @@ import ( "github.com/skyflowapi/skyflow-go/v2/internal/generated/core" - vaultapis "github.com/skyflowapi/skyflow-go/v2/internal/generated" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt" constants "github.com/skyflowapi/skyflow-go/v2/internal/constants" internal "github.com/skyflowapi/skyflow-go/v2/internal/generated" + vaultapis "github.com/skyflowapi/skyflow-go/v2/internal/generated" internalAuthApi "github.com/skyflowapi/skyflow-go/v2/internal/generated/authentication" "github.com/skyflowapi/skyflow-go/v2/internal/generated/option" common "github.com/skyflowapi/skyflow-go/v2/utils/common" @@ -125,23 +125,23 @@ func GetFormattedBatchInsertRecord(record interface{}, requestIndex int) (map[st return nil, skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.INVALID_RESPONSE) } - // Extract relevant data from "Body" - body, bodyExists := bodyObject["Body"].(map[string]interface{}) + // Extract relevant data from Body + body, bodyExists := bodyObject[constants.JSON_KEY_BODY].(map[string]interface{}) if !bodyExists { return nil, skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.INVALID_RESPONSE) } // Handle extracted data - if records, ok := body["records"].([]interface{}); ok { + if records, ok := body[constants.JSON_KEY_RECORDS].([]interface{}); ok { for _, rec := range records { recordObject, isMap := rec.(map[string]interface{}) if !isMap { continue } - if skyflowID, exists := recordObject["skyflow_id"].(string); exists { - insertRecord["skyflow_id"] = skyflowID + if skyflowID, exists := recordObject[constants.SKYFLOW_ID].(string); exists { + insertRecord[constants.SKYFLOW_ID] = skyflowID } - if tokens, exists := recordObject["tokens"].(map[string]interface{}); exists { + if tokens, exists := recordObject[constants.JSON_KEY_TOKENS].(map[string]interface{}); exists { for key, value := range tokens { insertRecord[key] = value } @@ -149,16 +149,16 @@ func GetFormattedBatchInsertRecord(record interface{}, requestIndex int) (map[st } } - if errorField, exists := body["error"].(string); exists { - insertRecord["error"] = errorField + if errorField, exists := body[constants.ERROR_KEY_ERROR].(string); exists { + insertRecord[constants.ERROR_KEY_ERROR] = errorField } - insertRecord["request_index"] = requestIndex + insertRecord[constants.JSON_KEY_REQUEST_INDEX] = requestIndex return insertRecord, nil } func GetFormattedBulkInsertRecord(record vaultapis.V1RecordMetaProperties) map[string]interface{} { insertRecord := make(map[string]interface{}) - insertRecord["skyflow_id"] = *record.GetSkyflowId() + insertRecord[constants.SKYFLOW_ID] = *record.GetSkyflowId() tokensMap := record.GetTokens() if len(tokensMap) > 0 { @@ -179,7 +179,7 @@ func GetFormattedQueryRecord(record vaultapis.V1FieldRecords) map[string]interfa for key, value := range record.Tokens { tokens[key] = value } - queryRecord["tokenized_data"] = tokens + queryRecord[constants.JSON_KEY_TOKENIZED_DATA] = tokens } } return queryRecord @@ -302,11 +302,11 @@ func GetFileForFileUpload(request common.FileUploadRequest) (*os.File, error) { if request.Base64 != "" { data, err := base64.StdEncoding.DecodeString(request.Base64) if err != nil { - return nil, fmt.Errorf("failed to decode base64: %w", err) + return nil, fmt.Errorf(logs.FAILED_TO_DECODE_BASE64, err) } file, err := os.Create(request.FileName) if err != nil { - return nil, fmt.Errorf("failed to create file: %w", err) + return nil, fmt.Errorf(logs.FAILED_TO_CREATE_FILE, err) } // Write data _, err = file.Write(data) @@ -346,9 +346,9 @@ func GetSignedDataTokens(credKeys map[string]interface{}, options common.SignedD // Helper for extracting credentials func GetCredentialParams(credKeys map[string]interface{}) (string, string, string, *skyflowError.SkyflowError) { - clientID, ok := credKeys["clientID"].(string) - tokenURI, ok2 := credKeys["tokenURI"].(string) - keyID, ok3 := credKeys["keyID"].(string) + clientID, ok := credKeys[constants.CRED_KEY_CLIENT_ID].(string) + tokenURI, ok2 := credKeys[constants.CRED_KEY_TOKEN_URI].(string) + keyID, ok3 := credKeys[constants.CRED_KEY_KEY_ID].(string) if !ok || !ok2 || !ok3 { logger.Error(logs.INVALID_CREDENTIALS_FILE_FORMAT) return "", "", "", skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.INVALID_CREDENTIALS) @@ -358,23 +358,31 @@ func GetCredentialParams(credKeys map[string]interface{}) (string, string, strin // Generate signed tokens func GenerateSignedDataTokensHelper(clientID, keyID string, pvtKey *rsa.PrivateKey, options common.SignedDataTokensOptions, tokenURI string) ([]common.SignedDataTokensResponse, *skyflowError.SkyflowError) { + if options.TokenURI != "" { + if !isValidURL(options.TokenURI) { + logger.Error(logs.INVALID_TOKEN_URI) + return nil, skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.INVALID_TOKEN_URI) + } + tokenURI = options.TokenURI + } + var responseArray []common.SignedDataTokensResponse for _, token := range options.DataTokens { claims := jwt.MapClaims{ - "iss": "sdk", - "key": keyID, - "aud": tokenURI, - "iat": time.Now().Unix(), - "sub": clientID, - "tok": token, + constants.JWT_CLAIM_ISS: constants.SDK_ISSUER, + constants.JWT_CLAIM_KEY: keyID, + constants.JWT_CLAIM_AUD: tokenURI, + constants.JWT_CLAIM_IAT: time.Now().Unix(), + constants.JWT_CLAIM_SUB: clientID, + constants.JWT_CLAIM_TOK: token, } if options.TimeToLive > 0 { - claims["exp"] = time.Now().Add(time.Duration(options.TimeToLive) * time.Second).Unix() + claims[constants.JWT_CLAIM_EXP] = time.Now().Add(time.Duration(options.TimeToLive) * time.Second).Unix() } else { - claims["exp"] = time.Now().Add(time.Duration(60) * time.Second).Unix() + claims[constants.JWT_CLAIM_EXP] = time.Now().Add(time.Duration(60) * time.Second).Unix() } if options.Ctx != "" { - claims["ctx"] = options.Ctx + claims[constants.JWT_CLAIM_CTX] = options.Ctx } tokenString, err := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(pvtKey) @@ -382,14 +390,14 @@ func GenerateSignedDataTokensHelper(clientID, keyID string, pvtKey *rsa.PrivateK logger.Error(logs.PARSE_JWT_PAYLOAD) return nil, skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, fmt.Sprintf(skyflowError.ERROR_OCCURRED+"%v", err)) } - responseArray = append(responseArray, common.SignedDataTokensResponse{Token: token, SignedToken: "signed_token_" + tokenString}) + responseArray = append(responseArray, common.SignedDataTokensResponse{Token: token, SignedToken: constants.SIGNED_TOKEN_PREFIX + tokenString}) } logger.Info(logs.GENERATE_SIGNED_DATA_TOKEN_SUCCESS) return responseArray, nil } func GetPrivateKey(credKeys map[string]interface{}) (*rsa.PrivateKey, *skyflowError.SkyflowError) { - privateKeyStr, ok := credKeys["privateKey"].(string) + privateKeyStr, ok := credKeys[constants.CRED_KEY_PRIVATE_KEY].(string) if !ok { logger.Error(logs.PRIVATE_KEY_NOT_FOUND) return nil, skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.MISSING_PRIVATE_KEY) @@ -404,7 +412,7 @@ func ParsePrivateKey(pemKey string) (*rsa.PrivateKey, *skyflowError.SkyflowError logger.Error(logs.JWT_INVALID_FORMAT) return nil, skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.JWT_INVALID_FORMAT) } - if privPem.Type != "PRIVATE KEY" { + if privPem.Type != constants.PRIVATE_KEY_PEM_TYPE { logger.Error(fmt.Sprintf(logs.PRIVATE_KEY_TYPE, privPem.Type)) return nil, skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.JWT_INVALID_FORMAT) } @@ -428,9 +436,17 @@ func ParsePrivateKey(pemKey string) (*rsa.PrivateKey, *skyflowError.SkyflowError var GetBaseURLHelper = GetBaseURL +func isValidURL(urlStr string) bool { + parsedUrl, err := url.Parse(urlStr) + if err != nil { + return false + } + return parsedUrl.Scheme == constants.HTTPS_PROTOCOL && parsedUrl.Host != "" +} + // GenerateBearerTokenHelper helper functions func GenerateBearerTokenHelper(credKeys map[string]interface{}, options common.BearerTokenOptions) (*internal.V1GetAuthTokenResponse, *skyflowError.SkyflowError) { - privateKey := credKeys["privateKey"] + privateKey := credKeys[constants.CRED_KEY_PRIVATE_KEY] if privateKey == nil { logger.Error(fmt.Sprintf(logs.PRIVATE_KEY_NOT_FOUND)) return nil, skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.MISSING_PRIVATE_KEY) @@ -439,17 +455,26 @@ func GenerateBearerTokenHelper(credKeys map[string]interface{}, options common.B if err1 != nil { return nil, err1 } - clientID, ok := credKeys["clientID"].(string) + clientID, ok := credKeys[constants.CRED_KEY_CLIENT_ID].(string) if !ok { logger.Error(fmt.Sprintf(logs.CLIENT_ID_NOT_FOUND)) return nil, skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.MISSING_CLIENT_ID) } - tokenURI, ok1 := credKeys["tokenURI"].(string) - if !ok1 { - logger.Error(fmt.Sprintf(logs.TOKEN_URI_NOT_FOUND)) - return nil, skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.MISSING_TOKEN_URI) + tokenURI := options.TokenURI + if tokenURI != "" { + if !isValidURL(tokenURI) { + logger.Error(logs.INVALID_TOKEN_URI) + return nil, skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.INVALID_TOKEN_URI) + } + } else { + var ok1 bool + tokenURI, ok1 = credKeys[constants.CRED_KEY_TOKEN_URI].(string) + if !ok1 { + logger.Error(fmt.Sprintf(logs.TOKEN_URI_NOT_FOUND)) + return nil, skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.MISSING_TOKEN_URI) + } } - keyID, ok2 := credKeys["keyID"].(string) + keyID, ok2 := credKeys[constants.CRED_KEY_KEY_ID].(string) if !ok2 { logger.Error(fmt.Sprintf(logs.KEY_ID_NOT_FOUND)) return nil, skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.MISSING_KEY_ID) @@ -501,7 +526,7 @@ func GetScopeUsingRoles(roles []*string) string { } func GetBaseURL(urlStr string) (string, *skyflowError.SkyflowError) { parsedUrl, err := url.Parse(urlStr) - if err != nil || parsedUrl.Scheme != "https" || parsedUrl.Host == "" { + if err != nil || parsedUrl.Scheme != constants.HTTPS_PROTOCOL || parsedUrl.Host == "" { logger.Error(logs.INVALID_TOKEN_URI) return "", skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.INVALID_TOKEN_URI) // return error if URL parsing fails } @@ -512,19 +537,19 @@ func GetBaseURL(urlStr string) (string, *skyflowError.SkyflowError) { func GetSignedBearerUserToken(clientID, keyID, tokenURI string, pvtKey *rsa.PrivateKey, options common.BearerTokenOptions) (string, *skyflowError.SkyflowError) { token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ - "iss": clientID, - "key": keyID, - "aud": tokenURI, - "sub": clientID, - "exp": time.Now().Add(60 * time.Minute).Unix(), + constants.JWT_CLAIM_ISS: clientID, + constants.JWT_CLAIM_KEY: keyID, + constants.JWT_CLAIM_AUD: tokenURI, + constants.JWT_CLAIM_SUB: clientID, + constants.JWT_CLAIM_EXP: time.Now().Add(60 * time.Minute).Unix(), }) if options.Ctx != "" { - token.Claims.(jwt.MapClaims)["ctx"] = options.Ctx + token.Claims.(jwt.MapClaims)[constants.JWT_CLAIM_CTX] = options.Ctx } var err error signedToken, err := token.SignedString(pvtKey) if err != nil { - logger.Error(fmt.Sprintf("%s", "unable to parse jwt payload")) + logger.Error(fmt.Sprintf("%s", logs.PARSE_JWT_PAYLOAD)) return "", skyflowError.NewSkyflowError(skyflowError.SERVER, fmt.Sprintf(skyflowError.UNKNOWN_ERROR, err)) } return signedToken, nil @@ -536,7 +561,7 @@ func GetPrivateKeyFromPem(pemKey string) (*rsa.PrivateKey, *skyflowError.Skyflow logger.Error(fmt.Sprintf(logs.JWT_INVALID_FORMAT)) return nil, skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.JWT_INVALID_FORMAT) } - if privPem.Type != "PRIVATE KEY" { + if privPem.Type != constants.PRIVATE_KEY_PEM_TYPE { logger.Error(logs.JWT_INVALID_FORMAT) return nil, skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.JWT_INVALID_FORMAT) } @@ -561,16 +586,16 @@ func GetPrivateKeyFromPem(pemKey string) (*rsa.PrivateKey, *skyflowError.Skyflow func CreateJsonMetadata() string { // Create a map to hold the key-value pairs data := map[string]string{ - "sdk_name_version": fmt.Sprintf("%s@%s", constants.SDK_NAME, constants.SDK_VERSION), - "sdk_client_device_model": string(runtime.GOOS), - "sdk_client_os_details": fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH), - "sdk_runtime_details": runtime.Version(), + constants.SDK_METADATA_KEY_NAME_VERSION: fmt.Sprintf("%s@%s", constants.SDK_NAME, constants.SDK_VERSION), + constants.SDK_METADATA_KEY_DEVICE_MODEL: string(runtime.GOOS), + constants.SDK_METADATA_KEY_OS_DETAILS: fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH), + constants.SDK_METADATA_KEY_RUNTIME_DETAILS: runtime.Version(), } // Marshal the map into JSON format jsonData, err := json.Marshal(data) if err != nil { - logger.Debug("failed for marshalling json data in createJSONMetadata()") + logger.Debug(logs.FAILED_TO_MARSHALL_JSON_METADATA) return "" } return string(jsonData) @@ -592,8 +617,8 @@ func GetHeader(err error) (http.Header, bool) { } func GetSkyflowID(data map[string]interface{}) (string, bool) { - if id, ok := data["skyflow_id"].(string); ok { + if id, ok := data[constants.SKYFLOW_ID].(string); ok { return id, true } return "", false -} \ No newline at end of file +} diff --git a/v2/internal/helpers/helpers_test.go b/v2/internal/helpers/helpers_test.go index 5c499f5..de6efd9 100644 --- a/v2/internal/helpers/helpers_test.go +++ b/v2/internal/helpers/helpers_test.go @@ -336,6 +336,89 @@ MIIBAAIBADANINVALIDKEY== }) }) + Context("GenerateSignedDataTokensHelper", func() { + var ( + credKeys map[string]interface{} + options common.SignedDataTokensOptions + response []common.SignedDataTokensResponse + err *SkyflowError + ) + + BeforeEach(func() { + credKeys = getValidCreds() + options = common.SignedDataTokensOptions{ + DataTokens: []string{"testToken1", "testToken2"}, + TimeToLive: 3600, + Ctx: "testContext", + } + }) + + Context("When tokenUri is provided in options", func() { + It("should use the tokenUri from options if valid", func() { + credKeys = getValidCreds() + options.TokenURI = "https://valid-token-uri.com" + response, err = GenerateSignedDataTokensHelper( + credKeys["clientID"].(string), + credKeys["keyID"].(string), + getValidPrivateKey(), + options, + "https://default-uri.com", + ) + Expect(err).Should(BeNil()) + Expect(response).Should(HaveLen(2)) + }) + + It("should return error if tokenUri in options is invalid", func() { + options.TokenURI = "http://invalid-uri.com" + response, err = GenerateSignedDataTokensHelper("client123", "key456", nil, options, "https://default-uri.com") + Expect(err).ShouldNot(BeNil()) + Expect(err.GetCode()).Should(Equal("Code: 400")) + Expect(err.GetMessage()).Should(ContainSubstring(INVALID_TOKEN_URI)) + Expect(response).Should(BeNil()) + }) + + It("should return error if tokenUri in options is malformed", func() { + options.TokenURI = "not-a-valid-url" + response, err = GenerateSignedDataTokensHelper("client123", "key456", nil, options, "https://default-uri.com") + Expect(err).ShouldNot(BeNil()) + Expect(err.GetCode()).Should(Equal("Code: 400")) + Expect(err.GetMessage()).Should(ContainSubstring(INVALID_TOKEN_URI)) + Expect(response).Should(BeNil()) + }) + + It("should use default tokenUri if options.TokenURI is empty", func() { + credKeys = getValidCreds() + options.TokenURI = "" // Empty tokenUri + response, err = GenerateSignedDataTokensHelper( + credKeys["clientID"].(string), + credKeys["keyID"].(string), + getValidPrivateKey(), + options, + "https://default-uri.com", + ) + Expect(err).Should(BeNil()) + Expect(response).Should(HaveLen(2)) + }) + }) + + Context("When tokenUri is provided and valid", func() { + It("should return signed data tokens successfully with valid tokenUri", func() { + credKeys = getValidCreds() + options.TokenURI = "https://valid-token-uri.com" + response, err = GenerateSignedDataTokensHelper( + credKeys["clientID"].(string), + credKeys["keyID"].(string), + getValidPrivateKey(), + options, + "https://ignored-default-uri.com", + ) + Expect(err).Should(BeNil()) + Expect(response).Should(HaveLen(2)) + Expect(response[0].Token).Should(Equal("testToken1")) + Expect(response[0].SignedToken).Should(ContainSubstring("signed_token_")) + }) + }) + }) Context("GetScopeUsingRoles", func() { // Test case 1: roles is nil It("should return an empty string when roles is nil", func() { @@ -520,6 +603,70 @@ MIIBAAIBADANINVALIDKEY== }) }) + + }) + Context("GenerateBearerTokenHelper with tokenUri in options", func() { + var ( + credKeys map[string]interface{} + options common.BearerTokenOptions + ) + + BeforeEach(func() { + credKeys = getValidCreds() + options = common.BearerTokenOptions{ + Ctx: "testContext", + RoleIDs: []string{"roleid1"}, + } + }) + + It("should use the tokenUri from options if valid", func() { + options.TokenURI = "https://valid-token-uri.com" + originalGetBaseURLHelper := GetBaseURLHelper + + defer func() { GetBaseURLHelper = originalGetBaseURLHelper }() + + GetBaseURLHelper = func(urlStr string) (string, *SkyflowError) { + return "https://valid-token-uri.com", nil + } + + _, err := GenerateBearerTokenHelper(credKeys, options) + Expect(err).ShouldNot(BeNil()) + }) + + It("should return error if tokenUri in options is invalid (http instead of https)", func() { + options.TokenURI = "http://invalid-uri.com" + response, err := GenerateBearerTokenHelper(credKeys, options) + + Expect(err).ShouldNot(BeNil()) + Expect(response).Should(BeNil()) + Expect(err.GetCode()).Should(Equal("Code: 400")) + Expect(err.GetMessage()).Should(ContainSubstring(INVALID_TOKEN_URI)) + }) + + It("should return error if tokenUri in options is malformed", func() { + options.TokenURI = "not-a-valid-url" + response, err := GenerateBearerTokenHelper(credKeys, options) + + Expect(err).ShouldNot(BeNil()) + Expect(response).Should(BeNil()) + Expect(err.GetCode()).Should(Equal("Code: 400")) + Expect(err.GetMessage()).Should(ContainSubstring(INVALID_TOKEN_URI)) + }) + + It("should use credKeys tokenUri when options.TokenURI is empty", func() { + options.TokenURI = "" + originalGetBaseURLHelper := GetBaseURLHelper + + defer func() { GetBaseURLHelper = originalGetBaseURLHelper }() + + GetBaseURLHelper = func(urlStr string) (string, *SkyflowError) { + Expect(urlStr).Should(ContainSubstring(credKeys["tokenURI"].(string))) + return "https://valid-uri.com", nil + } + + _, err := GenerateBearerTokenHelper(credKeys, options) + Expect(err).ShouldNot(BeNil()) + }) }) Context("GetHeader", func() { @@ -884,3 +1031,9 @@ func getValidCreds() map[string]interface{} { _ = json.Unmarshal([]byte(pvtKey), &credMap) return credMap } + +func getValidPrivateKey() *rsa.PrivateKey { + credKeys := getValidCreds() + pvtKey, _ := GetPrivateKey(credKeys) + return pvtKey +} diff --git a/v2/internal/validation/validations.go b/v2/internal/validation/validations.go index 6669599..5bcf744 100644 --- a/v2/internal/validation/validations.go +++ b/v2/internal/validation/validations.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" + constants "github.com/skyflowapi/skyflow-go/v2/internal/constants" vaultapis "github.com/skyflowapi/skyflow-go/v2/internal/generated" "github.com/skyflowapi/skyflow-go/v2/internal/helpers" "github.com/skyflowapi/skyflow-go/v2/utils/common" @@ -19,13 +20,13 @@ import ( // ValidateDeidentifyTextRequest validates the required fields of DeidentifyTextRequest. func ValidateDeidentifyTextRequest(req common.DeidentifyTextRequest) *skyflowError.SkyflowError { if strings.TrimSpace(req.Text) == "" { - logger.Error(fmt.Sprintf(logs.INVALID_TEXT_IN_DEIDENTIFY, "DeidentifyTextRequest")) + logger.Error(fmt.Sprintf(logs.INVALID_TEXT_IN_DEIDENTIFY, constants.REQUEST_DEIDENTIFY_TEXT)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.INVALID_TEXT_IN_DEIDENTIFY) } // Validate entities if len(req.Entities) > 0 { - if err := validateEntities(req.Entities, "text"); err != nil { + if err := validateEntities(req.Entities, constants.ENTITY_TYPE_TEXT); err != nil { return err } } @@ -39,14 +40,14 @@ func ValidateDeidentifyTextRequest(req common.DeidentifyTextRequest) *skyflowErr // Validate EntityOnly tokens if len(req.TokenFormat.EntityOnly) > 0 { - if err := validateEntities(req.TokenFormat.EntityOnly, "entity_only"); err != nil { + if err := validateEntities(req.TokenFormat.EntityOnly, constants.ENTITY_TYPE_ENTITY_ONLY); err != nil { return err } } // Validate VaultToken entities if len(req.TokenFormat.VaultToken) > 0 { - if err := validateEntities(req.TokenFormat.VaultToken, "vault_token"); err != nil { + if err := validateEntities(req.TokenFormat.VaultToken, constants.ENTITY_TYPE_VAULT_TOKEN); err != nil { return err } } @@ -61,27 +62,27 @@ func ValidateDeidentifyTextRequest(req common.DeidentifyTextRequest) *skyflowErr // ValidateReidentifyTextRequest validates the required fields of ReidentifyTextRequest. func ValidateReidentifyTextRequest(req common.ReidentifyTextRequest) *skyflowError.SkyflowError { if strings.TrimSpace(req.Text) == "" { - logger.Error(fmt.Sprintf(logs.INVALID_TEXT_IN_REIDENTIFY, "ReidentifyTextRequest")) + logger.Error(fmt.Sprintf(logs.INVALID_TEXT_IN_REIDENTIFY, constants.REQUEST_REIDENTIFY_TEXT)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.INVALID_TEXT_IN_REIDENTIFY) } // Validate RedactedEntities if len(req.RedactedEntities) > 0 { - if err := validateEntities(req.RedactedEntities, "redacted"); err != nil { + if err := validateEntities(req.RedactedEntities, constants.ENTITY_TYPE_REDACTED); err != nil { return err } } // Validate MaskedEntities if len(req.MaskedEntities) > 0 { - if err := validateEntities(req.MaskedEntities, "masked"); err != nil { + if err := validateEntities(req.MaskedEntities, constants.ENTITY_TYPE_MASKED); err != nil { return err } } // Validate PlainTextEntities if len(req.PlainTextEntities) > 0 { - if err := validateEntities(req.PlainTextEntities, "plain_text"); err != nil { + if err := validateEntities(req.PlainTextEntities, constants.ENTITY_TYPE_PLAIN_TEXT); err != nil { return err } } @@ -92,31 +93,31 @@ func ValidateReidentifyTextRequest(req common.ReidentifyTextRequest) *skyflowErr func validateEntities(entities []common.DetectEntities, entityType string) *skyflowError.SkyflowError { for _, entity := range entities { // add entity type validation - if entityType == "redacted" { + if entityType == constants.ENTITY_TYPE_REDACTED { if _, err := vaultapis.NewFormatRedactedItemFromString(string(entity)); err != nil { return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, fmt.Sprintf(skyflowError.INVALID_ENTITY_TYPE, entity)) } - } else if entityType == "masked" { + } else if entityType == constants.ENTITY_TYPE_MASKED { if _, err := vaultapis.NewFormatMaskedItemFromString(string(entity)); err != nil { return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, fmt.Sprintf(skyflowError.INVALID_ENTITY_TYPE, entity)) } - } else if entityType == "plain_text" { + } else if entityType == constants.ENTITY_TYPE_PLAIN_TEXT { if _, err := vaultapis.NewFormatPlaintextItemFromString(string(entity)); err != nil { return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, fmt.Sprintf(skyflowError.INVALID_ENTITY_TYPE, entity)) } - } else if entityType == "text" { + } else if entityType == constants.ENTITY_TYPE_TEXT { if _, err := vaultapis.NewDeidentifyStringRequestEntityTypesItemFromString(string(entity)); err != nil { return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, fmt.Sprintf(skyflowError.INVALID_ENTITY_TYPE, entity)) } - } else if entityType == "entity_only" { + } else if entityType == constants.ENTITY_TYPE_ENTITY_ONLY { if _, err := vaultapis.NewTokenTypeMappingEntityOnlyItemFromString(string(entity)); err != nil { return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, fmt.Sprintf(skyflowError.INVALID_ENTITY_TYPE, entity)) } - } else if entityType == "vault_token" { + } else if entityType == constants.ENTITY_TYPE_VAULT_TOKEN { if _, err := vaultapis.NewTokenTypeMappingVaultTokenItemFromString(string(entity)); err != nil { return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, fmt.Sprintf(skyflowError.INVALID_ENTITY_TYPE, entity)) } - } else if entityType == "entity_unique_counter" { + } else if entityType == constants.ENTITY_TYPE_ENTITY_UNIQUE_CTR { if _, err := vaultapis.NewTokenTypeMappingEntityUnqCounterItemFromString(string(entity)); err != nil { return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, fmt.Sprintf(skyflowError.INVALID_ENTITY_TYPE, entity)) } @@ -143,7 +144,7 @@ func validateTransformations(transformations common.Transformations) *skyflowErr // ValidateGetDetectRunRequest validates the required fields of GetDetectRunRequest. func ValidateGetDetectRunRequest(req common.GetDetectRunRequest) *skyflowError.SkyflowError { if strings.TrimSpace(req.RunId) == "" { - logger.Error(fmt.Sprintf(logs.EMPTY_RUN_ID, "GetDetectRunRequest")) + logger.Error(fmt.Sprintf(logs.EMPTY_RUN_ID, constants.REQUEST_GET_DETECT_RUN)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_RUN_ID) } return nil @@ -151,9 +152,7 @@ func ValidateGetDetectRunRequest(req common.GetDetectRunRequest) *skyflowError.S // ValidateDeidentifyFileRequest validates the required fields of DeidentifyFileRequest. func ValidateDeidentifyFileRequest(req common.DeidentifyFileRequest) *skyflowError.SkyflowError { - tag := "DeidentifyFileRequest" - - // Validate required fields + tag := constants.REQUEST_DEIDENTIFY_FILE // Validate if file or filepath is provided if req.File.File == nil && req.File.FilePath == "" { logger.Error(fmt.Sprintf(logs.EMPTY_FILE_AND_FILE_PATH_IN_DEIDENTIFY_FILE, tag)) @@ -207,7 +206,7 @@ func ValidateDeidentifyFileRequest(req common.DeidentifyFileRequest) *skyflowErr // Validate entities if len(req.Entities) > 0 { - if err := validateEntities(req.Entities, "entities"); err != nil { + if err := validateEntities(req.Entities, constants.ENTITY_TYPE_ENTITIES); err != nil { return err } } @@ -229,13 +228,13 @@ func ValidateDeidentifyFileRequest(req common.DeidentifyFileRequest) *skyflowErr } if len(req.TokenFormat.EntityOnly) > 0 { - if err := validateEntities(req.TokenFormat.EntityOnly, "entity_only"); err != nil { + if err := validateEntities(req.TokenFormat.EntityOnly, constants.REQUEST_ENTITY_ONLY); err != nil { return err } } if len(req.TokenFormat.EntityUniqueCounter) > 0 { - if err := validateEntities(req.TokenFormat.EntityUniqueCounter, "entity_unique_counter"); err != nil { + if err := validateEntities(req.TokenFormat.EntityUniqueCounter, constants.ENTITY_TYPE_ENTITY_UNIQUE_CTR); err != nil { return err } } @@ -271,7 +270,7 @@ func ValidateDeidentifyFileRequest(req common.DeidentifyFileRequest) *skyflowErr // Helper function to check directory write permission func checkDirWritePermission(dir string) error { // Try to create a temporary file - tempFile := filepath.Join(dir, ".permission_check") + tempFile := filepath.Join(dir, constants.PERMISSION_CHECK_FILE) file, err := os.Create(tempFile) if err != nil { return err @@ -336,7 +335,7 @@ func ValidateFilePermissions(filePath string, file *os.File) *skyflowError.Skyfl func ValidateInsertRequest(request common.InsertRequest, options common.InsertOptions) *skyflowError.SkyflowError { // Validate table - tag := "Insert" + tag := constants.REQUEST_INSERT if request.Table == "" { logger.Error(fmt.Sprintf(logs.EMPTY_TABLE, tag)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.TABLE_KEY_ERROR) @@ -362,11 +361,8 @@ func ValidateInsertRequest(request common.InsertRequest, options common.InsertOp // Validate each key-value pair in values for _, valueMap := range request.Values { - for key, value := range valueMap { - if value == nil || value == "" { - logger.Error(fmt.Sprintf(logs.EMPTY_OR_NULL_VALUE_IN_VALUES, tag, key)) - return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_VALUE_IN_VALUES) - } else if key == "" { + for key := range valueMap { + if key == "" { logger.Error(fmt.Sprintf(logs.EMPTY_OR_NULL_KEY_IN_VALUES, tag)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_KEY_IN_VALUES) } @@ -407,7 +403,7 @@ func ValidateInsertRequest(request common.InsertRequest, options common.InsertOp } func ValidateTokensForInsertRequest(tokens []map[string]interface{}, values []map[string]interface{}, mode common.BYOT) *skyflowError.SkyflowError { - tag := "insert" + tag := constants.REQUEST_INSERT_LOWER if tokens == nil || len(tokens) == 0 { if mode == common.ENABLE || mode == common.ENABLE_STRICT { logger.Error(fmt.Sprintf(logs.EMPTY_TOKENS, tag)) @@ -573,13 +569,20 @@ func ValidateCredentials(credentials common.Credentials) *skyflowError.SkyflowEr // API key validation if credentials.ApiKey != "" { // Validate API key format - if len(credentials.ApiKey) != 42 || !strings.Contains(credentials.ApiKey, "sky-") { + if len(credentials.ApiKey) != constants.API_KEY_LENGTH || !strings.Contains(credentials.ApiKey, constants.API_KEY_PREFIX) { logger.Error(logs.INVALID_API_KEY) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.INVALID_API_KEY) } } + if credentials.TokenURI != "" { + if !isValidHTTPSURL(credentials.TokenURI) { + logger.Error(logs.INVALID_TOKEN_URI) + return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.INVALID_TOKEN_URI) + } + } + // Roles validation if credentials.Roles != nil { if len(credentials.Roles) == 0 { @@ -602,12 +605,12 @@ func ValidateInvokeConnectionRequest(request common.InvokeConnectionRequest) *sk // Validate headers if request.Headers != nil { if len(request.Headers) == 0 { - logger.Error(fmt.Sprintf(logs.EMPTY_REQUEST_HEADERS, "InvokeConnectionRequest")) + logger.Error(fmt.Sprintf(logs.EMPTY_REQUEST_HEADERS, constants.REQUEST_INVOKE_CONNECTION)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_REQUEST_HEADER) } for key, value := range request.Headers { if key == "" || value == "" { - logger.Error(fmt.Sprintf(logs.INVALID_REQUEST_HEADERS, "InvokeConnectionRequest")) + logger.Error(fmt.Sprintf(logs.INVALID_REQUEST_HEADERS, constants.REQUEST_INVOKE_CONNECTION)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.INVALID_REQUEST_HEADERS) } } @@ -616,15 +619,15 @@ func ValidateInvokeConnectionRequest(request common.InvokeConnectionRequest) *sk // Validate path parameters if request.PathParams != nil { if len(request.PathParams) == 0 { - logger.Error(fmt.Sprintf(logs.EMPTY_PATH_PARAMS, "InvokeConnectionRequest")) + logger.Error(fmt.Sprintf(logs.EMPTY_PATH_PARAMS, constants.REQUEST_INVOKE_CONNECTION)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_PARAMETERS) } for key, value := range request.PathParams { if key == "" { - logger.Error(fmt.Sprintf(logs.INVALID_PATH_PARAM, "InvokeConnectionRequest")) + logger.Error(fmt.Sprintf(logs.INVALID_PATH_PARAM, constants.REQUEST_INVOKE_CONNECTION)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_PARAMETER_NAME) } else if value == "" { - logger.Error(fmt.Sprintf(logs.INVALID_PATH_PARAM, "InvokeConnectionRequest")) + logger.Error(fmt.Sprintf(logs.INVALID_PATH_PARAM, constants.REQUEST_INVOKE_CONNECTION)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_PARAMETER_VALUE) } @@ -634,21 +637,24 @@ func ValidateInvokeConnectionRequest(request common.InvokeConnectionRequest) *sk // Validate query parameters if request.QueryParams != nil { if len(request.QueryParams) == 0 { - logger.Error(fmt.Sprintf(logs.EMPTY_QUERY_PARAMS, "InvokeConnectionRequest")) + logger.Error(fmt.Sprintf(logs.EMPTY_QUERY_PARAMS, constants.REQUEST_INVOKE_CONNECTION)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_QUERY_PARAM) } for key, value := range request.QueryParams { if key == "" || value == nil || value == "" { - logger.Error(fmt.Sprintf(logs.INVALID_QUERY_PARAM, "InvokeConnectionRequest")) + logger.Error(fmt.Sprintf(logs.INVALID_QUERY_PARAM, constants.REQUEST_INVOKE_CONNECTION)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.INVALID_QUERY_PARAM) } } } // Validate body if request.Body != nil { - if len(request.Body) == 0 { - logger.Error(fmt.Sprintf(logs.EMPTY_REQUEST_BODY, "InvokeConnectionRequest")) - 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 != "" { @@ -662,7 +668,7 @@ func ValidateInvokeConnectionRequest(request common.InvokeConnectionRequest) *sk } func ValidateDetokenizeRequest(request common.DetokenizeRequest) *skyflowError.SkyflowError { - tag := "DetokenizeRequest" + tag := constants.REQUEST_DETOKENIZE if request.DetokenizeData == nil { logger.Error(fmt.Sprintf(logs.DETOKENIZE_DATA_REQUIRED, tag)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.INVALID_DETOKENIZE_DATA) @@ -682,7 +688,7 @@ func ValidateDetokenizeRequest(request common.DetokenizeRequest) *skyflowError.S func ValidateGetRequest(getRequest common.GetRequest, options common.GetOptions) *skyflowError.SkyflowError { // Check if the table is valid - tag := "Get" + tag := constants.REQUEST_GET if getRequest.Table == "" { logger.Error(fmt.Sprintf(logs.EMPTY_TABLE, tag)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_TABLE) @@ -760,7 +766,7 @@ func ValidateGetRequest(getRequest common.GetRequest, options common.GetOptions) } func ValidateDeleteRequest(request common.DeleteRequest) *skyflowError.SkyflowError { - tag := "delete" + tag := constants.REQUEST_DELETE if request.Table == "" { logger.Error(fmt.Sprintf(logs.EMPTY_TABLE, tag)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_TABLE) @@ -798,7 +804,7 @@ func ValidateTokenizeRequest(request []common.TokenizeRequest) *skyflowError.Sky logger.Error(fmt.Sprintf(logs.EMPTY_COLUMN_GROUP_IN_COLUMN_VALUES, index)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_VALUE_IN_COLUMN_VALUES) } else if tokenize.Value == "" { - logger.Error(fmt.Sprintf(logs.EMPTY_OR_NULL_COLUMN_VALUE_IN_COLUMN_VALUES, "Tokenize", index)) + logger.Error(fmt.Sprintf(logs.EMPTY_OR_NULL_COLUMN_VALUE_IN_COLUMN_VALUES, constants.REQUEST_TOKENIZE, index)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_COLUMN_VALUES) } } @@ -807,7 +813,7 @@ func ValidateTokenizeRequest(request []common.TokenizeRequest) *skyflowError.Sky } func ValidateUpdateRequest(request common.UpdateRequest, options common.UpdateOptions) *skyflowError.SkyflowError { - tag := "update" + tag := constants.REQUEST_UPDATE skyflowId, _ := helpers.GetSkyflowID(request.Data) if request.Table == "" { logger.Error(fmt.Sprintf(logs.EMPTY_TABLE, tag)) @@ -821,14 +827,14 @@ func ValidateUpdateRequest(request common.UpdateRequest, options common.UpdateOp return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_DATA) } - for key, data := range request.Data { - if data == "" { - logger.Error(fmt.Sprintf(logs.EMPTY_OR_NULL_VALUE_IN_DATA, tag, key)) - return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_DATA_IN_DATA_KEY) - } else if key == "" { + for key := range request.Data { + if key == "" { logger.Error(fmt.Sprintf(logs.EMPTY_OR_NULL_KEY_IN_DATA, tag)) return skyflowError.NewSkyflowError(skyflowError.INVALID_INPUT_CODE, skyflowError.EMPTY_KEY_IN_DATA) } + if key == constants.SKYFLOW_ID { + continue + } } switch options.TokenMode { case common.DISABLE: @@ -859,7 +865,7 @@ func ValidateUpdateRequest(request common.UpdateRequest, options common.UpdateOp } // ValidateFileUploadRequest validates the required fields of FileUploadRequest. func ValidateFileUploadRequest(req common.FileUploadRequest) *skyflowError.SkyflowError { - tag := "UploadFile" +tag := constants.REQUEST_UPLOAD_FILE if strings.TrimSpace(req.Table) == "" { @@ -917,9 +923,18 @@ func isValidHTTPURL(raw string) bool { return false } - if u.Scheme != "http" && u.Scheme != "https" { + if u.Scheme != constants.HTTP_PROTOCOL && u.Scheme != constants.HTTPS_PROTOCOL { return false } return u.Host != "" } + +func isValidHTTPSURL(raw string) bool { + u, err := url.Parse(raw) + if err != nil { + return false + } + + return u.Scheme == constants.HTTPS_PROTOCOL && u.Host != "" +} diff --git a/v2/internal/validation/validations_test.go b/v2/internal/validation/validations_test.go index 4d8de5d..05b4298 100644 --- a/v2/internal/validation/validations_test.go +++ b/v2/internal/validation/validations_test.go @@ -165,8 +165,7 @@ var _ = Describe("ValidateTokensForInsertRequest", func() { options := common.InsertOptions{} err := ValidateInsertRequest(request, options) - Expect(err).ToNot(BeNil()) - Expect(err.GetMessage()).To(ContainSubstring(errors.EMPTY_VALUE_IN_VALUES)) + Expect(err).To(BeNil()) }) It("should return EMPTY_KEY_IN_VALUES when a key is empty", func() { @@ -601,6 +600,39 @@ var _ = Describe("ValidateTokensForInsertRequest", func() { Expect(err.GetCode()).To(ContainSubstring(string(errors.INVALID_INPUT_CODE))) Expect(err.GetMessage()).To(ContainSubstring(errors.EMPTY_ROLE_IN_ROLES)) }) + + It("should return an error if tokenUri uses http scheme instead of https", func() { + credentials := common.Credentials{ + Token: "token", + TokenURI: "http://invalid-uri.com", + } + err := ValidateCredentials(credentials) + Expect(err).To(HaveOccurred()) + Expect(err.GetCode()).To(ContainSubstring(string(errors.INVALID_INPUT_CODE))) + Expect(err.GetMessage()).To(ContainSubstring(errors.INVALID_TOKEN_URI)) + }) + + It("should return an error if tokenUri is malformed", func() { + credentials := common.Credentials{ + Token: "token", + TokenURI: "not-a-valid-url", + } + err := ValidateCredentials(credentials) + Expect(err).To(HaveOccurred()) + Expect(err.GetCode()).To(ContainSubstring(string(errors.INVALID_INPUT_CODE))) + Expect(err.GetMessage()).To(ContainSubstring(errors.INVALID_TOKEN_URI)) + }) + + It("should return an error if tokenUri has https scheme but no host", func() { + credentials := common.Credentials{ + Token: "token", + TokenURI: "https://", + } + err := ValidateCredentials(credentials) + Expect(err).To(HaveOccurred()) + Expect(err.GetCode()).To(ContainSubstring(string(errors.INVALID_INPUT_CODE))) + Expect(err.GetMessage()).To(ContainSubstring(errors.INVALID_TOKEN_URI)) + }) }) Context("Valid Credentials", func() { @@ -644,6 +676,52 @@ var _ = Describe("ValidateTokensForInsertRequest", func() { err := ValidateCredentials(credentials) Expect(err).To(BeNil()) }) + + It("should return nil for valid tokenUri with https scheme", func() { + credentials := common.Credentials{ + Token: "token", + TokenURI: "https://valid-token-uri.com", + } + err := ValidateCredentials(credentials) + Expect(err).To(BeNil()) + }) + + It("should return nil for valid tokenUri with path and query parameters", func() { + credentials := common.Credentials{ + Token: "token", + TokenURI: "https://valid-token-uri.com/path?param=value", + } + err := ValidateCredentials(credentials) + Expect(err).To(BeNil()) + }) + + It("should return nil for credentials with Path and valid tokenUri", func() { + credentials := common.Credentials{ + Path: "some/path", + TokenURI: "https://valid-token-uri.com", + } + err := ValidateCredentials(credentials) + Expect(err).To(BeNil()) + }) + + It("should return nil for credentials with Token and valid roles and tokenUri", func() { + credentials := common.Credentials{ + Token: "token", + Roles: []string{"admin", "user"}, + TokenURI: "https://valid-token-uri.com", + } + err := ValidateCredentials(credentials) + Expect(err).To(BeNil()) + }) + + It("should return nil for empty tokenUri (optional field)", func() { + credentials := common.Credentials{ + Token: "token", + TokenURI: "", + } + err := ValidateCredentials(credentials) + Expect(err).To(BeNil()) + }) }) }) }) @@ -837,8 +915,7 @@ var _ = Describe("ValidateTokensForInsertRequest", func() { } options := common.UpdateOptions{} err := ValidateUpdateRequest(request, options) - Expect(err).ToNot(BeNil()) - Expect(err.GetMessage()).To(ContainSubstring(errors.EMPTY_DATA_IN_DATA_KEY)) + Expect(err).To(BeNil()) }) It("should return an error if a key is empty in data", func() { diff --git a/v2/internal/vault/controller/connection_controller.go b/v2/internal/vault/controller/connection_controller.go index 21135a8..552b8f7 100644 --- a/v2/internal/vault/controller/connection_controller.go +++ b/v2/internal/vault/controller/connection_controller.go @@ -6,14 +6,15 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "mime/multipart" "net/http" + "net/url" "os" "reflect" "strconv" "strings" + constants "github.com/skyflowapi/skyflow-go/v2/internal/constants" "github.com/skyflowapi/skyflow-go/v2/internal/validation" "github.com/skyflowapi/skyflow-go/v2/serviceaccount" "github.com/skyflowapi/skyflow-go/v2/utils/common" @@ -21,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 { @@ -61,8 +65,8 @@ func setConnectionCredentials(config *common.ConnectionConfig, builderCreds *com // here if builder credentials are available if builderCreds != nil && !isCredentialsEmpty(*builderCreds) { creds = *builderCreds - } else if envCreds := os.Getenv("SKYFLOW_CREDENTIALS"); envCreds != "" { - creds.CredentialsString = os.Getenv("SKYFLOW_CREDENTIALS") + } else if envCreds := os.Getenv(constants.SKYFLOW_CREDENTIALS_ENV); envCreds != "" { + creds.CredentialsString = os.Getenv(constants.SKYFLOW_CREDENTIALS_ENV) } else { return nil, errors.NewSkyflowError(errors.ErrorCodesEnum(errors.INVALID_INPUT_CODE), errors.EMPTY_CREDENTIALS) } @@ -73,7 +77,7 @@ func setConnectionCredentials(config *common.ConnectionConfig, builderCreds *com } func (v *ConnectionController) Invoke(ctx context.Context, request common.InvokeConnectionRequest) (*common.InvokeConnectionResponse, *errors.SkyflowError) { - tag := "Invoke Connection" + tag := constants.REQUEST_INVOKE_CONN logger.Info(logs.INVOKE_CONNECTION_TRIGGERED) // Step 1: Validate Configuration logger.Info(logs.VALIDATING_INVOKE_CONNECTION_REQUEST) @@ -118,18 +122,70 @@ 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{}{ - "request_id": requestId, + constants.REQUEST_ID_KEY: requestId, } 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) } @@ -144,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("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 @@ -189,22 +386,52 @@ 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: pairs[renderKey(parents)] = fmt.Sprintf("%d", data) case reflect.Float32: - pairs[renderKey(parents)] = fmt.Sprintf("%f", data) + pairs[renderKey(parents)] = fmt.Sprintf("%f", data) //nolint:revive case reflect.Float64: - pairs[renderKey(parents)] = fmt.Sprintf("%f", data) + pairs[renderKey(parents)] = fmt.Sprintf("%f", data) //nolint:revive case reflect.Bool: pairs[renderKey(parents)] = fmt.Sprintf("%t", data) case reflect.Map: 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: @@ -218,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) } @@ -228,7 +455,7 @@ func renderKey(parents []interface{}) string { } func detectContentType(headers map[string]string) string { for key, value := range headers { - if strings.ToLower(key) == "content-type" { + if strings.ToLower(key) == constants.HEADER_CONTENT_TYPE { return value } } @@ -255,13 +482,21 @@ func setQueryParams(request *http.Request, queryParams map[string]interface{}) * } func setHeaders(request *http.Request, api ConnectionController, invokeRequest common.InvokeConnectionRequest) { if api.ApiKey != "" { - request.Header.Set("x-skyflow-authorization", api.ApiKey) + request.Header.Set(constants.HEADER_AUTHORIZATION, api.ApiKey) } else { - request.Header.Set("x-skyflow-authorization", api.Token) + request.Header.Set(constants.HEADER_AUTHORIZATION, api.Token) + } + + // 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) } - request.Header.Set("content-type", "application/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) } } @@ -269,21 +504,59 @@ func sendRequest(request *http.Request) (*http.Response, string, error) { response, err := http.DefaultClient.Do(request) requestId := "" if response != nil { - requestId = response.Header.Get("x-request-id") + requestId = response.Header.Get(constants.RESPONSE_HEADER_REQUEST_ID) } if err != nil { return nil, requestId, err } 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..2179752 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"}}]}` @@ -97,7 +109,7 @@ var _ = Describe("Vault controller Test cases", func() { header := http.Header{} header.Set("Content-Type", "application/json") CreateRequestClientFunc = func(v *VaultController) *skyflowError.SkyflowError { - if (v.CustomHeaders != nil ) { + if v.CustomHeaders != nil { for key, value := range v.CustomHeaders { header.Set(key, value) } @@ -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,20 +1448,2061 @@ 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 + }) -}) -var _ = Describe("VaultController", func() { - var vaultController *VaultController + AfterEach(func() { + mockServer.Close() + }) - BeforeEach(func() { - vaultController = &VaultController{ - Config: &VaultConfig{ - Credentials: Credentials{ - Path: "test/path", - }, - }, - } + 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() { + var vaultController *VaultController + + BeforeEach(func() { + vaultController = &VaultController{ + Config: &VaultConfig{ + Credentials: Credentials{ + Path: "test/path", + }, + }, + } + }) + + Describe("GenerateToken", func() { + Context("tokenUri parameter handling", func() { + It("should handle tokenUri when provided with Token (no network call)", func() { + credentials := Credentials{ + Token: "valid-token", + TokenURI: "https://custom-token-uri.com", + } + token, err := GenerateToken(credentials) + Expect(err).To(BeNil()) + Expect(token).ToNot(BeNil()) + Expect(*token).To(Equal("valid-token")) + }) + + It("should handle tokenUri when provided with ApiKey (no network call)", func() { + credentials := Credentials{ + ApiKey: "sky-api-key-1234567890123456789012", + TokenURI: "https://custom-token-uri.com", + } + token, err := GenerateToken(credentials) + Expect(err).To(BeNil()) + Expect(token).ToNot(BeNil()) + Expect(*token).To(Equal("sky-api-key-1234567890123456789012")) + }) + + It("should pass empty tokenUri to BearerTokenOptions when not provided with Path", func() { + credentials := Credentials{ + Path: "../../" + os.Getenv("CRED_FILE_PATH"), + TokenURI: "", + } + token, err := GenerateToken(credentials) + Expect(err).To(BeNil()) + Expect(token).ToNot(BeNil()) + }) + + It("should pass empty tokenUri to BearerTokenOptions when not provided with CredentialsString", func() { + credentials := Credentials{ + CredentialsString: os.Getenv("VALID_CREDS_PVT_KEY"), + TokenURI: "", + } + token, err := GenerateToken(credentials) + Expect(err).To(BeNil()) + Expect(token).ToNot(BeNil()) + }) + + It("should pass roles and context to BearerTokenOptions along with Token and tokenUri", func() { + credentials := Credentials{ + Token: "valid-token", + Roles: []string{"admin", "user"}, + Context: "test-context", + TokenURI: "https://custom-token-uri.com", + } + token, err := GenerateToken(credentials) + Expect(err).To(BeNil()) + Expect(token).ToNot(BeNil()) + Expect(*token).To(Equal("valid-token")) + }) + }) }) Context("SetBearerTokenForVaultController", func() { diff --git a/v2/internal/vault/controller/detect_controller.go b/v2/internal/vault/controller/detect_controller.go index b19b19e..bf82662 100644 --- a/v2/internal/vault/controller/detect_controller.go +++ b/v2/internal/vault/controller/detect_controller.go @@ -211,7 +211,7 @@ func CreateReidentifyTextRequest(request common.ReidentifyTextRequest, config co // RedactedEntities if len(request.RedactedEntities) > 0 { - redactedEntities := CreateEntityTypes(request.RedactedEntities, "redacted").([]vaultapis.FormatRedactedItem) + redactedEntities := CreateEntityTypes(request.RedactedEntities, constants.DETECT_REDACTION_TYPE_REDACTED).([]vaultapis.FormatRedactedItem) if len(redactedEntities) > 0 { payload.Format.Redacted = redactedEntities } @@ -219,7 +219,7 @@ func CreateReidentifyTextRequest(request common.ReidentifyTextRequest, config co // MaskedEntities if len(request.MaskedEntities) > 0 { - maskedEntities := CreateEntityTypes(request.MaskedEntities, "masked").([]vaultapis.FormatMaskedItem) + maskedEntities := CreateEntityTypes(request.MaskedEntities, constants.DETECT_REDACTION_TYPE_MASKED).([]vaultapis.FormatMaskedItem) if len(maskedEntities) > 0 { payload.Format.Masked = maskedEntities } @@ -227,7 +227,7 @@ func CreateReidentifyTextRequest(request common.ReidentifyTextRequest, config co // PlainTextEntities if len(request.PlainTextEntities) > 0 { - plainTextEntities := CreateEntityTypes(request.PlainTextEntities, "plaintext").([]vaultapis.FormatPlaintextItem) + plainTextEntities := CreateEntityTypes(request.PlainTextEntities, constants.DETECT_REDACTION_TYPE_PLAINTEXT).([]vaultapis.FormatPlaintextItem) if len(plainTextEntities) > 0 { payload.Format.Plaintext = plainTextEntities } @@ -238,7 +238,7 @@ func CreateReidentifyTextRequest(request common.ReidentifyTextRequest, config co func CreateTextFileRequest(request *common.DeidentifyFileRequest, base64Content, vaultID string) *vaultapis.DeidentifyFileRequestDeidentifyText { var entityTypes []vaultapis.DeidentifyFileRequestDeidentifyTextEntityTypesItem - if result := CreateEntityTypesRef(request.Entities, "text"); result != nil { + if result := CreateEntityTypesRef(request.Entities, constants.FILE_TYPE_TEXT); result != nil { entityTypes = result.([]vaultapis.DeidentifyFileRequestDeidentifyTextEntityTypesItem) } return &vaultapis.DeidentifyFileRequestDeidentifyText{ @@ -256,7 +256,7 @@ func CreateTextFileRequest(request *common.DeidentifyFileRequest, base64Content, func CreateImageRequest(request *common.DeidentifyFileRequest, base64Content, vaultId, fileExt string) *vaultapis.DeidentifyFileImageRequestDeidentifyImage { var entityTypes []vaultapis.DeidentifyFileImageRequestDeidentifyImageEntityTypesItem - if result := CreateEntityTypesRef(request.Entities, "image"); result != nil { + if result := CreateEntityTypesRef(request.Entities, constants.FILE_TYPE_IMAGE); result != nil { entityTypes = result.([]vaultapis.DeidentifyFileImageRequestDeidentifyImageEntityTypesItem) } return &vaultapis.DeidentifyFileImageRequestDeidentifyImage{ @@ -277,7 +277,7 @@ func CreateImageRequest(request *common.DeidentifyFileRequest, base64Content, va func CreatePdfRequest(request *common.DeidentifyFileRequest, base64Content, vaultID string) *vaultapis.DeidentifyFileDocumentPdfRequestDeidentifyPdf { var entityTypes []vaultapis.DeidentifyFileDocumentPdfRequestDeidentifyPdfEntityTypesItem - if result := CreateEntityTypesRef(request.Entities, "pdf"); result != nil { + if result := CreateEntityTypesRef(request.Entities, constants.FILE_TYPE_PDF); result != nil { entityTypes = result.([]vaultapis.DeidentifyFileDocumentPdfRequestDeidentifyPdfEntityTypesItem) } return &vaultapis.DeidentifyFileDocumentPdfRequestDeidentifyPdf{ @@ -296,7 +296,7 @@ func CreatePdfRequest(request *common.DeidentifyFileRequest, base64Content, vaul func CreatePresentationRequest(request *common.DeidentifyFileRequest, base64Content, vaultID, fileExt string) *vaultapis.DeidentifyFileRequestDeidentifyPresentation { var entityTypes []vaultapis.DeidentifyFileRequestDeidentifyPresentationEntityTypesItem - if result := CreateEntityTypesRef(request.Entities, "ppt"); result != nil { + if result := CreateEntityTypesRef(request.Entities, constants.FILE_TYPE_PPT); result != nil { entityTypes = result.([]vaultapis.DeidentifyFileRequestDeidentifyPresentationEntityTypesItem) } return &vaultapis.DeidentifyFileRequestDeidentifyPresentation{ @@ -314,7 +314,7 @@ func CreatePresentationRequest(request *common.DeidentifyFileRequest, base64Cont func CreateSpreadsheetRequest(request *common.DeidentifyFileRequest, base64Content, vaultID, fileExt string) *vaultapis.DeidentifyFileRequestDeidentifySpreadsheet { var entityTypes []vaultapis.DeidentifyFileRequestDeidentifySpreadsheetEntityTypesItem - if result := CreateEntityTypesRef(request.Entities, "spread").([]vaultapis.DeidentifyFileRequestDeidentifySpreadsheetEntityTypesItem); result != nil { + if result := CreateEntityTypesRef(request.Entities, constants.FILE_TYPE_SPREAD).([]vaultapis.DeidentifyFileRequestDeidentifySpreadsheetEntityTypesItem); result != nil { entityTypes = result } return &vaultapis.DeidentifyFileRequestDeidentifySpreadsheet{ @@ -332,7 +332,7 @@ func CreateSpreadsheetRequest(request *common.DeidentifyFileRequest, base64Conte func CreateDocumentRequest(request *common.DeidentifyFileRequest, base64Content, vaultID, fileExt string) *vaultapis.DeidentifyFileRequestDeidentifyDocument { var entityTypes []vaultapis.DeidentifyFileRequestDeidentifyDocumentEntityTypesItem - if result := CreateEntityTypesRef(request.Entities, "document").([]vaultapis.DeidentifyFileRequestDeidentifyDocumentEntityTypesItem); result != nil { + if result := CreateEntityTypesRef(request.Entities, constants.FILE_TYPE_DOCUMENT).([]vaultapis.DeidentifyFileRequestDeidentifyDocumentEntityTypesItem); result != nil { entityTypes = result } return &vaultapis.DeidentifyFileRequestDeidentifyDocument{ @@ -350,7 +350,7 @@ func CreateDocumentRequest(request *common.DeidentifyFileRequest, base64Content, func CreateStructuredTextRequest(request *common.DeidentifyFileRequest, base64Content, vaultID, fileExt string) *vaultapis.DeidentifyFileRequestDeidentifyStructuredText { var entityTypes []vaultapis.DeidentifyFileRequestDeidentifyStructuredTextEntityTypesItem - if result := CreateEntityTypesRef(request.Entities, "structured").([]vaultapis.DeidentifyFileRequestDeidentifyStructuredTextEntityTypesItem); result != nil { + if result := CreateEntityTypesRef(request.Entities, constants.FILE_TYPE_STRUCTURED).([]vaultapis.DeidentifyFileRequestDeidentifyStructuredTextEntityTypesItem); result != nil { entityTypes = result } return &vaultapis.DeidentifyFileRequestDeidentifyStructuredText{ @@ -369,7 +369,7 @@ func CreateStructuredTextRequest(request *common.DeidentifyFileRequest, base64Co func CreateAudioRequest(request *common.DeidentifyFileRequest, base64Content, vaultID, fileExt string) *vaultapis.DeidentifyFileAudioRequestDeidentifyAudio { var entityTypes []vaultapis.DeidentifyFileAudioRequestDeidentifyAudioEntityTypesItem - if result := CreateEntityTypesRef(request.Entities, "audio").([]vaultapis.DeidentifyFileAudioRequestDeidentifyAudioEntityTypesItem); result != nil { + if result := CreateEntityTypesRef(request.Entities, constants.FILE_TYPE_AUDIO).([]vaultapis.DeidentifyFileAudioRequestDeidentifyAudioEntityTypesItem); result != nil { entityTypes = result } req := &vaultapis.DeidentifyFileAudioRequestDeidentifyAudio{ @@ -400,7 +400,7 @@ func CreateAudioRequest(request *common.DeidentifyFileRequest, base64Content, va func CreateGenericFileRequest(request *common.DeidentifyFileRequest, base64Content, vaultID, fileExtension string) *vaultapis.DeidentifyFileRequest { var entityTypes []vaultapis.DeidentifyFileRequestEntityTypesItem - if result := CreateEntityTypesRef(request.Entities, "generic").([]vaultapis.DeidentifyFileRequestEntityTypesItem); result != nil { + if result := CreateEntityTypesRef(request.Entities, constants.FILE_TYPE_GENERIC).([]vaultapis.DeidentifyFileRequestEntityTypesItem); result != nil { entityTypes = result } return &vaultapis.DeidentifyFileRequest{ @@ -424,63 +424,63 @@ func CreateEntityTypesRef(entities []common.DetectEntities, dataType string) any switch strings.ToLower(dataType) { - case "text": + case constants.FILE_TYPE_TEXT: entityTypes := make([]vaultapis.DeidentifyFileRequestDeidentifyTextEntityTypesItem, len(entities)) for i, e := range entities { entityTypes[i] = vaultapis.DeidentifyFileRequestDeidentifyTextEntityTypesItem(e) } return entityTypes - case "image": + case constants.FILE_TYPE_IMAGE: entityTypes := make([]vaultapis.DeidentifyFileImageRequestDeidentifyImageEntityTypesItem, len(entities)) for i, e := range entities { entityTypes[i] = vaultapis.DeidentifyFileImageRequestDeidentifyImageEntityTypesItem(e) } return entityTypes - case "pdf": + case constants.FILE_TYPE_PDF: entityTypes := make([]vaultapis.DeidentifyFileDocumentPdfRequestDeidentifyPdfEntityTypesItem, len(entities)) for i, e := range entities { entityTypes[i] = vaultapis.DeidentifyFileDocumentPdfRequestDeidentifyPdfEntityTypesItem(e) } return entityTypes - case "ppt": + case constants.FILE_TYPE_PPT: entityTypes := make([]vaultapis.DeidentifyFileRequestDeidentifyPresentationEntityTypesItem, len(entities)) for i, e := range entities { entityTypes[i] = vaultapis.DeidentifyFileRequestDeidentifyPresentationEntityTypesItem(e) } return entityTypes - case "spread": + case constants.FILE_TYPE_SPREAD: entityTypes := make([]vaultapis.DeidentifyFileRequestDeidentifySpreadsheetEntityTypesItem, len(entities)) for i, e := range entities { entityTypes[i] = vaultapis.DeidentifyFileRequestDeidentifySpreadsheetEntityTypesItem(e) } return entityTypes - case "document": + case constants.FILE_TYPE_DOCUMENT: entityTypes := make([]vaultapis.DeidentifyFileRequestDeidentifyDocumentEntityTypesItem, len(entities)) for i, e := range entities { entityTypes[i] = vaultapis.DeidentifyFileRequestDeidentifyDocumentEntityTypesItem(e) } return entityTypes - case "structured": + case constants.FILE_TYPE_STRUCTURED: entityTypes := make([]vaultapis.DeidentifyFileRequestDeidentifyStructuredTextEntityTypesItem, len(entities)) for i, e := range entities { entityTypes[i] = vaultapis.DeidentifyFileRequestDeidentifyStructuredTextEntityTypesItem(e) } return entityTypes - case "audio": + case constants.FILE_TYPE_AUDIO: entityTypes := make([]vaultapis.DeidentifyFileAudioRequestDeidentifyAudioEntityTypesItem, len(entities)) for i, e := range entities { entityTypes[i] = vaultapis.DeidentifyFileAudioRequestDeidentifyAudioEntityTypesItem(e) } return entityTypes - case "generic": + case constants.FILE_TYPE_GENERIC: entityTypes := make([]vaultapis.DeidentifyFileRequestEntityTypesItem, len(entities)) for i, e := range entities { entityTypes[i] = vaultapis.DeidentifyFileRequestEntityTypesItem(e) @@ -498,21 +498,21 @@ func CreateEntityTypes(entities []common.DetectEntities, entityType string) any } switch strings.ToLower(entityType) { - case "redacted": + case constants.ENTITY_TYPE_REDACTED: entityTypes := make([]vaultapis.FormatRedactedItem, len(entities)) for i, e := range entities { entityTypes[i] = vaultapis.FormatRedactedItem(e) } return entityTypes - case "masked": + case constants.ENTITY_TYPE_MASKED: entityTypes := make([]vaultapis.FormatMaskedItem, len(entities)) for i, e := range entities { entityTypes[i] = vaultapis.FormatMaskedItem(e) } return entityTypes - case "plaintext": + case constants.DETECT_REDACTION_TYPE_PLAINTEXT: entityTypes := make([]vaultapis.FormatPlaintextItem, len(entities)) for i, e := range entities { entityTypes[i] = vaultapis.FormatPlaintextItem(e) @@ -833,21 +833,21 @@ func (d *DetectController) processFileByType(ctx context.Context, fileExtension, var apiErr error switch fileExtension { - case "txt": + case constants.FILE_EXTENSION_TXT: apiResponse, apiErr = d.FilesApiClient.DeidentifyText(ctx, CreateTextFileRequest(request, base64Content, d.Config.VaultId)) - case "mp3", "wav": + case constants.FILE_EXTENSION_MP3, constants.FILE_EXTENSION_WAV: apiResponse, apiErr = d.FilesApiClient.DeidentifyAudio(ctx, CreateAudioRequest(request, base64Content, d.Config.VaultId, fileExtension)) - case "pdf": + case constants.FILE_EXTENSION_PDF: apiResponse, apiErr = d.FilesApiClient.DeidentifyPdf(ctx, CreatePdfRequest(request, base64Content, d.Config.VaultId)) - case "jpg", "jpeg", "png", "bmp", "tif", "tiff": + case constants.FILE_EXTENSION_JPG, constants.FILE_EXTENSION_JPEG, constants.FILE_EXTENSION_PNG, constants.FILE_EXTENSION_BMP, constants.FILE_EXTENSION_TIF, constants.FILE_EXTENSION_TIFF: apiResponse, apiErr = d.FilesApiClient.DeidentifyImage(ctx, CreateImageRequest(request, base64Content, d.Config.VaultId, fileExtension)) - case "ppt", "pptx": + case constants.FILE_EXTENSION_PPT, constants.FILE_EXTENSION_PPTX: apiResponse, apiErr = d.FilesApiClient.DeidentifyPresentation(ctx, CreatePresentationRequest(request, base64Content, d.Config.VaultId, fileExtension)) - case "csv", "xls", "xlsx": + case constants.FILE_EXTENSION_CSV, constants.FILE_EXTENSION_XLS, constants.FILE_EXTENSION_XLSX: apiResponse, apiErr = d.FilesApiClient.DeidentifySpreadsheet(ctx, CreateSpreadsheetRequest(request, base64Content, d.Config.VaultId, fileExtension)) - case "doc", "docx": + case constants.FILE_EXTENSION_DOC, constants.FILE_EXTENSION_DOCX: apiResponse, apiErr = d.FilesApiClient.DeidentifyDocument(ctx, CreateDocumentRequest(request, base64Content, d.Config.VaultId, fileExtension)) - case "json", "xml": + case constants.FILE_EXTENSION_JSON, constants.FILE_EXTENSION_XML: apiResponse, apiErr = d.FilesApiClient.DeidentifyStructuredText(ctx, CreateStructuredTextRequest(request, base64Content, d.Config.VaultId, fileExtension)) default: apiResponse, apiErr = d.FilesApiClient.DeidentifyFile(ctx, CreateGenericFileRequest(request, base64Content, d.Config.VaultId, fileExtension)) @@ -918,7 +918,7 @@ func processDeidentifyFileResponse(data *vaultapis.DetectRunsResponse, outputDir return nil } - deidentifyFilePrefix := "processed-" + deidentifyFilePrefix := constants.PROCESSED_PREFIX processedFile := data.Output[0].ProcessedFile if processedFile != nil { decodedBytes, err := base64.StdEncoding.DecodeString(string(*processedFile)) @@ -980,9 +980,9 @@ func parseDeidentifyFileResponse(response *vaultapis.DetectRunsResponse, runID s return nil, errors.New(string(skyflowError.SERVER) + logs.FAILED_TO_DECODE_PROCESSED_FILE) } fileResponse.File = common.FileInfo{ - Name: "deidentified." + string(*firstOutput.ProcessedFileExtension), + Name: constants.DEIDENTIFIED_FILE_PREFIX + string(*firstOutput.ProcessedFileExtension), Size: int64(len(decodedBytes)), - Type: "redacted_file", + Type: constants.FILE_OUTPUT_TYPE_REDACTED_FILE, LastModified: time.Now().UnixMilli(), } fileResponse.FileBase64 = *firstOutput.ProcessedFile @@ -992,7 +992,7 @@ func parseDeidentifyFileResponse(response *vaultapis.DetectRunsResponse, runID s if firstOutput.ProcessedFileType != nil { fileResponse.Type = string(*firstOutput.ProcessedFileType) } else { - fileResponse.Type = "UNKNOWN" + fileResponse.Type = constants.UNKNOWN_STATUS } if firstOutput.ProcessedFileExtension != nil { diff --git a/v2/internal/vault/controller/vault_controller.go b/v2/internal/vault/controller/vault_controller.go index b1f77b5..2b04f22 100644 --- a/v2/internal/vault/controller/vault_controller.go +++ b/v2/internal/vault/controller/vault_controller.go @@ -42,6 +42,9 @@ func GenerateToken(credentials common.Credentials) (*string, *skyflowError.Skyfl if credentials.Context != "" { options.Ctx = credentials.Context } + if credentials.TokenURI != "" { + options.TokenURI = credentials.TokenURI + } switch { case credentials.Token != "": bearerToken = credentials.Token @@ -143,8 +146,8 @@ func setVaultCredentials(config *common.VaultConfig, builderCreds *common.Creden // here if builder credentials are available if builderCreds != nil && !isCredentialsEmpty(*builderCreds) { creds = *builderCreds - } else if envCreds := os.Getenv("SKYFLOW_CREDENTIALS"); envCreds != "" { - creds.CredentialsString = os.Getenv("SKYFLOW_CREDENTIALS") + } else if envCreds := os.Getenv(constants.SKYFLOW_CREDENTIALS_ENV); envCreds != "" { + creds.CredentialsString = os.Getenv(constants.SKYFLOW_CREDENTIALS_ENV) } else { return nil, skyflowError.NewSkyflowError(skyflowError.ErrorCodesEnum(skyflowError.INVALID_INPUT_CODE), skyflowError.EMPTY_CREDENTIALS) } @@ -220,11 +223,11 @@ func (v *VaultController) Insert(ctx context.Context, request common.InsertReque if parseErr != nil { return nil, parseErr } - if formattedRecord["skyflow_id"] != nil { + if formattedRecord[constants.SKYFLOW_ID] != nil { insertedFields = append(insertedFields, formattedRecord) } else { - formattedRecord["RequestId"] = header.Get(constants.REQUEST_KEY) - formattedRecord["HttpCode"] = skyflowError.INVALID_INPUT_CODE + formattedRecord[constants.RESPONSE_KEY_REQUEST_ID] = header.Get(constants.REQUEST_KEY) + formattedRecord[constants.RESPONSE_KEY_HTTP_CODE] = skyflowError.INVALID_INPUT_CODE errors = append(errors, formattedRecord) } } @@ -504,7 +507,7 @@ func (v *VaultController) Update(ctx context.Context, request common.UpdateReque var updatedField map[string]interface{} updatedField = make(map[string]interface{}) updatedField = res - updatedField["skyflowId"] = *id + updatedField[constants.RESPONSE_KEY_SKYFLOW_ID] = *id return &common.UpdateResponse{ UpdatedField: updatedField, Errors: nil, diff --git a/v2/utils/common/common.go b/v2/utils/common/common.go index c94d243..fb530a8 100644 --- a/v2/utils/common/common.go +++ b/v2/utils/common/common.go @@ -167,6 +167,7 @@ type BearerTokenOptions struct { Ctx string RoleIDs []string LogLevel logger.LogLevel + TokenURI string } type SignedDataTokensOptions struct { @@ -174,6 +175,7 @@ type SignedDataTokensOptions struct { TimeToLive int Ctx string LogLevel logger.LogLevel + TokenURI string } type SignedDataTokensResponse struct { @@ -196,6 +198,7 @@ type Credentials struct { CredentialsString string Token string ApiKey string + TokenURI string } type ConnectionConfig struct { ConnectionId string @@ -218,7 +221,7 @@ const ( ) type InvokeConnectionResponse struct { - Data map[string]interface{} + Data interface{} Metadata map[string]interface{} Errors map[string]interface{} } @@ -346,7 +349,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 +360,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 6a1bd12..cdd8c24 100644 --- a/v2/utils/error/message.go +++ b/v2/utils/error/message.go @@ -1,10 +1,11 @@ package errors -import "github.com/skyflowapi/skyflow-go/v2/internal/constants" +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." @@ -35,7 +36,7 @@ const ( MISSING_CLIENT_ID string = internal.SDK_PREFIX + " Initialization failed. Unable to read client ID in credentials. Verify your client ID." MISSING_KEY_ID string = internal.SDK_PREFIX + " Initialization failed. Unable to read key ID in credentials. Verify your key ID." MISSING_TOKEN_URI string = internal.SDK_PREFIX + " Initialization failed. Unable to read token URI in credentials. Verify your token URI." - INVALID_TOKEN_URI string = internal.SDK_PREFIX + " Initialization failed. Token URI in not a valid URL in credentials. Verify your token URI." + INVALID_TOKEN_URI string = internal.SDK_PREFIX + " Initialization failed. Invalid Skyflow credentials. The token URI must be a string and a valid URL." JWT_INVALID_FORMAT string = internal.SDK_PREFIX + " Initialization failed. Invalid private key format. Verify your credentials." INVALID_ALGORITHM string = internal.SDK_PREFIX + " Initialization failed. Invalid algorithm to parse private key. Specify valid algorithm." INVALID_KEY_SPEC string = internal.SDK_PREFIX + " Initialization failed. Unable to parse RSA private key. Verify your credentials." @@ -120,6 +121,7 @@ const ( UNKNOWN_ERROR string = internal.SDK_PREFIX + " Error occurred. %s" INVALID_BYOT string = internal.SDK_PREFIX + " Validation error. Invalid BYOT." SKYFLOW_ID_KEY_ERROR string = internal.SDK_PREFIX + " Validation error. 'skyflow_id' is missing from the data payload. Specify a 'skyflow_id'." + FAILED_TO_UNMARSHAL_ERROR string = internal.SDK_PREFIX + "Failed to unmarshal error" COLUMN_NAME_KEY_ERROR_FILE_UPLOAD = internal.SDK_PREFIX + " Validation error. columnName is missing from the payload. Specify a columnName key." MISSING_FILE_SOURCE_IN_UPLOAD_FILE = internal.SDK_PREFIX + " Validation error. Provide exactly one of filePath, base64, or fileObject." FILE_NAME_MUST_BE_PROVIDED_WITH_FILE_OBJECT = internal.SDK_PREFIX + " Validation error. fileName must be provided when using fileObject." diff --git a/v2/utils/error/skyflow_exception.go b/v2/utils/error/skyflow_exception.go index 8894094..15bd6ce 100644 --- a/v2/utils/error/skyflow_exception.go +++ b/v2/utils/error/skyflow_exception.go @@ -27,10 +27,10 @@ func (se *SkyflowError) Error() string { if se.originalError != nil { return fmt.Sprintf("Message: %s, Original Error (if any): %s", se.message, se.originalError.Error()) } - return fmt.Sprintf("Message: %s", se.message) + return fmt.Sprintf("Message: %s", se.message) //nolint:revive } func (se *SkyflowError) GetMessage() string { - return fmt.Sprintf("Message: %s", se.message) + return fmt.Sprintf("Message: %s", se.message) //nolint:revive } func (se *SkyflowError) GetCode() string { return fmt.Sprintf("Code: %s", se.httpCode) @@ -54,62 +54,62 @@ func NewSkyflowError(code ErrorCodesEnum, message string) *SkyflowError { return &SkyflowError{ httpCode: string(code), message: message, - httpStatusCode: string("Bad Request"), + httpStatusCode: constants.HTTP_STATUS_BAD_REQUEST, } } func SkyflowApiError(responseHeaders http.Response) *SkyflowError { skyflowError := SkyflowError{ requestId: responseHeaders.Header.Get(constants.REQUEST_KEY), } - if responseHeaders.Header.Get("Content-Type") == "application/json" { + if responseHeaders.Header.Get(constants.HEADER_CONTENT_TYPE_CAPITAL) == constants.CONTENT_TYPE_JSON { bodyBytes, _ := io.ReadAll(responseHeaders.Body) // Parse JSON into a struct var apiError map[string]interface{} if err := json.Unmarshal(bodyBytes, &apiError); err != nil { - return NewSkyflowError(INVALID_INPUT_CODE, "Failed to unmarhsal error") + return NewSkyflowError(INVALID_INPUT_CODE, FAILED_TO_UNMARSHAL_ERROR) } - if errorBody, ok := apiError["error"].(map[string]interface{}); ok { - if httpCode, exists := errorBody["http_code"].(float64); exists { + if errorBody, ok := apiError[constants.ERROR_KEY_ERROR].(map[string]interface{}); ok { + if httpCode, exists := errorBody[constants.ERROR_KEY_HTTP_CODE].(float64); exists { skyflowError.httpCode = strconv.FormatFloat(httpCode, 'f', 0, 64) } else { skyflowError.httpCode = strconv.Itoa(responseHeaders.StatusCode) } - if message, exists := errorBody["message"].(string); exists { + if message, exists := errorBody[constants.ERROR_KEY_MESSAGE].(string); exists { skyflowError.message = message } else { - skyflowError.message = "Unknown error" + skyflowError.message = constants.UNKNOWN_ERROR } - if grpcCode, exists := errorBody["grpc_code"].(float64); exists { + if grpcCode, exists := errorBody[constants.ERROR_KEY_GRPC_CODE].(float64); exists { skyflowError.grpcCode = strconv.FormatFloat(grpcCode, 'f', 0, 64) } - if httpStatus, exists := errorBody["http_status"].(string); exists { + if httpStatus, exists := errorBody[constants.ERROR_KEY_HTTP_STATUS].(string); exists { skyflowError.httpStatusCode = httpStatus } - if details, exists := errorBody["details"].([]interface{}); exists { + if details, exists := errorBody[constants.ERROR_KEY_DETAILS].([]interface{}); exists { // initalize details if nil if skyflowError.details == nil { skyflowError.details = make([]interface{}, 0) } skyflowError.details = details } - } else if errBody, ok := apiError["error"].(string); ok { + } else if errBody, ok := apiError[constants.ERROR_KEY_ERROR].(string); ok { skyflowError.message = errBody } else { skyflowError.message = string(bodyBytes) } - } else if responseHeaders.Header.Get("Content-Type") == "text/plain" { + } else if responseHeaders.Header.Get(constants.HEADER_CONTENT_TYPE_CAPITAL) == constants.CONTENT_TYPE_TEXT_PLAIN { bodyBytes, err := io.ReadAll(responseHeaders.Body) if err != nil { - return NewSkyflowError(INVALID_INPUT_CODE, "Failed to read error") + return NewSkyflowError(INVALID_INPUT_CODE, constants.ERROR_FAILED_TO_READ) } else { skyflowError.message = string(bodyBytes) skyflowError.httpStatusCode = responseHeaders.Status } - } else if responseHeaders.Header.Get("Content-Type") == "text/plain; charset=utf-8" { + } else if responseHeaders.Header.Get(constants.HEADER_CONTENT_TYPE_CAPITAL) == constants.CONTENT_TYPE_TEXT_CHARSET { bodyBytes, errs := io.ReadAll(responseHeaders.Body) if errs != nil { - return NewSkyflowError(INVALID_INPUT_CODE, "Failed to read error") + return NewSkyflowError(INVALID_INPUT_CODE, constants.ERROR_FAILED_TO_READ) } // Parse JSON into a struct var apiError map[string]interface{} @@ -118,30 +118,30 @@ func SkyflowApiError(responseHeaders http.Response) *SkyflowError { skyflowError.httpStatusCode = responseHeaders.Status } if apiError != nil { - if errorBody, ok := apiError["error"].(map[string]interface{}); ok { - if httpCode, exists := errorBody["http_code"].(float64); exists { + if errorBody, ok := apiError[constants.ERROR_KEY_ERROR].(map[string]interface{}); ok { + if httpCode, exists := errorBody[constants.ERROR_KEY_HTTP_CODE].(float64); exists { skyflowError.httpCode = strconv.FormatFloat(httpCode, 'f', 0, 64) } else { skyflowError.httpCode = strconv.Itoa(responseHeaders.StatusCode) } - if message, exists := errorBody["message"].(string); exists { - skyflowError.message = message - } else { - skyflowError.message = "Unknown error" + if message, exists := errorBody[constants.ERROR_KEY_MESSAGE].(string); exists { + skyflowError.message = message + } else { + skyflowError.message = constants.UNKNOWN_ERROR } - if grpcCode, exists := errorBody["grpc_code"].(float64); exists { + if grpcCode, exists := errorBody[constants.ERROR_KEY_GRPC_CODE].(float64); exists { skyflowError.grpcCode = strconv.FormatFloat(grpcCode, 'f', 0, 64) } - if httpStatus, exists := errorBody["http_status"].(string); exists { + if httpStatus, exists := errorBody[constants.ERROR_KEY_HTTP_STATUS].(string); exists { skyflowError.httpStatusCode = httpStatus } - if details, exists := errorBody["details"].([]interface{}); exists { + if details, exists := errorBody[constants.ERROR_KEY_DETAILS].([]interface{}); exists { if skyflowError.details == nil { skyflowError.details = make([]interface{}, 0) } skyflowError.details = details } - } else if errBody, ok := apiError["error"].(string); ok { + } else if errBody, ok := apiError[constants.ERROR_KEY_ERROR].(string); ok { skyflowError.message = errBody skyflowError.httpStatusCode = responseHeaders.Status } else { @@ -166,7 +166,7 @@ func SkyflowApiError(responseHeaders http.Response) *SkyflowError { boolValue = false } // set the error detail - errorDetail["errorFromClient"] = boolValue + errorDetail[constants.ERROR_KEY_FROM_CLIENT] = boolValue skyflowError.details = append(skyflowError.details, errorDetail) } return &skyflowError @@ -186,28 +186,28 @@ func SkyflowErrorApi(error error, header http.Header) *SkyflowError { if err != nil { return NewSkyflowError(INVALID_INPUT_CODE, error.Error()) } - if errorBody, ok := apiError["error"].(map[string]interface{}); ok { - if httpCode, exists := errorBody["http_code"].(float64); exists { + if errorBody, ok := apiError[constants.ERROR_KEY_ERROR].(map[string]interface{}); ok { + if httpCode, exists := errorBody[constants.ERROR_KEY_HTTP_CODE].(float64); exists { skyflowError.httpCode = strconv.FormatFloat(httpCode, 'f', 0, 64) } - if message, exists := errorBody["message"].(string); exists { + if message, exists := errorBody[constants.ERROR_KEY_MESSAGE].(string); exists { skyflowError.message = message } else { - skyflowError.message = "Unknown error" + skyflowError.message = constants.UNKNOWN_ERROR } - if grpcCode, exists := errorBody["grpc_code"].(float64); exists { + if grpcCode, exists := errorBody[constants.ERROR_KEY_GRPC_CODE].(float64); exists { skyflowError.grpcCode = strconv.FormatFloat(grpcCode, 'f', 0, 64) } - if httpStatus, exists := errorBody["http_status"].(string); exists { + if httpStatus, exists := errorBody[constants.ERROR_KEY_HTTP_STATUS].(string); exists { skyflowError.httpStatusCode = httpStatus } - if details, exists := errorBody["details"].([]interface{}); exists { + if details, exists := errorBody[constants.ERROR_KEY_DETAILS].([]interface{}); exists { if skyflowError.details == nil { skyflowError.details = make([]interface{}, 0) } skyflowError.details = details } - } else if errBody, ok := apiError["error"].(string); ok { + } else if errBody, ok := apiError[constants.ERROR_KEY_ERROR].(string); ok { skyflowError.message = errBody } else { skyflowError.message = error.Error() diff --git a/v2/utils/messages/error_logs.go b/v2/utils/messages/error_logs.go index 7865828..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." @@ -67,6 +68,9 @@ const ( BEARER_TOKEN_REJECTED = SDK_LOG_PREFIX + "Bearer token request resulted in failure." PRIVATE_KEY_TYPE = SDK_LOG_PREFIX + "RSA private key is of the wrong type Pem Type: %s" PARSE_JWT_PAYLOAD = SDK_LOG_PREFIX + "Unable to parse jwt payload" + FAILED_TO_MARSHALL_JSON_METADATA = SDK_LOG_PREFIX + "Failed to marshal json data in createJSONMetadata()." + FAILED_TO_DECODE_BASE64 = SDK_LOG_PREFIX + "Failed to decode base64: %v" + FAILED_TO_CREATE_FILE = SDK_LOG_PREFIX + "Failed to create file: %v" EMPTY_REQUEST_HEADERS = SDK_LOG_PREFIX + "Invalid %s request. Request headers can not be empty." INVALID_REQUEST_HEADERS = SDK_LOG_PREFIX + "Invalid %s request. Request header can not be nil or empty in request headers." EMPTY_PATH_PARAMS = SDK_LOG_PREFIX + "Invalid %s request. Path params can not 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"