test(tiktok): cover cleanup edge cases and fake against the config base URL
- Multi-image carousel: only the oversized photo is resized, order preserved, the compliant one passes through untouched. - Cleanup runs even when publishing fails (no orphaned derivative). - A throwing Storage::delete does not turn a successful publish into a failure. - Replace hardcoded https://open.tiktokapis.com/v2 with config('trypost.platforms.tiktok.api') so the fakes can't drift from the configured (overridable) host.
This commit is contained in:
parent
3cf9af9caf
commit
5df854092b
1 changed files with 201 additions and 41 deletions
|
|
@ -41,6 +41,8 @@
|
|||
]);
|
||||
|
||||
$this->publisher = new TikTokPublisher;
|
||||
|
||||
$this->api = config('trypost.platforms.tiktok.api');
|
||||
});
|
||||
|
||||
test('tiktok publisher throws exception when no media', function () {
|
||||
|
|
@ -62,10 +64,10 @@
|
|||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/' => Http::response([
|
||||
$this->api.'/post/publish/video/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_123'],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/' => Http::response([
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => [
|
||||
'status' => 'PUBLISH_COMPLETE',
|
||||
'publish_id' => 'pub_123',
|
||||
|
|
@ -100,10 +102,10 @@
|
|||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/post/publish/content/init/' => Http::response([
|
||||
$this->api.'/post/publish/content/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_photo_123'],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/' => Http::response([
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => [
|
||||
'status' => 'PUBLISH_COMPLETE',
|
||||
'publish_id' => 'pub_photo_123',
|
||||
|
|
@ -141,7 +143,7 @@
|
|||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/' => Http::response([
|
||||
$this->api.'/post/publish/video/init/' => Http::response([
|
||||
'error' => [
|
||||
'code' => 'invalid_request',
|
||||
'message' => 'Invalid request',
|
||||
|
|
@ -167,7 +169,7 @@
|
|||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/' => Http::response([
|
||||
$this->api.'/post/publish/video/init/' => Http::response([
|
||||
'error' => [
|
||||
'code' => 'access_token_invalid',
|
||||
'message' => 'Access token is invalid',
|
||||
|
|
@ -195,15 +197,15 @@
|
|||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/oauth/token/' => Http::response([
|
||||
$this->api.'/oauth/token/' => Http::response([
|
||||
'access_token' => 'new-access-token',
|
||||
'refresh_token' => 'new-refresh-token',
|
||||
'expires_in' => 86400,
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/' => Http::response([
|
||||
$this->api.'/post/publish/video/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_123'],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/' => Http::response([
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => ['status' => 'PUBLISH_COMPLETE'],
|
||||
], 200),
|
||||
]);
|
||||
|
|
@ -256,7 +258,7 @@
|
|||
]);
|
||||
|
||||
Http::fake([
|
||||
'*/oauth/token/' => Http::response([
|
||||
$this->api.'/oauth/token/' => Http::response([
|
||||
'error' => ['code' => 'invalid_grant', 'message' => 'Refresh token expired'],
|
||||
], 400),
|
||||
]);
|
||||
|
|
@ -296,10 +298,10 @@
|
|||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/' => Http::response([
|
||||
$this->api.'/post/publish/video/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_123'],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/' => Http::response([
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => ['status' => 'PUBLISH_COMPLETE'],
|
||||
], 200),
|
||||
]);
|
||||
|
|
@ -325,10 +327,10 @@
|
|||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/' => Http::response([
|
||||
$this->api.'/post/publish/video/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_123'],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/' => Http::response([
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => ['status' => 'PUBLISH_COMPLETE'],
|
||||
], 200),
|
||||
]);
|
||||
|
|
@ -356,13 +358,13 @@
|
|||
|
||||
Http::fake([
|
||||
// creator_info/query returns 500 — should not affect publishing since user picked privacy_level.
|
||||
'https://open.tiktokapis.com/v2/post/publish/creator_info/query/' => Http::response([
|
||||
$this->api.'/post/publish/creator_info/query/' => Http::response([
|
||||
'error' => ['code' => 'internal_error', 'message' => 'Internal server error'],
|
||||
], 500),
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/' => Http::response([
|
||||
$this->api.'/post/publish/video/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_fallback_123'],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/' => Http::response([
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => [
|
||||
'status' => 'PUBLISH_COMPLETE',
|
||||
'publish_id' => 'pub_fallback_123',
|
||||
|
|
@ -400,10 +402,10 @@
|
|||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/' => Http::response([
|
||||
$this->api.'/post/publish/video/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_123'],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/' => Http::response([
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => [
|
||||
'status' => 'FAILED',
|
||||
'fail_reason' => 'video_rejected',
|
||||
|
|
@ -441,15 +443,15 @@
|
|||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/post/publish/creator_info/query/' => Http::response([
|
||||
$this->api.'/post/publish/creator_info/query/' => Http::response([
|
||||
'data' => [
|
||||
'privacy_level_options' => ['PUBLIC_TO_EVERYONE', 'SELF_ONLY'],
|
||||
],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/' => Http::response([
|
||||
$this->api.'/post/publish/video/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_meta_123'],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/' => Http::response([
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => ['status' => 'PUBLISH_COMPLETE'],
|
||||
], 200),
|
||||
]);
|
||||
|
|
@ -496,13 +498,13 @@
|
|||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/post/publish/creator_info/query/' => Http::response([
|
||||
$this->api.'/post/publish/creator_info/query/' => Http::response([
|
||||
'data' => ['privacy_level_options' => ['SELF_ONLY']],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/content/init/' => Http::response([
|
||||
$this->api.'/post/publish/content/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_music_123'],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/' => Http::response([
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => ['status' => 'PUBLISH_COMPLETE'],
|
||||
], 200),
|
||||
]);
|
||||
|
|
@ -546,13 +548,13 @@
|
|||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/post/publish/creator_info/query/' => Http::response([
|
||||
$this->api.'/post/publish/creator_info/query/' => Http::response([
|
||||
'data' => ['privacy_level_options' => ['SELF_ONLY']],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/' => Http::response([
|
||||
$this->api.'/post/publish/video/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_vid_123'],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/' => Http::response([
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => ['status' => 'PUBLISH_COMPLETE'],
|
||||
], 200),
|
||||
]);
|
||||
|
|
@ -587,15 +589,15 @@
|
|||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/post/publish/creator_info/query/' => Http::response([
|
||||
$this->api.'/post/publish/creator_info/query/' => Http::response([
|
||||
'data' => [
|
||||
'privacy_level_options' => ['PUBLIC_TO_EVERYONE', 'FOLLOWER_OF_CREATOR', 'SELF_ONLY'],
|
||||
],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/' => Http::response([
|
||||
$this->api.'/post/publish/video/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_default_123'],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/' => Http::response([
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => ['status' => 'PUBLISH_COMPLETE'],
|
||||
], 200),
|
||||
]);
|
||||
|
|
@ -636,15 +638,15 @@
|
|||
$this->postPlatform->update(['meta' => ['privacy_level' => 'SELF_ONLY']]);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/post/publish/creator_info/query/' => Http::response([
|
||||
$this->api.'/post/publish/creator_info/query/' => Http::response([
|
||||
'data' => [
|
||||
'privacy_level_options' => ['SELF_ONLY'],
|
||||
],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/video/init/' => Http::response([
|
||||
$this->api.'/post/publish/video/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_video_123'],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/' => Http::response([
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => ['status' => 'PUBLISH_COMPLETE', 'publish_id' => 'pub_video_123'],
|
||||
], 200),
|
||||
]);
|
||||
|
|
@ -679,7 +681,7 @@
|
|||
|
||||
Http::fake([
|
||||
// creator_info returns a healthy response — fallback would have silently picked PUBLIC_TO_EVERYONE.
|
||||
'https://open.tiktokapis.com/v2/post/publish/creator_info/query/' => Http::response([
|
||||
$this->api.'/post/publish/creator_info/query/' => Http::response([
|
||||
'data' => [
|
||||
'creator_nickname' => 'test',
|
||||
'creator_username' => 'test',
|
||||
|
|
@ -725,10 +727,10 @@
|
|||
app()->instance(MediaOptimizer::class, $mockOptimizer);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/post/publish/content/init/' => Http::response([
|
||||
$this->api.'/post/publish/content/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_resize_123'],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/' => Http::response([
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => ['status' => 'PUBLISH_COMPLETE'],
|
||||
], 200),
|
||||
'*' => Http::response('fake-image-content', 200),
|
||||
|
|
@ -769,10 +771,10 @@
|
|||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/post/publish/content/init/' => Http::response([
|
||||
$this->api.'/post/publish/content/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_passthrough_123'],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/' => Http::response([
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => ['status' => 'PUBLISH_COMPLETE'],
|
||||
], 200),
|
||||
]);
|
||||
|
|
@ -821,10 +823,10 @@
|
|||
app()->instance(MediaOptimizer::class, $mockOptimizer);
|
||||
|
||||
Http::fake([
|
||||
'https://open.tiktokapis.com/v2/post/publish/content/init/' => Http::response([
|
||||
$this->api.'/post/publish/content/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_unknown_123'],
|
||||
], 200),
|
||||
'https://open.tiktokapis.com/v2/post/publish/status/fetch/' => Http::response([
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => ['status' => 'PUBLISH_COMPLETE'],
|
||||
], 200),
|
||||
'*' => Http::response('fake-image-content', 200),
|
||||
|
|
@ -879,3 +881,161 @@
|
|||
// Nothing should be left hosted when resizing never completes.
|
||||
expect(Storage::allFiles('social-tiktok-photos'))->toBeEmpty();
|
||||
});
|
||||
|
||||
test('tiktok publisher resizes only the oversized photos in a mixed carousel', function () {
|
||||
Storage::fake();
|
||||
|
||||
// TikTok carousels can carry many images; here one is oversized, one is compliant.
|
||||
$this->postPlatform->update(['meta' => ['privacy_level' => 'SELF_ONLY']]);
|
||||
$this->post->update([
|
||||
'media' => [
|
||||
[
|
||||
'id' => 'oversized',
|
||||
'path' => 'media/2026-01/big.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/big.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'big.jpg',
|
||||
'meta' => ['width' => 1254, 'height' => 1254],
|
||||
],
|
||||
[
|
||||
'id' => 'compliant',
|
||||
'path' => 'media/2026-01/small.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/small.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'small.jpg',
|
||||
'meta' => ['width' => 1080, 'height' => 1080],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$mockOptimizer = Mockery::mock(MediaOptimizer::class);
|
||||
$mockOptimizer->shouldReceive('maxWidthForPlatform')->with(Platform::TikTok)->andReturn(1080);
|
||||
// optimizeImage must run exactly once — only for the oversized image.
|
||||
$mockOptimizer->shouldReceive('optimizeImage')->once()->with(Mockery::type('string'), Platform::TikTok)->andReturnUsing(function (string $tempFile) {
|
||||
$optimized = tempnam(sys_get_temp_dir(), 'tt_opt_');
|
||||
copy($tempFile, $optimized);
|
||||
|
||||
return $optimized;
|
||||
});
|
||||
app()->instance(MediaOptimizer::class, $mockOptimizer);
|
||||
|
||||
Http::fake([
|
||||
$this->api.'/post/publish/content/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_mixed_123'],
|
||||
], 200),
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => ['status' => 'PUBLISH_COMPLETE'],
|
||||
], 200),
|
||||
'*' => Http::response('fake-image-content', 200),
|
||||
]);
|
||||
|
||||
$this->publisher->publish($this->postPlatform);
|
||||
|
||||
// Order is preserved: oversized -> hosted derivative, compliant -> original URL untouched.
|
||||
Http::assertSent(function ($request) {
|
||||
if (! str_contains($request->url(), '/post/publish/content/init/')) {
|
||||
return false;
|
||||
}
|
||||
$images = data_get(json_decode($request->body(), true), 'source_info.photo_images');
|
||||
|
||||
return is_array($images)
|
||||
&& count($images) === 2
|
||||
&& str_contains($images[0], 'social-tiktok-photos/')
|
||||
&& ! str_contains($images[0], 'example.com')
|
||||
&& $images[1] === 'https://example.com/media/2026-01/small.jpg';
|
||||
});
|
||||
|
||||
// The compliant image is never downloaded; only the oversized one is fetched for resizing.
|
||||
Http::assertNotSent(fn ($request) => str_contains($request->url(), 'small.jpg'));
|
||||
|
||||
// The single derivative is pruned after publish.
|
||||
expect(Storage::allFiles('social-tiktok-photos'))->toBeEmpty();
|
||||
});
|
||||
|
||||
test('tiktok publisher prunes the hosted derivative even when publishing fails', function () {
|
||||
Storage::fake();
|
||||
|
||||
$this->postPlatform->update(['meta' => ['privacy_level' => 'SELF_ONLY']]);
|
||||
$this->post->update([
|
||||
'media' => [
|
||||
[
|
||||
'id' => 'oversized',
|
||||
'path' => 'media/2026-01/big.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/big.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'big.jpg',
|
||||
'meta' => ['width' => 1254, 'height' => 1254],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$mockOptimizer = Mockery::mock(MediaOptimizer::class);
|
||||
$mockOptimizer->shouldReceive('maxWidthForPlatform')->with(Platform::TikTok)->andReturn(1080);
|
||||
$mockOptimizer->shouldReceive('optimizeImage')->with(Mockery::type('string'), Platform::TikTok)->andReturnUsing(function (string $tempFile) {
|
||||
$optimized = tempnam(sys_get_temp_dir(), 'tt_opt_');
|
||||
copy($tempFile, $optimized);
|
||||
|
||||
return $optimized;
|
||||
});
|
||||
app()->instance(MediaOptimizer::class, $mockOptimizer);
|
||||
|
||||
// The derivative is hosted first, then TikTok rejects the publish: the finally
|
||||
// must still remove it so a failed post never orphans a file on disk.
|
||||
Http::fake([
|
||||
$this->api.'/post/publish/content/init/' => Http::response([
|
||||
'error' => ['code' => 'internal_error', 'message' => 'boom'],
|
||||
], 500),
|
||||
'*' => Http::response('fake-image-content', 200),
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(TikTokPublishException::class);
|
||||
|
||||
expect(Storage::allFiles('social-tiktok-photos'))->toBeEmpty();
|
||||
});
|
||||
|
||||
test('tiktok publisher still reports success when derivative cleanup throws on the storage disk', function () {
|
||||
// The production default disk (r2) is configured to throw on a failed delete.
|
||||
// Cleanup must never turn an already-published post into a reported failure.
|
||||
$this->postPlatform->update(['meta' => ['privacy_level' => 'SELF_ONLY']]);
|
||||
$this->post->update([
|
||||
'media' => [
|
||||
[
|
||||
'id' => 'oversized',
|
||||
'path' => 'media/2026-01/big.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/big.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'big.jpg',
|
||||
'meta' => ['width' => 1254, 'height' => 1254],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$mockOptimizer = Mockery::mock(MediaOptimizer::class);
|
||||
$mockOptimizer->shouldReceive('maxWidthForPlatform')->with(Platform::TikTok)->andReturn(1080);
|
||||
$mockOptimizer->shouldReceive('optimizeImage')->with(Mockery::type('string'), Platform::TikTok)->andReturnUsing(function (string $tempFile) {
|
||||
$optimized = tempnam(sys_get_temp_dir(), 'tt_opt_');
|
||||
copy($tempFile, $optimized);
|
||||
|
||||
return $optimized;
|
||||
});
|
||||
app()->instance(MediaOptimizer::class, $mockOptimizer);
|
||||
|
||||
Storage::shouldReceive('put')->andReturnTrue();
|
||||
Storage::shouldReceive('url')->andReturn('https://cdn.example.com/social-tiktok-photos/x.jpg');
|
||||
Storage::shouldReceive('delete')->andThrow(new RuntimeException('r2 delete failed'));
|
||||
|
||||
Http::fake([
|
||||
$this->api.'/post/publish/content/init/' => Http::response([
|
||||
'data' => ['publish_id' => 'pub_cleanup_throws_123'],
|
||||
], 200),
|
||||
$this->api.'/post/publish/status/fetch/' => Http::response([
|
||||
'data' => ['status' => 'PUBLISH_COMPLETE'],
|
||||
], 200),
|
||||
'*' => Http::response('fake-image-content', 200),
|
||||
]);
|
||||
|
||||
$result = $this->publisher->publish($this->postPlatform);
|
||||
|
||||
expect($result['id'])->toBe('pub_cleanup_throws_123');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue