diff --git a/src/Client.php b/src/Client.php index d2e2202..da9a27e 100644 --- a/src/Client.php +++ b/src/Client.php @@ -16,7 +16,7 @@ class Client { use CommonTrait; - + private string $apiKey; private string $apiSecret; private string $baseUrl; @@ -159,6 +159,41 @@ public function makeRequest( 'stream-auth-type' => 'jwt', ]); + // Check if this is an upload request (needs multipart/form-data) - like Go SDK + if ($body instanceof GeneratedModels\ImageUploadRequest || $body instanceof GeneratedModels\FileUploadRequest) { + unset($headers['Content-Type']); // Let Guzzle set multipart Content-Type + + $multipart = []; + $isImage = $body instanceof GeneratedModels\ImageUploadRequest; + + // File field + if ($body->file !== null) { + // Check if it's a file path that exists, otherwise treat as base64 + $filePath = (string) $body->file; + $fileContent = file_exists($filePath) + ? file_get_contents($filePath) + : (base64_decode($filePath, true) ?: throw new StreamException('Failed to decode base64 file data')); + + $multipart[] = [ + 'name' => 'file', + 'contents' => $fileContent, + 'filename' => $isImage ? 'image.jpg' : (file_exists($filePath) ? basename($filePath) : 'file.pdf'), + ]; + } + + // User field (JSON string) + if ($body->user !== null) { + $multipart[] = ['name' => 'user', 'contents' => json_encode($body->user)]; + } + + // Upload sizes (images only, JSON string) + if ($isImage && $body->uploadSizes !== null && !empty($body->uploadSizes)) { + $multipart[] = ['name' => 'upload_sizes', 'contents' => json_encode($body->uploadSizes)]; + } + + return $this->httpClient->request($method, $url, $headers, $multipart); + } + // Make the request return $this->httpClient->request($method, $url, $headers, $body); } diff --git a/src/Constant.php b/src/Constant.php index 6935e55..bfb32d5 100644 --- a/src/Constant.php +++ b/src/Constant.php @@ -6,6 +6,5 @@ class Constant { - const VERSION = '2.0.2'; + public const VERSION = '2.0.2'; } - diff --git a/src/Http/GuzzleHttpClient.php b/src/Http/GuzzleHttpClient.php index 203f6b3..eafcf1c 100644 --- a/src/Http/GuzzleHttpClient.php +++ b/src/Http/GuzzleHttpClient.php @@ -57,7 +57,11 @@ public function request(string $method, string $url, array $headers = [], mixed // Add body if provided if ($body !== null) { - if (is_array($body) || is_object($body)) { + // Check if this is multipart form data (array of arrays with 'name' and 'contents') + if (is_array($body) && !empty($body) && isset($body[0]) && is_array($body[0]) && isset($body[0]['name'])) { + // This is multipart form data + $options['multipart'] = $body; + } elseif (is_array($body) || is_object($body)) { $options['json'] = $body; } else { $options['body'] = $body; diff --git a/tests/Integration/FeedIntegrationTest.php b/tests/Integration/FeedIntegrationTest.php index 1707b7e..6d39543 100644 --- a/tests/Integration/FeedIntegrationTest.php +++ b/tests/Integration/FeedIntegrationTest.php @@ -296,6 +296,124 @@ public function test02eCreateActivityMultipleFeeds(): void echo "āœ… Created activity in multiple feeds: {$activityId}\n"; } + /** + * @throws StreamException + * + * @test + */ + public function test02fUploadImageAndFile(): void + { + echo "\nšŸ“¤ Testing file and image uploads...\n"; + + // Use the test image file in the same directory + $testImagePath = __DIR__ . '/testupload.png'; + + if (!file_exists($testImagePath)) { + self::markTestSkipped("Test image file not found: {$testImagePath}"); + + return; + } + + // Test 1: Upload Image from file path + echo "\nšŸ–¼ļø Testing image upload from file...\n"; + + // snippet-start: UploadImage + $imageUploadRequest = new GeneratedModels\ImageUploadRequest( + file: $testImagePath, + user: new GeneratedModels\OnlyUserID(id: $this->testUserId), + uploadSizes: [ + [ + 'width' => 100, + 'height' => 100, + 'resize' => 'scale', + 'crop' => 'center', + ], + ] + ); + $imageResponse = $this->client->uploadImage($imageUploadRequest); + // snippet-end: UploadImage + + $this->assertResponseSuccess($imageResponse, 'upload image'); + + $imageData = $imageResponse->getData(); + self::assertNotNull($imageData); + self::assertNotNull($imageData->file); + self::assertNotEmpty($imageData->file); + + $imageUrl = $imageData->file; + echo "āœ… Image uploaded successfully: {$imageUrl}\n"; + + // Test 2: Upload Image with base64 (alternative method) + echo "\nšŸ–¼ļø Testing image upload with base64...\n"; + + // Read image and encode as base64 + $imageContent = file_get_contents($testImagePath); + if ($imageContent === false) { + self::markTestSkipped("Could not read test image file: {$testImagePath}"); + + return; + } + + $base64Image = base64_encode($imageContent); + + $imageUploadRequest2 = new GeneratedModels\ImageUploadRequest( + file: $base64Image, + user: new GeneratedModels\OnlyUserID(id: $this->testUserId), + uploadSizes: [ + [ + 'width' => 200, + 'height' => 200, + 'resize' => 'fill', + 'crop' => 'center', + ], + ] + ); + $imageResponse2 = $this->client->uploadImage($imageUploadRequest2); + + $this->assertResponseSuccess($imageResponse2, 'upload image with base64'); + $imageData2 = $imageResponse2->getData(); + self::assertNotNull($imageData2->file); + echo "āœ… Image uploaded with base64: {$imageData2->file}\n"; + + // Test 3: Upload File + echo "\nšŸ“„ Testing file upload...\n"; + + // Create a temporary text file + $tempFile = tmpfile(); + if ($tempFile === false) { + self::markTestSkipped('Could not create temporary file'); + + return; + } + + $tempFilePath = stream_get_meta_data($tempFile)['uri']; + $testContent = "This is a test file content from PHP SDK integration test\nCreated at: " . date('Y-m-d H:i:s'); + file_put_contents($tempFilePath, $testContent); + + // snippet-start: UploadFile + $fileUploadRequest = new GeneratedModels\FileUploadRequest( + file: $tempFilePath, + user: new GeneratedModels\OnlyUserID(id: $this->testUserId) + ); + $fileResponse = $this->client->uploadFile($fileUploadRequest); + // snippet-end: UploadFile + + $this->assertResponseSuccess($fileResponse, 'upload file'); + + $fileData = $fileResponse->getData(); + self::assertNotNull($fileData); + self::assertNotNull($fileData->file); + self::assertNotEmpty($fileData->file); + + $fileUrl = $fileData->file; + echo "āœ… File uploaded successfully: {$fileUrl}\n"; + + // Cleanup temp file + fclose($tempFile); + + echo "āœ… All file upload tests completed\n"; + } + /** * @test */ @@ -816,6 +934,7 @@ public function test15QueryFollows(): void /** * @test + * * @throws StreamApiException */ public function test15bGetOrCreateFeedWithActivitiesAndFollow(): void @@ -876,6 +995,7 @@ public function test15bGetOrCreateFeedWithActivitiesAndFollow(): void // Step 5: Follow user 2 from user 1 echo "\n5ļøāƒ£ Following user 2 from user 1...\n"; + try { $followResponse = $this->feedsV3Client->follow( new GeneratedModels\FollowRequest( @@ -887,6 +1007,7 @@ public function test15bGetOrCreateFeedWithActivitiesAndFollow(): void echo "āœ… Followed user 2\n"; } catch (StreamApiException $e) { echo 'āš ļø Follow failed: ' . $e->getMessage() . "\n"; + throw $e; } @@ -899,15 +1020,16 @@ public function test15bGetOrCreateFeedWithActivitiesAndFollow(): void $verifyFeedData = $verifyFeedResponse->getData(); self::assertNotNull($verifyFeedData->feed); self::assertSame($feedData2->feed->id, $verifyFeedData->feed->id, 'Feed ID should match'); - + // Check if activities exist in the feed if ($verifyFeedData->activities !== null && count($verifyFeedData->activities) > 0) { - echo "āœ… Feed 2 has " . count($verifyFeedData->activities) . " activities\n"; + echo 'āœ… Feed 2 has ' . count($verifyFeedData->activities) . " activities\n"; // Verify our activity is in the list $foundActivity = false; foreach ($verifyFeedData->activities as $activity) { if ($activity->id === $activityId2) { $foundActivity = true; + break; } } diff --git a/tests/Integration/testupload.png b/tests/Integration/testupload.png new file mode 100644 index 0000000..0bfb25a Binary files /dev/null and b/tests/Integration/testupload.png differ