feat: adding tests..
This commit is contained in:
parent
d39de0752c
commit
100e52be54
5 changed files with 685 additions and 11 deletions
76
.env.ci
76
.env.ci
|
|
@ -1,2 +1,78 @@
|
|||
APP_NAME="TryPost"
|
||||
APP_ENV=testing
|
||||
APP_KEY=base64:Zfo+2dkSaHvem5LCiZS/baYHv2Pv1vrSc0F2NaF29Ec=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
SELF_HOSTED=true
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
|
||||
BCRYPT_ROUNDS=4
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# Database (PostgreSQL for CI)
|
||||
DB_CONNECTION=pgsql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_DATABASE=trypost_test
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=password
|
||||
|
||||
# Session
|
||||
SESSION_DRIVER=array
|
||||
SESSION_LIFETIME=1440
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
# Broadcasting, Queue, Cache
|
||||
BROADCAST_CONNECTION=null
|
||||
QUEUE_CONNECTION=sync
|
||||
CACHE_STORE=array
|
||||
|
||||
# File Storage
|
||||
FILESYSTEM_DISK=local
|
||||
|
||||
# Redis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
# Mail
|
||||
MAIL_MAILER=array
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
# Reverb (WebSockets) - disabled for testing
|
||||
REVERB_APP_ID=1001
|
||||
REVERB_APP_KEY=test-reverb-key
|
||||
REVERB_APP_SECRET=test-reverb-secret
|
||||
REVERB_HOST="localhost"
|
||||
REVERB_PORT=8080
|
||||
REVERB_SCHEME=http
|
||||
|
||||
# Disable optional services
|
||||
PULSE_ENABLED=false
|
||||
TELESCOPE_ENABLED=false
|
||||
NIGHTWATCH_ENABLED=false
|
||||
|
||||
# Vite
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
|
||||
VITE_REVERB_HOST="${REVERB_HOST}"
|
||||
VITE_REVERB_PORT="${REVERB_PORT}"
|
||||
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
|
||||
|
|
|
|||
12
.github/workflows/tests.yml
vendored
12
.github/workflows/tests.yml
vendored
|
|
@ -69,17 +69,7 @@ jobs:
|
|||
run: npm run build
|
||||
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
cp .env.example .env
|
||||
php artisan key:generate
|
||||
|
||||
- name: Configure test environment
|
||||
run: |
|
||||
sed -i 's/DB_DATABASE=trypost/DB_DATABASE=trypost_test/' .env
|
||||
sed -i 's/DB_PASSWORD=$/DB_PASSWORD=password/' .env
|
||||
sed -i 's/CACHE_STORE=redis/CACHE_STORE=array/' .env
|
||||
sed -i 's/QUEUE_CONNECTION=redis/QUEUE_CONNECTION=sync/' .env
|
||||
sed -i 's/SESSION_DRIVER=database/SESSION_DRIVER=array/' .env
|
||||
run: cp .env.ci .env
|
||||
|
||||
- name: Run migrations
|
||||
run: php artisan migrate --force
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
use App\Models\Workspace;
|
||||
use App\Services\Social\LinkedInPublisher;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->workspace = Workspace::factory()->create();
|
||||
|
|
@ -161,3 +162,58 @@
|
|||
expect(fn () => $publisher->publish($this->postPlatform))
|
||||
->toThrow(TokenExpiredException::class);
|
||||
});
|
||||
|
||||
test('linkedin publisher handles token refresh failure', function () {
|
||||
$this->socialAccount->update([
|
||||
'token_expires_at' => now()->subHour(),
|
||||
'refresh_token' => 'refresh-token-123',
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://www.linkedin.com/oauth/v2/accessToken' => Http::response([
|
||||
'code' => 'INVALID_REFRESH_TOKEN',
|
||||
'message' => 'Invalid refresh token',
|
||||
], 400),
|
||||
]);
|
||||
|
||||
$publisher = new LinkedInPublisher;
|
||||
|
||||
expect(fn () => $publisher->publish($this->postPlatform))
|
||||
->toThrow(\Exception::class);
|
||||
});
|
||||
|
||||
test('linkedin publisher refreshes token when expiring soon', function () {
|
||||
$this->socialAccount->update([
|
||||
'token_expires_at' => now()->addMinutes(5),
|
||||
'refresh_token' => 'refresh-token-123',
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://www.linkedin.com/oauth/v2/accessToken' => Http::response([
|
||||
'access_token' => 'new-access-token',
|
||||
'refresh_token' => 'new-refresh-token',
|
||||
'expires_in' => 3600,
|
||||
], 200),
|
||||
'*/rest/posts' => Http::response(null, 201, ['x-restli-id' => 'urn:li:share:123456']),
|
||||
]);
|
||||
|
||||
$publisher = new LinkedInPublisher;
|
||||
$result = $publisher->publish($this->postPlatform);
|
||||
|
||||
expect($result['id'])->toBe('urn:li:share:123456');
|
||||
$this->socialAccount->refresh();
|
||||
expect($this->socialAccount->access_token)->toBe('new-access-token');
|
||||
});
|
||||
|
||||
test('linkedin publisher handles empty post id header', function () {
|
||||
Http::fake([
|
||||
'*/rest/posts' => Http::response(null, 201),
|
||||
]);
|
||||
|
||||
$publisher = new LinkedInPublisher;
|
||||
$result = $publisher->publish($this->postPlatform);
|
||||
|
||||
// When header is empty, id will be empty string or 'unknown'
|
||||
expect($result)->toHaveKey('id');
|
||||
expect($result)->toHaveKey('url');
|
||||
});
|
||||
|
|
|
|||
502
tests/Unit/Services/Social/InstagramPublisherTest.php
Normal file
502
tests/Unit/Services/Social/InstagramPublisherTest.php
Normal file
|
|
@ -0,0 +1,502 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Models\Post;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Services\Social\InstagramPublisher;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
$this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
|
||||
|
||||
$this->socialAccount = SocialAccount::factory()->instagram()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform_user_id' => 'ig_123456789',
|
||||
'username' => 'testuser',
|
||||
'token_expires_at' => now()->addDays(60),
|
||||
]);
|
||||
|
||||
$this->post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
$this->postPlatform = PostPlatform::factory()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $this->socialAccount->id,
|
||||
'platform' => Platform::Instagram,
|
||||
'content_type' => ContentType::InstagramFeed,
|
||||
'content' => 'Hello from Instagram!',
|
||||
]);
|
||||
|
||||
$this->publisher = new InstagramPublisher;
|
||||
});
|
||||
|
||||
test('instagram publisher throws exception without media', function () {
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(\Exception::class, 'Instagram requires at least one image or video');
|
||||
});
|
||||
|
||||
test('instagram publisher can publish single image', function () {
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'image',
|
||||
'path' => 'media/2026-01/test-image.jpg',
|
||||
'original_filename' => 'test.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'size' => 12345,
|
||||
'order' => 0,
|
||||
'meta' => ['width' => 1920, 'height' => 1080],
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media' => Http::response([
|
||||
'id' => 'container-123',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/container-123*' => Http::response([
|
||||
'status_code' => 'FINISHED',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media_publish' => Http::response([
|
||||
'id' => 'media-123456789',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/media-123456789*' => Http::response([
|
||||
'permalink' => 'https://www.instagram.com/p/ABC123/',
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$result = $this->publisher->publish($this->postPlatform);
|
||||
|
||||
expect($result)->toHaveKey('id');
|
||||
expect($result)->toHaveKey('url');
|
||||
expect($result['id'])->toBe('media-123456789');
|
||||
expect($result['url'])->toBe('https://www.instagram.com/p/ABC123/');
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return str_contains($request->url(), '/ig_123456789/media');
|
||||
});
|
||||
});
|
||||
|
||||
test('instagram publisher can publish reel', function () {
|
||||
$this->postPlatform->update(['content_type' => ContentType::InstagramReel]);
|
||||
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'video',
|
||||
'path' => 'media/2026-01/test-video.mp4',
|
||||
'original_filename' => 'test.mp4',
|
||||
'mime_type' => 'video/mp4',
|
||||
'size' => 1234567,
|
||||
'order' => 0,
|
||||
'meta' => ['width' => 1080, 'height' => 1920, 'duration' => 30],
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media' => Http::response([
|
||||
'id' => 'container-123',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/container-123*' => Http::response([
|
||||
'status_code' => 'FINISHED',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media_publish' => Http::response([
|
||||
'id' => 'reel-123456789',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/reel-123456789*' => Http::response([
|
||||
'permalink' => 'https://www.instagram.com/reel/ABC123/',
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$result = $this->publisher->publish($this->postPlatform);
|
||||
|
||||
expect($result['id'])->toBe('reel-123456789');
|
||||
expect($result['url'])->toBe('https://www.instagram.com/reel/ABC123/');
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return str_contains($request->url(), '/ig_123456789/media')
|
||||
&& (str_contains($request->body(), 'REELS') || str_contains($request->url(), 'media_publish'));
|
||||
});
|
||||
});
|
||||
|
||||
test('instagram publisher can publish image story', function () {
|
||||
$this->postPlatform->update(['content_type' => ContentType::InstagramStory]);
|
||||
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'image',
|
||||
'path' => 'media/2026-01/story.jpg',
|
||||
'original_filename' => 'story.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'size' => 512000,
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media' => Http::response([
|
||||
'id' => 'story-container-123',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/story-container-123*' => Http::response([
|
||||
'status_code' => 'FINISHED',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media_publish' => Http::response([
|
||||
'id' => 'story-123456789',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/story-123456789*' => Http::response([
|
||||
'permalink' => 'https://www.instagram.com/stories/testuser/123/',
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$result = $this->publisher->publish($this->postPlatform);
|
||||
|
||||
expect($result['id'])->toBe('story-123456789');
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return str_contains($request->url(), '/ig_123456789/media');
|
||||
});
|
||||
});
|
||||
|
||||
test('instagram publisher can publish video story', function () {
|
||||
$this->postPlatform->update(['content_type' => ContentType::InstagramStory]);
|
||||
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'video',
|
||||
'path' => 'media/2026-01/story.mp4',
|
||||
'original_filename' => 'story.mp4',
|
||||
'mime_type' => 'video/mp4',
|
||||
'size' => 5120000,
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media' => Http::response([
|
||||
'id' => 'story-container-123',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/story-container-123*' => Http::response([
|
||||
'status_code' => 'FINISHED',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media_publish' => Http::response([
|
||||
'id' => 'story-video-123456789',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/story-video-123456789*' => Http::response([
|
||||
'permalink' => 'https://www.instagram.com/stories/testuser/456/',
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$result = $this->publisher->publish($this->postPlatform);
|
||||
|
||||
expect($result['id'])->toBe('story-video-123456789');
|
||||
});
|
||||
|
||||
test('instagram publisher can publish carousel', function () {
|
||||
// Create multiple media items
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'image',
|
||||
'path' => "media/2026-01/test-image-{$i}.jpg",
|
||||
'original_filename' => "test-{$i}.jpg",
|
||||
'mime_type' => 'image/jpeg',
|
||||
'size' => 12345,
|
||||
'order' => $i,
|
||||
'meta' => ['width' => 1920, 'height' => 1080],
|
||||
]);
|
||||
}
|
||||
|
||||
Http::fake([
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media' => Http::sequence()
|
||||
->push(['id' => 'child-1'], 200)
|
||||
->push(['id' => 'child-2'], 200)
|
||||
->push(['id' => 'child-3'], 200)
|
||||
->push(['id' => 'carousel-container-123'], 200),
|
||||
'https://graph.instagram.com/v24.0/carousel-container-123*' => Http::response([
|
||||
'status_code' => 'FINISHED',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media_publish' => Http::response([
|
||||
'id' => 'carousel-123456789',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/carousel-123456789*' => Http::response([
|
||||
'permalink' => 'https://www.instagram.com/p/CAROUSEL123/',
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$result = $this->publisher->publish($this->postPlatform);
|
||||
|
||||
expect($result['id'])->toBe('carousel-123456789');
|
||||
expect($result['url'])->toBe('https://www.instagram.com/p/CAROUSEL123/');
|
||||
});
|
||||
|
||||
test('instagram publisher can publish carousel with videos', function () {
|
||||
// Create image and video mix
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'image',
|
||||
'path' => 'media/2026-01/test-image.jpg',
|
||||
'original_filename' => 'test.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'size' => 12345,
|
||||
'order' => 0,
|
||||
]);
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'video',
|
||||
'path' => 'media/2026-01/test-video.mp4',
|
||||
'original_filename' => 'test.mp4',
|
||||
'mime_type' => 'video/mp4',
|
||||
'size' => 1234567,
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media' => Http::sequence()
|
||||
->push(['id' => 'child-1'], 200)
|
||||
->push(['id' => 'child-2'], 200)
|
||||
->push(['id' => 'carousel-container-123'], 200),
|
||||
'https://graph.instagram.com/v24.0/child-2*' => Http::response([
|
||||
'status_code' => 'FINISHED',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/carousel-container-123*' => Http::response([
|
||||
'status_code' => 'FINISHED',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media_publish' => Http::response([
|
||||
'id' => 'carousel-mix-123456789',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/carousel-mix-123456789*' => Http::response([
|
||||
'permalink' => 'https://www.instagram.com/p/CAROUSELMIX/',
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$result = $this->publisher->publish($this->postPlatform);
|
||||
|
||||
expect($result['id'])->toBe('carousel-mix-123456789');
|
||||
});
|
||||
|
||||
test('instagram publisher throws exception on api error', function () {
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'image',
|
||||
'path' => 'media/2026-01/test-image.jpg',
|
||||
'original_filename' => 'test.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'size' => 12345,
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media' => Http::response([
|
||||
'error' => [
|
||||
'message' => 'Invalid parameter',
|
||||
'type' => 'GraphMethodException',
|
||||
'code' => 100,
|
||||
],
|
||||
], 400),
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('instagram publisher throws token expired exception on oauth error', function () {
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'image',
|
||||
'path' => 'media/2026-01/test-image.jpg',
|
||||
'original_filename' => 'test.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'size' => 12345,
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media' => Http::response([
|
||||
'error' => [
|
||||
'message' => 'Error validating access token',
|
||||
'type' => 'OAuthException',
|
||||
'code' => 190,
|
||||
],
|
||||
], 400),
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(TokenExpiredException::class);
|
||||
});
|
||||
|
||||
test('instagram publisher throws token expired exception on session expired subcode', function () {
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'image',
|
||||
'path' => 'media/2026-01/test-image.jpg',
|
||||
'original_filename' => 'test.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'size' => 12345,
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media' => Http::response([
|
||||
'error' => [
|
||||
'message' => 'Session has expired',
|
||||
'type' => 'OAuthException',
|
||||
'code' => 190,
|
||||
'error_subcode' => 463,
|
||||
],
|
||||
], 400),
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(TokenExpiredException::class);
|
||||
});
|
||||
|
||||
test('instagram publisher throws exception for unsupported content type', function () {
|
||||
$this->postPlatform->update(['content_type' => ContentType::LinkedInPost]);
|
||||
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'image',
|
||||
'path' => 'media/2026-01/test-image.jpg',
|
||||
'original_filename' => 'test.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'size' => 12345,
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(Exception::class, 'Unsupported Instagram content type');
|
||||
});
|
||||
|
||||
test('instagram publisher throws exception when no container id returned', function () {
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'image',
|
||||
'path' => 'media/2026-01/test-image.jpg',
|
||||
'original_filename' => 'test.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'size' => 12345,
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media' => Http::response([
|
||||
'success' => true,
|
||||
// No id returned
|
||||
], 200),
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(Exception::class, 'No container ID returned');
|
||||
});
|
||||
|
||||
test('instagram publisher handles media processing error', function () {
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'image',
|
||||
'path' => 'media/2026-01/test-image.jpg',
|
||||
'original_filename' => 'test.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'size' => 12345,
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media' => Http::response([
|
||||
'id' => 'container-123',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/container-123*' => Http::response([
|
||||
'status_code' => 'ERROR',
|
||||
], 200),
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(Exception::class, 'Instagram media processing failed');
|
||||
});
|
||||
|
||||
test('instagram publisher waits for media processing', function () {
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'image',
|
||||
'path' => 'media/2026-01/test-image.jpg',
|
||||
'original_filename' => 'test.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'size' => 12345,
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media' => Http::response([
|
||||
'id' => 'container-123',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/container-123*' => Http::sequence()
|
||||
->push(['status_code' => 'IN_PROGRESS'], 200)
|
||||
->push(['status_code' => 'IN_PROGRESS'], 200)
|
||||
->push(['status_code' => 'FINISHED'], 200),
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media_publish' => Http::response([
|
||||
'id' => 'media-123456789',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/media-123456789*' => Http::response([
|
||||
'permalink' => 'https://www.instagram.com/p/ABC123/',
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$result = $this->publisher->publish($this->postPlatform);
|
||||
|
||||
expect($result['id'])->toBe('media-123456789');
|
||||
});
|
||||
|
||||
test('instagram publisher throws exception when all carousel items fail', function () {
|
||||
// Create multiple media items
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'image',
|
||||
'path' => "media/2026-01/test-image-{$i}.jpg",
|
||||
'original_filename' => "test-{$i}.jpg",
|
||||
'mime_type' => 'image/jpeg',
|
||||
'size' => 12345,
|
||||
'order' => $i,
|
||||
]);
|
||||
}
|
||||
|
||||
Http::fake([
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media' => Http::response([
|
||||
'error' => ['message' => 'Upload failed'],
|
||||
], 400),
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(Exception::class, 'Failed to create any carousel items');
|
||||
});
|
||||
|
||||
test('instagram publisher handles publish failure', function () {
|
||||
$this->postPlatform->media()->create([
|
||||
'collection' => 'default',
|
||||
'type' => 'image',
|
||||
'path' => 'media/2026-01/test-image.jpg',
|
||||
'original_filename' => 'test.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'size' => 12345,
|
||||
'order' => 0,
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media' => Http::response([
|
||||
'id' => 'container-123',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/container-123*' => Http::response([
|
||||
'status_code' => 'FINISHED',
|
||||
], 200),
|
||||
'https://graph.instagram.com/v24.0/ig_123456789/media_publish' => Http::response([
|
||||
'error' => [
|
||||
'message' => 'Publish failed',
|
||||
'type' => 'GraphMethodException',
|
||||
'code' => 100,
|
||||
],
|
||||
], 400),
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
|
|
@ -140,3 +140,53 @@
|
|||
expect(fn () => $publisher->publish($this->postPlatform))
|
||||
->toThrow(TokenExpiredException::class);
|
||||
});
|
||||
|
||||
test('x publisher refreshes token when expiring soon', function () {
|
||||
$this->socialAccount->update([
|
||||
'token_expires_at' => now()->addMinutes(5),
|
||||
'refresh_token' => 'refresh-token-123',
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'*/2/oauth2/token' => Http::response([
|
||||
'access_token' => 'new-access-token',
|
||||
'refresh_token' => 'new-refresh-token',
|
||||
'expires_in' => 7200,
|
||||
], 200),
|
||||
'*/2/tweets' => Http::response([
|
||||
'data' => ['id' => 'tweet-123'],
|
||||
], 201),
|
||||
]);
|
||||
|
||||
$publisher = new XPublisher;
|
||||
$result = $publisher->publish($this->postPlatform);
|
||||
|
||||
expect($result['id'])->toBe('tweet-123');
|
||||
$this->socialAccount->refresh();
|
||||
expect($this->socialAccount->access_token)->toBe('new-access-token');
|
||||
});
|
||||
|
||||
test('x publisher handles 403 error as generic error', function () {
|
||||
Http::fake([
|
||||
'*' => Http::response([
|
||||
'title' => 'Forbidden',
|
||||
'detail' => 'You do not have access',
|
||||
], 403),
|
||||
]);
|
||||
|
||||
$publisher = new XPublisher;
|
||||
|
||||
expect(fn () => $publisher->publish($this->postPlatform))
|
||||
->toThrow(\Exception::class, 'X API error');
|
||||
});
|
||||
|
||||
test('x publisher handles empty error response', function () {
|
||||
Http::fake([
|
||||
'*' => Http::response(null, 500),
|
||||
]);
|
||||
|
||||
$publisher = new XPublisher;
|
||||
|
||||
expect(fn () => $publisher->publish($this->postPlatform))
|
||||
->toThrow(\Exception::class);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue