2026-01-19 00:49:13 +00:00
|
|
|
<?php
|
|
|
|
|
|
refactor: settings redesign, Spanish translations, language system, strict_types
Settings pages:
- Redesign layout to match Sendkit (max-w-4xl, space-y-12, Separator sections)
- Merge Members page into Workspace settings with Table, invite Dialog, ConfirmDeleteModal
- Add workspace logo upload/delete routes and controller methods
- Translate all hardcoded strings in Workspace.vue modals
Language system:
- Drop languages table, replace language_id FK with locale string column on users
- Create config/languages.php for available languages and default locale
- Add Spanish (es) translations (13 files)
- Simplify HandleInertiaRequests, ProfileController, RegisteredUserController
Code quality:
- Add declare(strict_types=1) to all PHP files
- Fix MastodonPublisher using wrong attribute (filename -> original_filename)
- Fix HasMediaTest for new has_photo/photo_url accessors
- Fix PublishToSocialPlatformTest type error revealed by strict_types
- Remove orphaned Language model from AppServiceProvider morph map
- Update User TypeScript interface (has_photo, photo_url, locale)
- Eager load media relation on workspaces to prevent N+1
- Add 8 new tests for workspace logo upload/delete
- Update workspace settings test to assert members/invitations props
All 710 tests passing.
2026-03-30 03:20:43 +00:00
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
2026-01-19 00:49:13 +00:00
|
|
|
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');
|
2026-04-02 20:57:06 +00:00
|
|
|
expect(Platform::Instagram->label())->toBe('Instagram (Standalone)');
|
|
|
|
|
expect(Platform::InstagramFacebook->label())->toBe('Instagram (Facebook Business)');
|
2026-01-19 00:49:13 +00:00
|
|
|
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 () {
|
2026-06-24 20:36:39 +00:00
|
|
|
expect(Platform::LinkedIn->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video, MediaType::Document);
|
|
|
|
|
expect(Platform::LinkedInPage->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video, MediaType::Document);
|
2026-01-19 00:49:13 +00:00
|
|
|
expect(Platform::X->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video);
|
2026-06-24 20:36:39 +00:00
|
|
|
expect(Platform::X->allowedMediaTypes())->not->toContain(MediaType::Document);
|
2026-01-19 00:49:13 +00:00
|
|
|
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 () {
|
2026-06-25 14:45:11 +00:00
|
|
|
expect(Platform::LinkedIn->maxImages())->toBe(10);
|
2026-01-19 00:49:13 +00:00
|
|
|
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);
|
2026-05-03 21:38:17 +00:00
|
|
|
expect(Platform::YouTube->maxContentLength())->toBe(100);
|
2026-05-11 23:06:44 +00:00
|
|
|
expect(Platform::Facebook->maxContentLength())->toBe(10000);
|
2026-01-19 00:49:13 +00:00
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-03 16:11:36 +00:00
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-19 00:49:13 +00:00
|
|
|
test('platform is enabled by default', function () {
|
|
|
|
|
expect(Platform::LinkedIn->isEnabled())->toBeTrue();
|
|
|
|
|
expect(Platform::Instagram->isEnabled())->toBeTrue();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('platform can be disabled via config', function () {
|
|
|
|
|
config(['trypost.platforms.linkedin.enabled' => false]);
|
|
|
|
|
|
|
|
|
|
expect(Platform::LinkedIn->isEnabled())->toBeFalse();
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-25 00:05:09 +00:00
|
|
|
test('only linkedin pages are not directly connectable', function () {
|
|
|
|
|
expect(Platform::LinkedInPage->isConnectable())->toBeFalse();
|
|
|
|
|
expect(Platform::LinkedIn->isConnectable())->toBeTrue();
|
|
|
|
|
expect(Platform::InstagramFacebook->isConnectable())->toBeTrue();
|
|
|
|
|
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();
|
|
|
|
|
});
|