diff --git a/chat_test.go b/chat_test.go index 526a89e..789ee69 100644 --- a/chat_test.go +++ b/chat_test.go @@ -114,7 +114,9 @@ func TestChatDeleteChannelFile(t *testing.T) { _, err = client.Chat().DeleteChannelFile(context.Background(), "", "", &getstream.DeleteChannelFileRequest{}) require.NoError(t, err) } + func TestChatUploadChannelFile(t *testing.T) { + t.Skip("current test is wrong, autogenerated, skipping due to seperate integration test for upload") client, err := getstream.NewClient("key", "secret", getstream.WithHTTPClient(&StubHTTPClient{})) require.NoError(t, err) @@ -135,7 +137,9 @@ func TestChatDeleteChannelImage(t *testing.T) { _, err = client.Chat().DeleteChannelImage(context.Background(), "", "", &getstream.DeleteChannelImageRequest{}) require.NoError(t, err) } + func TestChatUploadChannelImage(t *testing.T) { + t.Skip("current test is wrong, autogenerated, skipping due to seperate integration test for upload") client, err := getstream.NewClient("key", "secret", getstream.WithHTTPClient(&StubHTTPClient{})) require.NoError(t, err) diff --git a/common_test.go b/common_test.go index 4c5fd60..5984509 100644 --- a/common_test.go +++ b/common_test.go @@ -360,6 +360,8 @@ func TestCommonDeleteFile(t *testing.T) { require.NoError(t, err) } func TestCommonUploadFile(t *testing.T) { + t.Skip("current test is wrong, autogenerated, skipping due to seperate integration test for upload") + client, err := getstream.NewClient("key", "secret", getstream.WithHTTPClient(&StubHTTPClient{})) require.NoError(t, err) @@ -374,6 +376,7 @@ func TestCommonDeleteImage(t *testing.T) { require.NoError(t, err) } func TestCommonUploadImage(t *testing.T) { + t.Skip("current test is wrong, autogenerated, skipping due to seperate integration test for upload") client, err := getstream.NewClient("key", "secret", getstream.WithHTTPClient(&StubHTTPClient{})) require.NoError(t, err) diff --git a/feeds_integration_test.go b/feeds_integration_test.go index 620405b..b4cd7b2 100644 --- a/feeds_integration_test.go +++ b/feeds_integration_test.go @@ -3,6 +3,7 @@ package getstream_test import ( "context" "fmt" + "os" "testing" "time" @@ -215,6 +216,18 @@ func TestFeedIntegrationSuite(t *testing.T) { t.Run("Test34_FeedViewCRUD", func(t *testing.T) { test34FeedViewCRUD(t, ctx, feedsClient) }) + + t.Run("UploadFileFromPath", func(t *testing.T) { + testFileUploadIntegration(t, ctx, client, testUserID) + }) + + t.Run("UploadImageWithSizes", func(t *testing.T) { + testImageUploadIntegration(t, ctx, client, testUserID) + }) + + t.Run("UploadFileError", func(t *testing.T) { + testFileUploadErrorIntegration(t, ctx, client, testUserID) + }) } // ================================================================= @@ -1888,6 +1901,91 @@ func test36BatchFeedOperations(t *testing.T, ctx context.Context, feedsClient *g fmt.Println("✅ Completed Batch Feed operations") } +func testFileUploadIntegration(t *testing.T, ctx context.Context, client *getstream.Stream, testUserID string) { + // Create a temporary test file + testContent := "This is a test file for multipart upload integration test\nContains multiple lines\nWith various content" + tmpFile, err := os.CreateTemp("", "integration-test-*.txt") + require.NoError(t, err, "Failed to create temp file") + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.WriteString(testContent) + require.NoError(t, err, "Failed to write test content") + tmpFile.Close() + + // snippet-start: UploadFile + uploadReq := &getstream.UploadFileRequest{ + File: getstream.PtrTo(tmpFile.Name()), + User: &getstream.OnlyUserID{ + ID: testUserID, + }, + } + + response, err := client.UploadFile(ctx, uploadReq) + // snippet-stop: UploadFile + + assertResponseSuccess(t, response, err, "upload file from path") + + // Verify response contains file URL + assert.NotNil(t, response.Data.File, "File URL should not be nil") + assert.NotEmpty(t, *response.Data.File, "File URL should not be empty") + assert.Contains(t, *response.Data.File, "http", "File URL should be a valid HTTP URL") + + fmt.Printf("✅ File uploaded successfully: %s\n", *response.Data.File) +} + +func testImageUploadIntegration(t *testing.T, ctx context.Context, client *getstream.Stream, testUserID string) { + // Create a temporary test image file (fake image data) + testImageContent := "fake-png-image-data-for-testing" + tmpFile, err := os.CreateTemp("", "integration-test-*.png") + require.NoError(t, err, "Failed to create temp image file") + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.WriteString(testImageContent) + require.NoError(t, err, "Failed to write test image content") + tmpFile.Close() + + // snippet-start: UploadImage + // Upload image with upload sizes + uploadReq := &getstream.UploadImageRequest{ + File: getstream.PtrTo(tmpFile.Name()), + UploadSizes: []getstream.ImageSize{ + {Width: getstream.PtrTo(100), Height: getstream.PtrTo(100)}, + {Width: getstream.PtrTo(300), Height: getstream.PtrTo(200)}, + }, + User: &getstream.OnlyUserID{ + ID: testUserID, + }, + } + + response, err := client.UploadImage(ctx, uploadReq) + // snippet-stop: UploadImage + + assertResponseSuccess(t, response, err, "upload image with sizes") + + // Verify response contains file URL + assert.NotNil(t, response.Data.File, "Image URL should not be nil") + assert.NotEmpty(t, *response.Data.File, "Image URL should not be empty") + assert.Contains(t, *response.Data.File, "http", "Image URL should be a valid HTTP URL") + + fmt.Printf("✅ Image uploaded successfully: %s\n", *response.Data.File) +} + +func testFileUploadErrorIntegration(t *testing.T, ctx context.Context, client *getstream.Stream, testUserID string) { + // Try to upload non-existent file + uploadReq := &getstream.UploadFileRequest{ + File: getstream.PtrTo("/non/existent/file.txt"), + User: &getstream.OnlyUserID{ + ID: testUserID, + }, + } + + _, err := client.UploadFile(ctx, uploadReq) + assert.Error(t, err, "Should fail when file doesn't exist") + assert.Contains(t, err.Error(), "failed to open file", "Error should mention file opening failure") + + fmt.Printf("✅ Error handling works correctly: %s\n", err.Error()) +} + // ================================================================= // HELPER METHODS // ================================================================= diff --git a/http.go b/http.go index d211ddb..edd3c19 100644 --- a/http.go +++ b/http.go @@ -6,8 +6,10 @@ import ( "encoding/json" "fmt" "io" + "mime/multipart" "net/http" "net/url" + "os" "reflect" "strconv" "strings" @@ -150,6 +152,7 @@ func newRequest[T any](c *Client, ctx context.Context, method, path string, para // Handle other methods with body c.logger.Debug("Method: %s, Data: %#v (Type: %T)", method, data, data) + switch t := any(data).(type) { case nil: c.logger.Debug("Data is nil") @@ -160,6 +163,8 @@ func newRequest[T any](c *Client, ctx context.Context, method, path string, para case io.Reader: c.logger.Debug("Data is io.Reader") r.Body = io.NopCloser(t) + case *UploadFileRequest, *UploadImageRequest: + return c.createMultipartRequest(r, t) default: c.logger.Debug("Data is of type %T, attempting to marshal to JSON", t) b, err := json.Marshal(data) @@ -175,9 +180,119 @@ func newRequest[T any](c *Client, ctx context.Context, method, path string, para return r, nil } +func getFileContent(fileName string, fileContent io.Reader) (io.Reader, error) { + if fileContent != nil { + return fileContent, nil + } + if fileName != "" { + file, err := os.Open(fileName) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + return file, nil + } + return nil, fmt.Errorf("either file name or file content must be provided") +} + +// createMultipartRequest creates a multipart form request for file/image uploads +func (c *Client) createMultipartRequest(r *http.Request, data any) (*http.Request, error) { + var buf bytes.Buffer + writer := multipart.NewWriter(&buf) + + var fileContent io.Reader + var fileName string + var err error + // Handle both UploadFileRequest and UploadImageRequest + switch req := data.(type) { + case *UploadFileRequest: + if req.File == nil { + return nil, fmt.Errorf("file name must be provided") + } + fileName = *req.File + fileContent, err = getFileContent(*req.File, nil) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + + // Add user field if present + if req.User != nil { + userJSON, err := json.Marshal(req.User) + if err != nil { + return nil, fmt.Errorf("failed to marshal user: %w", err) + } + err = writer.WriteField("user", string(userJSON)) + if err != nil { + return nil, fmt.Errorf("failed to write user field: %w", err) + } + } + + case *UploadImageRequest: + if req.File == nil { + return nil, fmt.Errorf("file name must be provided") + } + fileName = *req.File + fileContent, err = getFileContent(*req.File, nil) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + + // Add upload_sizes field if present + if req.UploadSizes != nil && len(req.UploadSizes) > 0 { + uploadSizesJSON, err := json.Marshal(req.UploadSizes) + if err != nil { + return nil, fmt.Errorf("failed to marshal upload_sizes: %w", err) + } + err = writer.WriteField("upload_sizes", string(uploadSizesJSON)) + if err != nil { + return nil, fmt.Errorf("failed to write upload_sizes field: %w", err) + } + } + + // Add user field if present + if req.User != nil { + userJSON, err := json.Marshal(req.User) + if err != nil { + return nil, fmt.Errorf("failed to marshal user: %w", err) + } + err = writer.WriteField("user", string(userJSON)) + if err != nil { + return nil, fmt.Errorf("failed to write user field: %w", err) + } + } + + default: + return nil, fmt.Errorf("unsupported request type for multipart: %T", data) + } + + // Add file field + fileWriter, err := writer.CreateFormFile("file", fileName) + if err != nil { + return nil, fmt.Errorf("failed to create form file: %w", err) + } + + _, err = io.Copy(fileWriter, fileContent) + if err != nil { + return nil, fmt.Errorf("failed to copy file content: %w", err) + } + + err = writer.Close() + if err != nil { + return nil, fmt.Errorf("failed to close multipart writer: %w", err) + } + + // Update request body and content type + r.Body = io.NopCloser(&buf) + r.Header.Set("Content-Type", writer.FormDataContentType()) + + c.logger.Debug("Created multipart request with file: %s", fileName) + return r, nil +} + // setHeaders sets necessary headers for the request func (c *Client) setHeaders(r *http.Request) { - r.Header.Set("Content-Type", "application/json") + if r.Header.Get("Content-Type") == "" { + r.Header.Set("Content-Type", "application/json") + } r.Header.Set("X-Stream-Client", versionHeader()) r.Header.Set("Authorization", c.authToken) r.Header.Set("Stream-Auth-Type", "jwt")