Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions chat_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions common_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

98 changes: 98 additions & 0 deletions feeds_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package getstream_test
import (
"context"
"fmt"
"os"
"testing"
"time"

Expand Down Expand Up @@ -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)
})
}

// =================================================================
Expand Down Expand Up @@ -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
// =================================================================
Expand Down
117 changes: 116 additions & 1 deletion http.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import (
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"reflect"
"strconv"
"strings"
Expand Down Expand Up @@ -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")
Expand All @@ -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)
Expand All @@ -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")
Expand Down