trypost/tests/Unit/Enums/PlatformTest.php
Maciej Dzierżek ca497e1f3c
perf(horizon): skip queues for platforms that are disabled (#321)
* perf(horizon): skip queues for platforms that are disabled

The social-publishing supervisor listens on Platform::allQueues(), which maps
over every enum case regardless of the per-platform *_ENABLED toggles. With
minProcesses => 1 that means one worker per supported platform - even on an
installation that only ever connects two or three of them.

On a single-workspace self-host that was 19 Horizon workers at roughly 75 MB
each; filtering by the toggles brought it to 9 and cut container memory from
1.66 GB to 1.06 GB, with no change in publishing behaviour.

Note on the implementation: the filter reads env() directly rather than calling
Platform::isEnabled(). Config files load alphabetically, so config('trypost.*')
does not exist yet while horizon.php is evaluated - isEnabled() would silently
return its default of true and the filter would be a no-op. This bites at
config:cache time, so it is invisible in tinker.

* refactor(horizon): filter disabled platform queues via enum

Move queue filtering to Platform::enabledQueues() and apply it from
AppServiceProvider after config has loaded, avoiding env() parsing in
horizon.php while keeping isEnabled() as the single source of truth.

* refactor(horizon): use enabledQueues directly in horizon config

Remove AppServiceProvider boot override and let isEnabled() fall back
to env when trypost config is not loaded yet.

* chore: enable Eloquent strict mode in all environments

* refactor(platform): replace enabled env key derivation with explicit match

* refactor(platform): simplify isEnabled using config default fallback

* test(platform): cover publishing queues and enabled toggles exhaustively

* refactor(platform): collapse isEnabled env fallback into one method

* refactor(platform): drop filter_var and rely on env boolean casting

* revert: keep Eloquent strict mode out of production

shouldBeStrict() in production would throw on lazy loads in queued
publish jobs and can stop posting. Restore the Laravel default.

---------

Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
2026-09-03 13:51:03 -03:00

365 lines
15 KiB
PHP

<?php
declare(strict_types=1);
use App\Enums\Media\Type as MediaType;
use App\Enums\SocialAccount\Platform;
test('platform has correct labels', function () {
expect(Platform::LinkedIn->label())->toBe('LinkedIn');
expect(Platform::LinkedInPage->label())->toBe('LinkedIn Page');
expect(Platform::X->label())->toBe('X');
expect(Platform::TikTok->label())->toBe('TikTok');
expect(Platform::YouTube->label())->toBe('YouTube Shorts');
expect(Platform::Facebook->label())->toBe('Facebook Page');
expect(Platform::Instagram->label())->toBe('Instagram');
expect(Platform::InstagramFacebook->label())->toBe('Instagram (Facebook Business)');
expect(Platform::Threads->label())->toBe('Threads');
expect(Platform::Pinterest->label())->toBe('Pinterest');
expect(Platform::Bluesky->label())->toBe('Bluesky');
expect(Platform::Mastodon->label())->toBe('Mastodon');
});
test('platform has correct colors', function () {
expect(Platform::LinkedIn->color())->toBe('#0A66C2');
expect(Platform::LinkedInPage->color())->toBe('#0A66C2');
expect(Platform::X->color())->toBe('#000000');
expect(Platform::TikTok->color())->toBe('#000000');
expect(Platform::YouTube->color())->toBe('#FF0000');
expect(Platform::Facebook->color())->toBe('#1877F2');
expect(Platform::Instagram->color())->toBe('#E4405F');
expect(Platform::Threads->color())->toBe('#000000');
expect(Platform::Pinterest->color())->toBe('#E60023');
expect(Platform::Bluesky->color())->toBe('#0085FF');
expect(Platform::Mastodon->color())->toBe('#6364FF');
});
test('platform has correct allowed media types', function () {
expect(Platform::LinkedIn->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video, MediaType::Document);
expect(Platform::LinkedInPage->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video, MediaType::Document);
expect(Platform::X->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video);
expect(Platform::X->allowedMediaTypes())->not->toContain(MediaType::Document);
expect(Platform::TikTok->allowedMediaTypes())->toBe([MediaType::Video]);
expect(Platform::YouTube->allowedMediaTypes())->toBe([MediaType::Video]);
expect(Platform::Instagram->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video);
});
test('platform has correct max images', function () {
expect(Platform::LinkedIn->maxImages())->toBe(10);
expect(Platform::X->maxImages())->toBe(4);
expect(Platform::TikTok->maxImages())->toBe(0);
expect(Platform::YouTube->maxImages())->toBe(0);
expect(Platform::Facebook->maxImages())->toBe(10);
expect(Platform::Instagram->maxImages())->toBe(10);
expect(Platform::Threads->maxImages())->toBe(10);
expect(Platform::Pinterest->maxImages())->toBe(5);
expect(Platform::Bluesky->maxImages())->toBe(4);
expect(Platform::Mastodon->maxImages())->toBe(4);
});
test('platform has correct max content length', function () {
expect(Platform::LinkedIn->maxContentLength())->toBe(3000);
expect(Platform::X->maxContentLength())->toBe(280);
expect(Platform::TikTok->maxContentLength())->toBe(2200);
expect(Platform::YouTube->maxContentLength())->toBe(100);
expect(Platform::Facebook->maxContentLength())->toBe(10000);
expect(Platform::Instagram->maxContentLength())->toBe(2200);
expect(Platform::Threads->maxContentLength())->toBe(500);
expect(Platform::Pinterest->maxContentLength())->toBe(800);
expect(Platform::Bluesky->maxContentLength())->toBe(300);
expect(Platform::Mastodon->maxContentLength())->toBe(500);
});
test('platform supports text only correctly', function () {
expect(Platform::LinkedIn->supportsTextOnly())->toBeTrue();
expect(Platform::LinkedInPage->supportsTextOnly())->toBeTrue();
expect(Platform::X->supportsTextOnly())->toBeTrue();
expect(Platform::Facebook->supportsTextOnly())->toBeTrue();
expect(Platform::Threads->supportsTextOnly())->toBeTrue();
expect(Platform::Bluesky->supportsTextOnly())->toBeTrue();
expect(Platform::Mastodon->supportsTextOnly())->toBeTrue();
expect(Platform::TikTok->supportsTextOnly())->toBeFalse();
expect(Platform::YouTube->supportsTextOnly())->toBeFalse();
expect(Platform::Instagram->supportsTextOnly())->toBeFalse();
expect(Platform::Pinterest->supportsTextOnly())->toBeFalse();
});
test('platform exposes the correct default token TTL fallback', function () {
// X access tokens live 2 hours.
expect(Platform::X->defaultTokenTtlSeconds())->toBe(7200);
// Instagram and Threads use Meta's 60-day long-lived token.
expect(Platform::Instagram->defaultTokenTtlSeconds())->toBe(5184000);
expect(Platform::Threads->defaultTokenTtlSeconds())->toBe(5184000);
// Networks that always return expires_in, set a fixed lifetime directly, or
// never expire have no fallback here.
expect(Platform::LinkedIn->defaultTokenTtlSeconds())->toBeNull();
expect(Platform::TikTok->defaultTokenTtlSeconds())->toBeNull();
expect(Platform::YouTube->defaultTokenTtlSeconds())->toBeNull();
expect(Platform::Pinterest->defaultTokenTtlSeconds())->toBeNull();
expect(Platform::Bluesky->defaultTokenTtlSeconds())->toBeNull();
expect(Platform::Facebook->defaultTokenTtlSeconds())->toBeNull();
});
test('platform is enabled by default for every platform', function (Platform $platform) {
expect($platform->isEnabled())->toBeTrue();
})->with([
Platform::LinkedIn,
Platform::LinkedInPage,
Platform::X,
Platform::TikTok,
Platform::YouTube,
Platform::Facebook,
Platform::Instagram,
Platform::InstagramFacebook,
Platform::Threads,
Platform::Pinterest,
Platform::Bluesky,
Platform::Mastodon,
Platform::Telegram,
Platform::Discord,
]);
test('each platform can be disabled via config', function (Platform $platform) {
config(["trypost.platforms.{$platform->value}.enabled" => false]);
expect($platform->isEnabled())->toBeFalse();
})->with([
Platform::LinkedIn,
Platform::LinkedInPage,
Platform::X,
Platform::TikTok,
Platform::YouTube,
Platform::Facebook,
Platform::Instagram,
Platform::InstagramFacebook,
Platform::Threads,
Platform::Pinterest,
Platform::Bluesky,
Platform::Mastodon,
Platform::Telegram,
Platform::Discord,
]);
test('each platform maps to its publishing queue', function (Platform $platform, string $queue) {
expect($platform->queue())->toBe($queue);
})->with([
[Platform::LinkedIn, 'social-linkedin'],
[Platform::LinkedInPage, 'social-linkedin-page'],
[Platform::X, 'social-x'],
[Platform::TikTok, 'social-tiktok'],
[Platform::YouTube, 'social-youtube'],
[Platform::Facebook, 'social-facebook'],
[Platform::Instagram, 'social-instagram'],
[Platform::InstagramFacebook, 'social-instagram-facebook'],
[Platform::Threads, 'social-threads'],
[Platform::Pinterest, 'social-pinterest'],
[Platform::Bluesky, 'social-bluesky'],
[Platform::Mastodon, 'social-mastodon'],
[Platform::Telegram, 'social-telegram'],
[Platform::Discord, 'social-discord'],
]);
test('allQueues lists every platform publishing queue in enum order', function () {
expect(Platform::allQueues())->toBe([
'social-linkedin',
'social-linkedin-page',
'social-x',
'social-tiktok',
'social-youtube',
'social-facebook',
'social-instagram',
'social-instagram-facebook',
'social-threads',
'social-pinterest',
'social-bluesky',
'social-mastodon',
'social-telegram',
'social-discord',
])->and(Platform::allQueues())->toHaveCount(count(Platform::cases()));
});
test('enabledQueues matches allQueues when every platform is enabled', function () {
foreach (Platform::cases() as $platform) {
config(["trypost.platforms.{$platform->value}.enabled" => true]);
}
expect(Platform::enabledQueues())->toBe(Platform::allQueues());
});
test('disabling a platform removes only its queue from enabledQueues', function (Platform $disabled) {
foreach (Platform::cases() as $platform) {
config(["trypost.platforms.{$platform->value}.enabled" => true]);
}
config(["trypost.platforms.{$disabled->value}.enabled" => false]);
$enabledQueues = Platform::enabledQueues();
expect($enabledQueues)
->not->toContain($disabled->queue())
->toHaveCount(count(Platform::cases()) - 1);
foreach (Platform::cases() as $platform) {
if ($platform === $disabled) {
continue;
}
expect($enabledQueues)->toContain($platform->queue());
}
expect(Platform::allQueues())->toContain($disabled->queue());
})->with([
Platform::LinkedIn,
Platform::LinkedInPage,
Platform::X,
Platform::TikTok,
Platform::YouTube,
Platform::Facebook,
Platform::Instagram,
Platform::InstagramFacebook,
Platform::Threads,
Platform::Pinterest,
Platform::Bluesky,
Platform::Mastodon,
Platform::Telegram,
Platform::Discord,
]);
test('disabling every platform yields no enabled queues', function () {
foreach (Platform::cases() as $platform) {
config(["trypost.platforms.{$platform->value}.enabled" => false]);
}
expect(Platform::enabledQueues())->toBe([]);
});
test('enabledQueues is always a subset of allQueues', function () {
config([
'trypost.platforms.linkedin.enabled' => true,
'trypost.platforms.linkedin-page.enabled' => false,
'trypost.platforms.x.enabled' => false,
'trypost.platforms.tiktok.enabled' => true,
'trypost.platforms.youtube.enabled' => false,
'trypost.platforms.facebook.enabled' => true,
'trypost.platforms.instagram.enabled' => false,
'trypost.platforms.instagram-facebook.enabled' => true,
'trypost.platforms.threads.enabled' => false,
'trypost.platforms.pinterest.enabled' => true,
'trypost.platforms.bluesky.enabled' => false,
'trypost.platforms.mastodon.enabled' => true,
'trypost.platforms.telegram.enabled' => false,
'trypost.platforms.discord.enabled' => true,
]);
$enabledQueues = Platform::enabledQueues();
expect($enabledQueues)->toEqual(array_values(array_intersect(Platform::allQueues(), $enabledQueues)))
->and(array_diff($enabledQueues, Platform::allQueues()))->toBe([]);
});
test('horizon social publishing queues match enabledQueues', function () {
expect(config('horizon.defaults.social-publishing.queue'))->toBe(Platform::enabledQueues());
});
test('isEnabled falls back to env when enabled config is missing', function (Platform $platform, string $envKey) {
$platforms = config('trypost.platforms');
unset($platforms[$platform->value]['enabled']);
config(['trypost.platforms' => $platforms]);
$original = getenv($envKey);
putenv("{$envKey}=false");
try {
expect($platform->isEnabled())->toBeFalse();
} finally {
if ($original === false) {
putenv($envKey);
} else {
putenv("{$envKey}={$original}");
}
}
})->with([
[Platform::LinkedIn, 'LINKEDIN_ENABLED'],
[Platform::LinkedInPage, 'LINKEDIN_PAGE_ENABLED'],
[Platform::X, 'X_ENABLED'],
[Platform::InstagramFacebook, 'INSTAGRAM_FACEBOOK_ENABLED'],
[Platform::Telegram, 'TELEGRAM_ENABLED'],
[Platform::Discord, 'DISCORD_ENABLED'],
]);
test('linkedin pages and instagram facebook are not directly connectable', function () {
expect(Platform::LinkedInPage->isConnectable())->toBeFalse();
expect(Platform::LinkedIn->isConnectable())->toBeTrue();
expect(Platform::InstagramFacebook->isConnectable())->toBeFalse();
expect(Platform::Instagram->isConnectable())->toBeTrue();
});
test('the linkedin card is connectable while either capability is enabled', function () {
config(['trypost.platforms.linkedin.enabled' => true, 'trypost.platforms.linkedin-page.enabled' => false]);
expect(Platform::LinkedIn->isConnectable())->toBeTrue();
config(['trypost.platforms.linkedin.enabled' => false, 'trypost.platforms.linkedin-page.enabled' => true]);
expect(Platform::LinkedIn->isConnectable())->toBeTrue();
config(['trypost.platforms.linkedin.enabled' => false, 'trypost.platforms.linkedin-page.enabled' => false]);
expect(Platform::LinkedIn->isConnectable())->toBeFalse();
// A company page never gets its own card regardless of toggles.
config(['trypost.platforms.linkedin-page.enabled' => true]);
expect(Platform::LinkedInPage->isConnectable())->toBeFalse();
});
test('the instagram card is connectable while either capability is enabled', function () {
config(['trypost.platforms.instagram.enabled' => true, 'trypost.platforms.instagram-facebook.enabled' => false]);
expect(Platform::Instagram->isConnectable())->toBeTrue();
config(['trypost.platforms.instagram.enabled' => false, 'trypost.platforms.instagram-facebook.enabled' => true]);
expect(Platform::Instagram->isConnectable())->toBeTrue();
config(['trypost.platforms.instagram.enabled' => false, 'trypost.platforms.instagram-facebook.enabled' => false]);
expect(Platform::Instagram->isConnectable())->toBeFalse();
// Via-Facebook never gets its own card regardless of toggles.
config(['trypost.platforms.instagram-facebook.enabled' => true]);
expect(Platform::InstagramFacebook->isConnectable())->toBeFalse();
});
test('connectable options are sorted alphabetically by label', function () {
$labels = array_column(Platform::connectableOptions(), 'label');
$sorted = $labels;
natcasesort($sorted);
expect($labels)->toBe(array_values($sorted));
});
test('connectable options include a single instagram card and omit the facebook variant', function () {
$values = array_column(Platform::connectableOptions(), 'value');
expect($values)->toContain(Platform::Instagram->value)
->and($values)->not->toContain(Platform::InstagramFacebook->value);
});
test('instagram connectable option lists only enabled connect methods', function () {
config(['trypost.platforms.instagram.enabled' => true, 'trypost.platforms.instagram-facebook.enabled' => false]);
expect(Platform::instagramConnectMethods())->toBe([Platform::Instagram->value]);
$instagram = collect(Platform::connectableOptions())->firstWhere('value', Platform::Instagram->value);
expect($instagram['connect_methods'])->toBe([Platform::Instagram->value]);
config(['trypost.platforms.instagram.enabled' => false, 'trypost.platforms.instagram-facebook.enabled' => true]);
expect(Platform::instagramConnectMethods())->toBe([Platform::InstagramFacebook->value]);
config(['trypost.platforms.instagram.enabled' => true, 'trypost.platforms.instagram-facebook.enabled' => true]);
expect(Platform::instagramConnectMethods())->toBe([
Platform::Instagram->value,
Platform::InstagramFacebook->value,
]);
});