user = User::factory()->create(); $this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]); $this->post = Post::factory()->create([ 'workspace_id' => $this->workspace->id, 'user_id' => $this->user->id, 'status' => PostStatus::PartiallyPublished, 'published_at' => now()->subHour(), ]); }); test('it does not expose retry filters or confirmation bypasses', function () { $command = Artisan::all()['posts:retry']; expect($command->getDefinition()->hasOption('force'))->toBeFalse() ->and($command->getDefinition()->hasOption('platform'))->toBeFalse(); }); test('it queues fresh attempts only for failed enabled platforms', function () { Bus::fake([PublishToSocialPlatform::class]); $publishedPlatform = PostPlatform::factory()->published()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->linkedin()->create([ 'workspace_id' => $this->workspace->id, ]), ]); $failedThreads = PostPlatform::factory()->threads()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->threads()->create([ 'workspace_id' => $this->workspace->id, ]), 'platform_post_id' => 'stale-post-id', 'platform_url' => 'https://threads.net/stale', 'published_at' => now()->subHour(), 'error_context' => ['remote_operation_id' => 'stale-operation'], ]); $failedPinterest = PostPlatform::factory()->pinterest()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->pinterest()->create([ 'workspace_id' => $this->workspace->id, ]), ]); $disabledFailedPlatform = PostPlatform::factory()->tiktok()->failed()->disabled()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->tiktok()->create([ 'workspace_id' => $this->workspace->id, ]), ]); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->expectsOutput('2 publish attempt(s) queued.') ->assertSuccessful(); expect($this->post->fresh()->status)->toBe(PostStatus::Publishing) ->and($failedThreads->fresh()->status)->toBe(PlatformStatus::Pending) ->and($failedThreads->fresh()->platform_post_id)->toBeNull() ->and($failedThreads->fresh()->platform_url)->toBeNull() ->and($failedThreads->fresh()->published_at)->toBeNull() ->and($failedThreads->fresh()->error_message)->toBeNull() ->and($failedThreads->fresh()->error_context)->toBeNull() ->and($failedPinterest->fresh()->status)->toBe(PlatformStatus::Pending) ->and($publishedPlatform->fresh()->status)->toBe(PlatformStatus::Published) ->and($disabledFailedPlatform->fresh()->status)->toBe(PlatformStatus::Failed); Bus::assertDispatchedTimes(PublishToSocialPlatform::class, 2); Bus::assertDispatched( PublishToSocialPlatform::class, fn (PublishToSocialPlatform $job): bool => $job->postPlatform->is($failedThreads) && $job->uniqueAttempt === 0, ); }); test('it resumes a TikTok publish_id instead of starting from scratch', function () { Bus::fake([PublishToSocialPlatform::class]); Storage::fake(); $derivativePath = 'social-tiktok-photos/123e4567-e89b-12d3-a456-426614174000.jpg'; Storage::put($derivativePath, 'temporary image'); $failedTikTok = PostPlatform::factory()->tiktok()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->tiktok()->create([ 'workspace_id' => $this->workspace->id, ]), 'error_context' => [ 'tiktok_publish_id' => 'stale-publish-id', 'tiktok_derivative_paths' => [$derivativePath], 'retry_count' => 120, 'max_retries' => 120, 'category' => 'platform_unavailable', ], ]); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->expectsOutputToContain('Resume') ->assertSuccessful(); Storage::assertExists($derivativePath); expect($failedTikTok->fresh()->status)->toBe(PlatformStatus::Pending) ->and($failedTikTok->fresh()->error_context)->toBe([ 'tiktok_publish_id' => 'stale-publish-id', 'tiktok_derivative_paths' => [$derivativePath], ]); Bus::assertDispatched(PublishToSocialPlatform::class, fn (PublishToSocialPlatform $job): bool => $job->postPlatform->is($failedTikTok)); }); test('it resumes a TikTok publish_id after an account or job interruption', function (ErrorCategory $category) { Bus::fake([PublishToSocialPlatform::class]); $failedTikTok = PostPlatform::factory()->tiktok()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->tiktok()->create([ 'workspace_id' => $this->workspace->id, ]), 'error_context' => [ 'tiktok_publish_id' => 'pub_in_flight', 'category' => $category->value, ], ]); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->expectsOutputToContain('Resume') ->assertSuccessful(); expect($failedTikTok->fresh()->status)->toBe(PlatformStatus::Pending) ->and($failedTikTok->fresh()->error_context)->toBe([ 'tiktok_publish_id' => 'pub_in_flight', ]); })->with([ 'token expired' => [ErrorCategory::TokenExpired], 'job failed' => [ErrorCategory::JobFailed], ]); test('it keeps an Instagram workflow checkpoint on retry', function () { Bus::fake([PublishToSocialPlatform::class]); $workflow = [ 'stage' => 'final_container', 'container_id' => 'container-123', ]; $failedInstagram = PostPlatform::factory()->instagram()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->instagram()->create([ 'workspace_id' => $this->workspace->id, ]), 'error_context' => [ 'instagram_workflow' => $workflow, 'retry_count' => 90, 'max_retries' => 90, 'category' => 'timeout', ], ]); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->expectsOutputToContain('Resume') ->assertSuccessful(); expect($failedInstagram->fresh()->status)->toBe(PlatformStatus::Pending) ->and($failedInstagram->fresh()->error_context)->toBe([ 'instagram_workflow' => $workflow, ]); }); test('it removes stale TikTok derivatives when there is no publish_id to resume', function () { Bus::fake([PublishToSocialPlatform::class]); Storage::fake(); $derivativePath = 'social-tiktok-photos/123e4567-e89b-12d3-a456-426614174000.jpg'; Storage::put($derivativePath, 'temporary image'); $failedTikTok = PostPlatform::factory()->tiktok()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->tiktok()->create([ 'workspace_id' => $this->workspace->id, ]), 'error_context' => [ 'tiktok_derivative_paths' => [$derivativePath], 'category' => 'unknown', ], ]); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->expectsOutputToContain('New') ->assertSuccessful(); Storage::assertMissing($derivativePath); expect($failedTikTok->fresh()->status)->toBe(PlatformStatus::Pending) ->and($failedTikTok->fresh()->error_context)->toBeNull(); Bus::assertDispatched(PublishToSocialPlatform::class, fn (PublishToSocialPlatform $job): bool => $job->postPlatform->is($failedTikTok)); }); test('it does not change the post when confirmation is declined', function () { Bus::fake([PublishToSocialPlatform::class]); $failedPlatform = PostPlatform::factory()->threads()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->threads()->create([ 'workspace_id' => $this->workspace->id, ]), ]); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'no') ->expectsOutput('Retry cancelled.') ->assertSuccessful(); expect($this->post->fresh()->status)->toBe(PostStatus::PartiallyPublished) ->and($failedPlatform->fresh()->status)->toBe(PlatformStatus::Failed); Bus::assertNotDispatched(PublishToSocialPlatform::class); }); test('it rejects posts that are not in a terminal failure state', function () { Bus::fake([PublishToSocialPlatform::class]); $this->post->update(['status' => PostStatus::Publishing]); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsOutput('Only failed or partially published posts can be retried.') ->assertFailed(); Bus::assertNotDispatched(PublishToSocialPlatform::class); }); test('it retries a completely failed post', function () { Bus::fake([PublishToSocialPlatform::class]); $this->post->update(['status' => PostStatus::Failed]); $failedPlatform = PostPlatform::factory()->threads()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->threads()->create([ 'workspace_id' => $this->workspace->id, ]), ]); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->assertSuccessful(); expect($this->post->fresh()->status)->toBe(PostStatus::Publishing) ->and($failedPlatform->fresh()->status)->toBe(PlatformStatus::Pending); Bus::assertDispatched(PublishToSocialPlatform::class, fn (PublishToSocialPlatform $job): bool => $job->postPlatform->is($failedPlatform)); }); test('it fails when no failed enabled platform matches', function () { Bus::fake([PublishToSocialPlatform::class]); PostPlatform::factory()->published()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->linkedin()->create([ 'workspace_id' => $this->workspace->id, ]), ]); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsOutput('No failed enabled platforms matched this post.') ->assertFailed(); expect($this->post->fresh()->status)->toBe(PostStatus::PartiallyPublished); Bus::assertNotDispatched(PublishToSocialPlatform::class); }); test('it fails when the post does not exist', function () { Bus::fake([PublishToSocialPlatform::class]); $this->artisan('posts:retry', ['post' => '019ff9ae-068b-72bf-9f2e-0314ce7dc0e2']) ->expectsOutput('Post not found.') ->assertFailed(); Bus::assertNotDispatched(PublishToSocialPlatform::class); }); test('a TikTok retry with a publish_id resumes instead of calling init', function () { $this->post->update([ 'media' => [[ 'id' => 'test-media-video', 'path' => 'media/2026-01/test-video.mp4', 'url' => 'https://example.com/media/2026-01/test-video.mp4', 'mime_type' => 'video/mp4', 'original_filename' => 'test-video.mp4', ]], ]); $failedTikTok = PostPlatform::factory()->tiktok()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->tiktok()->create([ 'workspace_id' => $this->workspace->id, 'username' => 'tiktoker', 'token_expires_at' => now()->addDay(), ]), 'error_context' => [ 'tiktok_publish_id' => 'pub_existing', 'retry_count' => 120, 'max_retries' => 120, 'category' => 'platform_unavailable', ], ]); Mail::fake(); Queue::fake(); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->assertSuccessful(); $api = config('trypost.platforms.tiktok.api'); Http::fake([ $api.'/post/publish/status/fetch/' => Http::response([ 'data' => [ 'status' => 'PUBLISH_COMPLETE', 'publicaly_available_post_id' => ['video_123'], ], ]), ]); (new PublishToSocialPlatform($failedTikTok->fresh()))->handle(); expect($failedTikTok->fresh()->status)->toBe(PlatformStatus::Published) ->and($failedTikTok->fresh()->platform_post_id)->toBe('video_123'); Http::assertNotSent(fn ($request) => str_contains($request->url(), '/init/')); }); test('an Instagram retry with a workflow resumes instead of creating a container', function () { $this->post->update([ 'media' => [[ 'id' => 'test-media-id', 'path' => 'media/2026-01/test-image.jpg', 'url' => 'https://example.com/media/2026-01/test-image.jpg', 'mime_type' => 'image/jpeg', 'original_filename' => 'test.jpg', ]], ]); $failedInstagram = PostPlatform::factory()->instagram()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->instagram()->create([ 'workspace_id' => $this->workspace->id, 'platform_user_id' => 'ig_123456789', 'token_expires_at' => now()->addDays(60), ]), 'content_type' => ContentType::InstagramFeed, 'error_context' => [ 'instagram_workflow' => [ 'stage' => 'final_container', 'container_id' => 'container-123', ], 'retry_count' => 90, 'max_retries' => 90, 'category' => 'platform_unavailable', ], ]); Mail::fake(); Queue::fake(); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->assertSuccessful(); Http::fake([ 'https://graph.instagram.com/v25.0/container-123*' => Http::response(['status_code' => 'FINISHED'], 200), 'https://graph.instagram.com/v25.0/ig_123456789/media_publish' => Http::response(['id' => 'media-123456789'], 200), 'https://graph.instagram.com/v25.0/media-123456789*' => Http::response([ 'permalink' => 'https://www.instagram.com/p/ABC123/', ], 200), ]); (new PublishToSocialPlatform($failedInstagram->fresh()))->handle(); expect($failedInstagram->fresh()->status)->toBe(PlatformStatus::Published) ->and($failedInstagram->fresh()->platform_post_id)->toBe('media-123456789'); Http::assertNotSent(fn ($request) => $request->method() === 'POST' && str_ends_with($request->url(), '/ig_123456789/media')); }); test('it treats an empty TikTok publish_id as a new attempt', function () { Bus::fake([PublishToSocialPlatform::class]); Storage::fake(); $derivativePath = 'social-tiktok-photos/123e4567-e89b-12d3-a456-426614174000.jpg'; Storage::put($derivativePath, 'temporary image'); $failedTikTok = PostPlatform::factory()->tiktok()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->tiktok()->create([ 'workspace_id' => $this->workspace->id, ]), 'error_context' => [ 'tiktok_publish_id' => '', 'tiktok_derivative_paths' => [$derivativePath], 'category' => 'unknown', ], ]); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->expectsOutputToContain('New') ->assertSuccessful(); Storage::assertMissing($derivativePath); expect($failedTikTok->fresh()->status)->toBe(PlatformStatus::Pending) ->and($failedTikTok->fresh()->error_context)->toBeNull(); }); test('it treats an empty Instagram workflow as a new attempt', function () { Bus::fake([PublishToSocialPlatform::class]); $failedInstagram = PostPlatform::factory()->instagram()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->instagram()->create([ 'workspace_id' => $this->workspace->id, ]), 'error_context' => [ 'instagram_workflow' => [], 'category' => 'unknown', ], ]); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->expectsOutputToContain('New') ->assertSuccessful(); expect($failedInstagram->fresh()->status)->toBe(PlatformStatus::Pending) ->and($failedInstagram->fresh()->error_context)->toBeNull(); }); test('it keeps TikTok and Instagram checkpoints independently on the same post', function () { Bus::fake([PublishToSocialPlatform::class]); $workflow = [ 'stage' => 'final_container', 'container_id' => 'container-123', ]; $failedTikTok = PostPlatform::factory()->tiktok()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->tiktok()->create([ 'workspace_id' => $this->workspace->id, ]), 'error_context' => [ 'tiktok_publish_id' => 'pub_existing', 'retry_count' => 12, 'category' => 'platform_unavailable', ], ]); $failedInstagram = PostPlatform::factory()->instagram()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->instagram()->create([ 'workspace_id' => $this->workspace->id, ]), 'error_context' => [ 'instagram_workflow' => $workflow, 'retry_count' => 8, 'category' => 'timeout', ], ]); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->expectsOutputToContain('Resume') ->assertSuccessful(); expect($failedTikTok->fresh()->error_context)->toBe([ 'tiktok_publish_id' => 'pub_existing', ])->and($failedInstagram->fresh()->error_context)->toBe([ 'instagram_workflow' => $workflow, ]); }); test('it starts over when the failure category is not resumable', function (?string $category) { Bus::fake([PublishToSocialPlatform::class]); $errorContext = ['tiktok_publish_id' => 'pub_dead']; if ($category !== null) { $errorContext['category'] = $category; } $failedTikTok = PostPlatform::factory()->tiktok()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->tiktok()->create([ 'workspace_id' => $this->workspace->id, ]), 'error_context' => $errorContext, ]); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->expectsOutputToContain('New') ->assertSuccessful(); expect($failedTikTok->fresh()->status)->toBe(PlatformStatus::Pending) ->and($failedTikTok->fresh()->error_context)->toBeNull(); })->with([ 'media format' => ['media_format'], 'content policy' => ['content_policy'], 'server error' => ['server_error'], 'permission' => ['permission'], 'rate limit' => ['rate_limit'], 'unknown' => ['unknown'], 'missing category' => [null], ]); test('a TikTok retry after a remote FAILED starts a new publish', function () { $this->post->update([ 'media' => [[ 'id' => 'test-media-video', 'path' => 'media/2026-01/test-video.mp4', 'url' => 'https://example.com/media/2026-01/test-video.mp4', 'mime_type' => 'video/mp4', 'original_filename' => 'test-video.mp4', ]], ]); $failedTikTok = PostPlatform::factory()->tiktok()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->tiktok()->create([ 'workspace_id' => $this->workspace->id, 'username' => 'tiktoker', 'token_expires_at' => now()->addDay(), ]), 'error_context' => [ 'tiktok_publish_id' => 'pub_dead', 'category' => 'media_format', ], ]); Mail::fake(); Queue::fake(); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->expectsOutputToContain('New') ->assertSuccessful(); expect($failedTikTok->fresh()->error_context)->toBeNull(); $api = config('trypost.platforms.tiktok.api'); Http::fake([ $api.'/post/publish/video/init/' => Http::response([ 'data' => ['publish_id' => 'pub_fresh'], ], 200), $api.'/post/publish/status/fetch/' => Http::response([ 'data' => [ 'status' => 'PUBLISH_COMPLETE', 'publicaly_available_post_id' => ['video_456'], ], ]), ]); (new PublishToSocialPlatform($failedTikTok->fresh()))->handle(); expect($failedTikTok->fresh()->status)->toBe(PlatformStatus::Published) ->and($failedTikTok->fresh()->platform_post_id)->toBe('video_456'); Http::assertSent(fn ($request) => str_contains($request->url(), '/init/')); }); test('an Instagram retry after a container ERROR starts a new container', function () { $this->post->update([ 'media' => [[ 'id' => 'test-media-id', 'path' => 'media/2026-01/test-image.jpg', 'url' => 'https://example.com/media/2026-01/test-image.jpg', 'mime_type' => 'image/jpeg', 'original_filename' => 'test.jpg', ]], ]); $failedInstagram = PostPlatform::factory()->instagram()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->instagram()->create([ 'workspace_id' => $this->workspace->id, 'platform_user_id' => 'ig_123456789', 'token_expires_at' => now()->addDays(60), ]), 'content_type' => ContentType::InstagramFeed, 'error_context' => [ 'instagram_workflow' => [ 'stage' => 'final_container', 'container_id' => 'container-dead', ], 'category' => 'server_error', ], ]); Mail::fake(); Queue::fake(); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->expectsOutputToContain('New') ->assertSuccessful(); expect($failedInstagram->fresh()->error_context)->toBeNull(); Http::fake([ 'https://graph.instagram.com/v25.0/ig_123456789/media' => Http::response(['id' => 'container-fresh'], 200), 'https://graph.instagram.com/v25.0/container-fresh*' => Http::response(['status_code' => 'FINISHED'], 200), 'https://graph.instagram.com/v25.0/ig_123456789/media_publish' => Http::response(['id' => 'media-456'], 200), 'https://graph.instagram.com/v25.0/media-456*' => Http::response([ 'permalink' => 'https://www.instagram.com/p/DEF456/', ], 200), ]); (new PublishToSocialPlatform($failedInstagram->fresh()))->handle(); expect($failedInstagram->fresh()->status)->toBe(PlatformStatus::Published) ->and($failedInstagram->fresh()->platform_post_id)->toBe('media-456'); Http::assertSent(fn ($request) => $request->method() === 'POST' && str_ends_with($request->url(), '/ig_123456789/media')); Http::assertNotSent(fn ($request) => str_contains($request->url(), 'container-dead')); }); test('an Instagram retry after a container EXPIRED starts a new container', function () { $this->post->update([ 'media' => [[ 'id' => 'test-media-id', 'path' => 'media/2026-01/test-image.jpg', 'url' => 'https://example.com/media/2026-01/test-image.jpg', 'mime_type' => 'image/jpeg', 'original_filename' => 'test.jpg', ]], ]); $failedInstagram = PostPlatform::factory()->instagram()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->instagram()->create([ 'workspace_id' => $this->workspace->id, 'platform_user_id' => 'ig_123456789', 'token_expires_at' => now()->addDays(60), ]), 'content_type' => ContentType::InstagramFeed, 'error_context' => [ 'instagram_workflow' => [ 'stage' => 'final_container', 'container_id' => 'container-expired', ], 'category' => 'platform_unavailable', ], ]); Mail::fake(); Queue::fake(); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->expectsOutputToContain('Resume') ->assertSuccessful(); Http::fake([ 'https://graph.instagram.com/v25.0/container-expired*' => Http::response(['status_code' => 'EXPIRED'], 200), ]); (new PublishToSocialPlatform($failedInstagram->fresh()))->handle(); expect($failedInstagram->fresh()->status)->toBe(PlatformStatus::Failed) ->and($failedInstagram->fresh()->error_context['category'] ?? null)->toBe('server_error'); Http::assertNotSent(fn ($request) => str_contains($request->url(), '/media_publish')); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->expectsOutputToContain('New') ->assertSuccessful(); expect($failedInstagram->fresh()->error_context)->toBeNull(); Http::fake([ 'https://graph.instagram.com/v25.0/ig_123456789/media' => Http::response(['id' => 'container-fresh'], 200), 'https://graph.instagram.com/v25.0/container-fresh*' => Http::response(['status_code' => 'FINISHED'], 200), 'https://graph.instagram.com/v25.0/ig_123456789/media_publish' => Http::response(['id' => 'media-789'], 200), 'https://graph.instagram.com/v25.0/media-789*' => Http::response([ 'permalink' => 'https://www.instagram.com/p/GHI789/', ], 200), ]); (new PublishToSocialPlatform($failedInstagram->fresh()))->handle(); expect($failedInstagram->fresh()->status)->toBe(PlatformStatus::Published) ->and($failedInstagram->fresh()->platform_post_id)->toBe('media-789'); Http::assertSent(fn ($request) => $request->method() === 'POST' && str_ends_with($request->url(), '/ig_123456789/media')); Http::assertNotSent(fn ($request) => str_contains($request->url(), 'container-expired')); }); test('an Instagram resume of a published container completes without media_publish', function () { $failedInstagram = PostPlatform::factory()->instagram()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->instagram()->create([ 'workspace_id' => $this->workspace->id, 'platform_user_id' => 'ig_123456789', 'token_expires_at' => now()->addDays(60), ]), 'content_type' => ContentType::InstagramFeed, 'error_context' => [ 'instagram_workflow' => [ 'stage' => 'final_container', 'container_id' => 'container-123', ], 'category' => 'platform_unavailable', ], ]); Mail::fake(); Queue::fake(); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->expectsOutputToContain('Resume') ->assertSuccessful(); Http::fake(function (Request $request) { if ($request->method() === 'GET' && str_contains($request->url(), '/container-123')) { return Http::response(['status_code' => 'PUBLISHED'], 200); } if ($request->method() === 'GET' && str_contains($request->url(), '/ig_123456789/media')) { return Http::response([ 'data' => [[ 'id' => 'other-account-post', 'permalink' => 'https://www.instagram.com/p/WRONG/', ]], ], 200); } return Http::response(['error' => ['message' => 'unexpected']], 500); }); (new PublishToSocialPlatform($failedInstagram->fresh()))->handle(); expect($failedInstagram->fresh()->status)->toBe(PlatformStatus::Published) ->and($failedInstagram->fresh()->platform_post_id)->toBe('container-123') ->and($failedInstagram->fresh()->platform_url)->toBeNull(); Http::assertNotSent(fn ($request) => str_contains($request->url(), '/ig_123456789/media')); Http::assertNotSent(fn ($request) => str_contains($request->url(), '/media_publish')); Http::assertNotSent(fn ($request) => $request->method() === 'POST'); }); test('an Instagram resume of a checkpointed media id completes without media_publish', function () { $failedInstagram = PostPlatform::factory()->instagram()->failed()->create([ 'post_id' => $this->post->id, 'social_account_id' => SocialAccount::factory()->instagram()->create([ 'workspace_id' => $this->workspace->id, 'platform_user_id' => 'ig_123456789', 'token_expires_at' => now()->addDays(60), ]), 'content_type' => ContentType::InstagramFeed, 'error_context' => [ 'instagram_workflow' => [ 'stage' => 'final_container', 'container_id' => 'container-123', 'media_id' => 'media-persisted', ], 'category' => 'job_failed', ], ]); Mail::fake(); Queue::fake(); $this->artisan('posts:retry', ['post' => $this->post->id]) ->expectsConfirmation('Queue publish attempts for these failed platforms?', 'yes') ->expectsOutputToContain('Resume') ->assertSuccessful(); Http::fake(function (Request $request) { if ($request->method() === 'GET' && str_contains($request->url(), '/media-persisted')) { return Http::response([ 'permalink' => 'https://www.instagram.com/p/PERSISTED/', ], 200); } return Http::response(['error' => ['message' => 'unexpected']], 500); }); (new PublishToSocialPlatform($failedInstagram->fresh()))->handle(); expect($failedInstagram->fresh()->status)->toBe(PlatformStatus::Published) ->and($failedInstagram->fresh()->platform_post_id)->toBe('media-persisted') ->and($failedInstagram->fresh()->platform_url)->toBe('https://www.instagram.com/p/PERSISTED/'); Http::assertNotSent(fn ($request) => str_contains($request->url(), '/container-123')); Http::assertNotSent(fn ($request) => str_contains($request->url(), '/media_publish')); Http::assertNotSent(fn ($request) => $request->method() === 'POST'); });