fix: merge-readiness — close two billing/network bugs, harden tests
Bugs (both with regression tests): - OnboardingController::store now guards already-subscribed accounts (mirrors index), preventing a second Stripe Checkout / double subscription if a subscribed user re-POSTs /onboarding. - SocialAccountObserver: drop the `platform_user_id != …` clause from the creating-time one-per-network check. On create there is no "self" to exclude, so it only weakened the rule — the same account connected via two network variants (e.g. Instagram standalone + via Facebook, same id) could slip a second account into the network. Now any account in the network blocks. Robustness: - CreateWorkspace wraps create + member attach + switchWorkspace in a transaction (cache-forget / quantity-sync run after), so a partial failure can't leave an orphan workspace that inflates the Stripe seat count — covers both the signup and the add-workspace paths. Test honesty & coverage: - Scope the ten "connect multiple <platform> accounts" tests to self-hosted mode (config + name); they only passed because the test env defaults SELF_HOSTED=true, and in cloud the one-per-network rule blocks them. - network_taken popup now has controller-level tests on all six OAuth controllers (added Threads, YouTube, LinkedInPage, and a new InstagramFacebook test file; LinkedInPage/InstagramFacebook also exercise variant collapse). - Wiring tests that creating/deleting a workspace actually calls syncWorkspaceQuantity (guards per-seat billing against silent breakage). - Strengthen TrialLengthTest to assert the configured length reaches trial_ends_at; add a same-id network-variant block test.
This commit is contained in:
parent
3d01f3aab4
commit
17a091975d
18 changed files with 393 additions and 20 deletions
|
|
@ -7,6 +7,7 @@
|
|||
use App\Enums\UserWorkspace\Role;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CreateWorkspace
|
||||
{
|
||||
|
|
@ -26,16 +27,20 @@ public static function execute(User $user, array $data): Workspace
|
|||
'content_language' => data_get($data, 'content_language'),
|
||||
], static fn ($value): bool => $value !== null);
|
||||
|
||||
$workspace = Workspace::create([
|
||||
...$attributes,
|
||||
'account_id' => $user->account_id,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
$workspace = DB::transaction(function () use ($user, $attributes): Workspace {
|
||||
$workspace = Workspace::create([
|
||||
...$attributes,
|
||||
'account_id' => $user->account_id,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
|
||||
// Creator becomes Admin of the workspace they made. The Account Owner
|
||||
// is resolved separately (via account.owner_id) and outranks this role.
|
||||
$workspace->members()->attach($user->id, ['role' => Role::Admin->value]);
|
||||
$user->switchWorkspace($workspace);
|
||||
// Creator becomes Admin of the workspace they made. The Account Owner
|
||||
// is resolved separately (via account.owner_id) and outranks this role.
|
||||
$workspace->members()->attach($user->id, ['role' => Role::Admin->value]);
|
||||
$user->switchWorkspace($workspace);
|
||||
|
||||
return $workspace;
|
||||
});
|
||||
|
||||
$user->account?->forgetPlanFeatureCache();
|
||||
$user->account?->syncWorkspaceQuantity();
|
||||
|
|
|
|||
|
|
@ -48,6 +48,11 @@ public function store(
|
|||
|
||||
$user = $request->user();
|
||||
$account = $user->account;
|
||||
|
||||
if ($account?->subscribed(Account::SUBSCRIPTION_NAME)) {
|
||||
return redirect()->route('app.calendar');
|
||||
}
|
||||
|
||||
$persona = (string) $request->validated('persona');
|
||||
|
||||
$user->update(['persona' => $persona]);
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ public function creating(SocialAccount $socialAccount): void
|
|||
$conflict = SocialAccount::query()
|
||||
->where('workspace_id', $socialAccount->workspace_id)
|
||||
->whereIn('platform', $platform->networkPlatformValues())
|
||||
->where('platform_user_id', '!=', $socialAccount->platform_user_id)
|
||||
->exists();
|
||||
|
||||
if ($conflict) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,21 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Actions\User\CreateUser;
|
||||
|
||||
test('the default trial length is 8 days', function () {
|
||||
expect(config('cashier.trial_days'))->toBe(8);
|
||||
});
|
||||
|
||||
test('signup grants a trial of cashier.trial_days in no-card mode', function () {
|
||||
config(['trypost.billing.require_card_for_trial' => false]);
|
||||
|
||||
$user = CreateUser::execute([
|
||||
'name' => 'Trial User',
|
||||
'email' => 'trial@example.com',
|
||||
'password' => 'secret123',
|
||||
]);
|
||||
|
||||
expect($user->account->trial_ends_at->toDateString())
|
||||
->toBe(now()->addDays(config('cashier.trial_days'))->toDateString());
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Actions\Workspace\CreateWorkspace;
|
||||
use App\Actions\Workspace\DeleteWorkspace;
|
||||
use App\Models\Account;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Laravel\Cashier\Subscription;
|
||||
|
||||
test('syncWorkspaceQuantity does not touch Stripe in self-hosted mode', function () {
|
||||
|
|
@ -45,3 +49,29 @@
|
|||
|
||||
$account->syncWorkspaceQuantity();
|
||||
});
|
||||
|
||||
test('creating a workspace syncs the stripe quantity', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$account = mock(Account::class)->makePartial();
|
||||
$account->shouldReceive('forgetPlanFeatureCache')->once();
|
||||
$account->shouldReceive('syncWorkspaceQuantity')->once();
|
||||
$user->setRelation('account', $account);
|
||||
|
||||
CreateWorkspace::execute($user, ['name' => 'Wiring']);
|
||||
});
|
||||
|
||||
test('deleting a workspace syncs the stripe quantity', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create([
|
||||
'account_id' => $user->account_id,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
|
||||
$account = mock(Account::class)->makePartial();
|
||||
$account->shouldReceive('forgetPlanFeatureCache')->once();
|
||||
$account->shouldReceive('syncWorkspaceQuantity')->once();
|
||||
$workspace->setRelation('account', $account);
|
||||
|
||||
DeleteWorkspace::execute($user, $workspace);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -97,3 +97,23 @@
|
|||
|
||||
Bus::assertDispatched(SendEvent::class);
|
||||
});
|
||||
|
||||
test('onboarding store redirects an already-subscribed account to the calendar without starting a second checkout', function () {
|
||||
$this->user->account->subscriptions()->create([
|
||||
'type' => Account::SUBSCRIPTION_NAME,
|
||||
'stripe_id' => 'sub_'.fake()->uuid(),
|
||||
'stripe_status' => 'active',
|
||||
'stripe_price' => 'price_123',
|
||||
]);
|
||||
|
||||
$this->mock(StartSubscriptionCheckout::class)
|
||||
->shouldReceive('redirect')
|
||||
->never();
|
||||
|
||||
$response = $this->actingAs($this->user->fresh())->post(route('app.onboarding.store'), [
|
||||
'persona' => Persona::Agency->value,
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('app.calendar'));
|
||||
expect($this->user->fresh()->persona)->toBeNull();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -79,7 +79,9 @@
|
|||
]);
|
||||
});
|
||||
|
||||
test('user can connect multiple bluesky accounts', function () {
|
||||
test('user can connect multiple bluesky accounts in self-hosted mode', function () {
|
||||
config()->set('trypost.self_hosted', true);
|
||||
|
||||
SocialAccount::factory()->bluesky()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform_user_id' => 'did:plc:existing123',
|
||||
|
|
|
|||
|
|
@ -82,7 +82,9 @@
|
|||
$response->assertViewHas('message', 'Session expired. Please try again.');
|
||||
});
|
||||
|
||||
test('user can connect multiple instagram accounts', function () {
|
||||
test('user can connect multiple instagram accounts in self-hosted mode', function () {
|
||||
config()->set('trypost.self_hosted', true);
|
||||
|
||||
SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Instagram,
|
||||
|
|
@ -118,6 +120,43 @@
|
|||
expect($this->workspace->socialAccounts()->where('platform', Platform::Instagram)->count())->toBe(2);
|
||||
});
|
||||
|
||||
test('instagram callback shows network_taken when the network is already connected', function () {
|
||||
config()->set('trypost.self_hosted', false);
|
||||
|
||||
SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Instagram,
|
||||
'platform_user_id' => 'existing-ig',
|
||||
]);
|
||||
|
||||
session(['social_connect_workspace' => $this->workspace->id]);
|
||||
|
||||
$socialiteUser = Mockery::mock(SocialiteUser::class);
|
||||
$socialiteUser->shouldReceive('getId')->andReturn('12345678');
|
||||
$socialiteUser->shouldReceive('getNickname')->andReturn('testuser');
|
||||
$socialiteUser->shouldReceive('getName')->andReturn('Test User');
|
||||
$socialiteUser->shouldReceive('getAvatar')->andReturn(null);
|
||||
$socialiteUser->token = 'test-access-token';
|
||||
$socialiteUser->refreshToken = 'test-refresh-token';
|
||||
$socialiteUser->expiresIn = 5184000;
|
||||
$socialiteUser->user = ['account_type' => 'BUSINESS'];
|
||||
|
||||
Socialite::shouldReceive('driver')
|
||||
->with('instagram')
|
||||
->andReturn(Mockery::mock([
|
||||
'user' => $socialiteUser,
|
||||
]));
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('app.social.instagram.callback'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertViewIs('auth.social-callback');
|
||||
$response->assertViewHas('success', false);
|
||||
$response->assertViewHas('message', __('accounts.popup_callback.network_taken'));
|
||||
|
||||
expect($this->workspace->socialAccounts()->where('platform', Platform::Instagram)->count())->toBe(1);
|
||||
});
|
||||
|
||||
test('instagram callback handles oauth errors gracefully', function () {
|
||||
session([
|
||||
'social_connect_workspace' => $this->workspace->id,
|
||||
|
|
|
|||
94
tests/Feature/Social/InstagramFacebookControllerTest.php
Normal file
94
tests/Feature/Social/InstagramFacebookControllerTest.php
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Enums\UserWorkspace\Role;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
$this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
|
||||
$this->user->update(['current_workspace_id' => $this->workspace->id]);
|
||||
$this->workspace->members()->attach($this->user->id, ['role' => Role::Member->value]);
|
||||
});
|
||||
|
||||
test('instagram-facebook select connects the page in self-hosted mode', function () {
|
||||
config()->set('trypost.self_hosted', true);
|
||||
|
||||
session([
|
||||
'social_connect_workspace' => $this->workspace->id,
|
||||
'instagram_facebook_oauth' => [
|
||||
'user_token' => 'user-token',
|
||||
'reconnect_id' => null,
|
||||
'pages' => [
|
||||
[
|
||||
'page_id' => 'page-1',
|
||||
'page_name' => 'My Page',
|
||||
'page_access_token' => 'page-token',
|
||||
'ig_id' => 'ig-new',
|
||||
'ig_username' => 'mybiz',
|
||||
'ig_name' => 'My Biz',
|
||||
'ig_picture' => null,
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->post(route('app.social.instagram-facebook.select'), [
|
||||
'page_id' => 'page-1',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertViewIs('auth.social-callback');
|
||||
$response->assertViewHas('success', true);
|
||||
|
||||
$this->assertDatabaseHas('social_accounts', [
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::InstagramFacebook->value,
|
||||
'platform_user_id' => 'ig-new',
|
||||
'username' => 'mybiz',
|
||||
]);
|
||||
});
|
||||
|
||||
test('instagram-facebook select shows network_taken when a standalone instagram is already connected', function () {
|
||||
config()->set('trypost.self_hosted', false);
|
||||
|
||||
SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Instagram,
|
||||
'platform_user_id' => 'existing-ig',
|
||||
]);
|
||||
|
||||
session([
|
||||
'social_connect_workspace' => $this->workspace->id,
|
||||
'instagram_facebook_oauth' => [
|
||||
'user_token' => 'user-token',
|
||||
'reconnect_id' => null,
|
||||
'pages' => [
|
||||
[
|
||||
'page_id' => 'page-1',
|
||||
'page_name' => 'My Page',
|
||||
'page_access_token' => 'page-token',
|
||||
'ig_id' => 'ig-new',
|
||||
'ig_username' => 'mybiz',
|
||||
'ig_name' => 'My Biz',
|
||||
'ig_picture' => null,
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->post(route('app.social.instagram-facebook.select'), [
|
||||
'page_id' => 'page-1',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertViewIs('auth.social-callback');
|
||||
$response->assertViewHas('success', false);
|
||||
$response->assertViewHas('message', __('accounts.popup_callback.network_taken'));
|
||||
|
||||
expect($this->workspace->socialAccounts()->whereIn('platform', [Platform::Instagram->value, Platform::InstagramFacebook->value])->count())->toBe(1);
|
||||
});
|
||||
|
|
@ -181,7 +181,9 @@
|
|||
$response->assertViewHas('message', 'Session expired. Please try again.');
|
||||
});
|
||||
|
||||
test('user can connect multiple linkedin accounts', function () {
|
||||
test('user can connect multiple linkedin accounts in self-hosted mode', function () {
|
||||
config()->set('trypost.self_hosted', true);
|
||||
|
||||
SocialAccount::factory()->linkedin()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform_user_id' => 'abc123xyz',
|
||||
|
|
|
|||
|
|
@ -214,7 +214,9 @@
|
|||
$response->assertViewHas('message', 'Session expired. Please try again.');
|
||||
});
|
||||
|
||||
test('user can connect multiple linkedin pages', function () {
|
||||
test('user can connect multiple linkedin pages in self-hosted mode', function () {
|
||||
config()->set('trypost.self_hosted', true);
|
||||
|
||||
SocialAccount::factory()->linkedinPage()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform_user_id' => '123456',
|
||||
|
|
@ -268,3 +270,42 @@
|
|||
$response->assertViewHas('success', false);
|
||||
$response->assertViewHas('message', 'Error connecting account. Please try again.');
|
||||
});
|
||||
|
||||
test('linkedin page select shows network_taken when a linkedin account is already connected', function () {
|
||||
config()->set('trypost.self_hosted', false);
|
||||
|
||||
SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::LinkedIn,
|
||||
'platform_user_id' => 'existing-linkedin',
|
||||
]);
|
||||
|
||||
session([
|
||||
'linkedin_page_pending' => [
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => 'user123',
|
||||
'name' => 'John Doe',
|
||||
'avatar' => null,
|
||||
'token' => 'test-access-token',
|
||||
'refresh_token' => 'test-refresh-token',
|
||||
'expires_in' => 5184000,
|
||||
'organizations' => [
|
||||
['id' => 123456, 'name' => 'Test Company', 'vanity_name' => 'testcompany', 'logo' => null],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->post(route('app.social.linkedin-page.select'), [
|
||||
'organization_id' => 123456,
|
||||
'organization_name' => 'Test Company',
|
||||
'organization_vanity_name' => 'testcompany',
|
||||
'organization_logo' => null,
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertViewIs('auth.social-callback');
|
||||
$response->assertViewHas('success', false);
|
||||
$response->assertViewHas('message', __('accounts.popup_callback.network_taken'));
|
||||
|
||||
expect($this->workspace->socialAccounts()->whereIn('platform', [Platform::LinkedIn->value, Platform::LinkedInPage->value])->count())->toBe(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -143,7 +143,9 @@
|
|||
$response->assertViewHas('message', 'Session expired. Please try again.');
|
||||
});
|
||||
|
||||
test('user can connect multiple mastodon accounts', function () {
|
||||
test('user can connect multiple mastodon accounts in self-hosted mode', function () {
|
||||
config()->set('trypost.self_hosted', true);
|
||||
|
||||
SocialAccount::factory()->mastodon()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform_user_id' => '123456789',
|
||||
|
|
|
|||
|
|
@ -112,7 +112,9 @@
|
|||
$response->assertViewHas('message', 'Session expired. Please try again.');
|
||||
});
|
||||
|
||||
test('user can connect multiple pinterest accounts', function () {
|
||||
test('user can connect multiple pinterest accounts in self-hosted mode', function () {
|
||||
config()->set('trypost.self_hosted', true);
|
||||
|
||||
SocialAccount::factory()->pinterest()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform_user_id' => 'pinterest_user_123',
|
||||
|
|
|
|||
|
|
@ -105,7 +105,9 @@
|
|||
$response->assertViewHas('message', 'Session expired. Please try again.');
|
||||
});
|
||||
|
||||
test('user can connect multiple threads accounts', function () {
|
||||
test('user can connect multiple threads accounts in self-hosted mode', function () {
|
||||
config()->set('trypost.self_hosted', true);
|
||||
|
||||
SocialAccount::factory()->threads()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform_user_id' => '123456789',
|
||||
|
|
@ -169,3 +171,48 @@
|
|||
$response->assertViewHas('success', false);
|
||||
$response->assertViewHas('message', 'Error connecting account. Please try again.');
|
||||
});
|
||||
|
||||
test('threads callback shows network_taken when the network is already connected', function () {
|
||||
config()->set('trypost.self_hosted', false);
|
||||
|
||||
SocialAccount::factory()->threads()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform_user_id' => 'existing-threads',
|
||||
]);
|
||||
|
||||
$state = bin2hex(random_bytes(16));
|
||||
|
||||
session([
|
||||
'social_connect_workspace' => $this->workspace->id,
|
||||
'threads_oauth_state' => $state,
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://graph.threads.net/oauth/access_token' => Http::response([
|
||||
'access_token' => 'short-lived-token',
|
||||
'user_id' => '123456789',
|
||||
], 200),
|
||||
'https://graph.threads.net/access_token*' => Http::response([
|
||||
'access_token' => 'long-lived-token',
|
||||
'expires_in' => 5184000,
|
||||
], 200),
|
||||
'https://graph.threads.net/v1.0/123456789*' => Http::response([
|
||||
'id' => '123456789',
|
||||
'username' => 'testuser',
|
||||
'name' => 'Test User',
|
||||
'threads_profile_picture_url' => null,
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('app.social.threads.callback', [
|
||||
'code' => 'test-auth-code',
|
||||
'state' => $state,
|
||||
]));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertViewIs('auth.social-callback');
|
||||
$response->assertViewHas('success', false);
|
||||
$response->assertViewHas('message', __('accounts.popup_callback.network_taken'));
|
||||
|
||||
expect($this->workspace->socialAccounts()->where('platform', Platform::Threads)->count())->toBe(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -86,7 +86,9 @@
|
|||
$response->assertViewHas('message', 'Session expired. Please try again.');
|
||||
});
|
||||
|
||||
test('user can connect multiple tiktok accounts', function () {
|
||||
test('user can connect multiple tiktok accounts in self-hosted mode', function () {
|
||||
config()->set('trypost.self_hosted', true);
|
||||
|
||||
SocialAccount::factory()->tiktok()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform_user_id' => 'tiktok123',
|
||||
|
|
|
|||
|
|
@ -84,7 +84,9 @@
|
|||
$response->assertViewHas('message', 'Session expired. Please try again.');
|
||||
});
|
||||
|
||||
test('user can connect multiple x accounts', function () {
|
||||
test('user can connect multiple x accounts in self-hosted mode', function () {
|
||||
config()->set('trypost.self_hosted', true);
|
||||
|
||||
SocialAccount::factory()->x()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform_user_id' => '123456789',
|
||||
|
|
|
|||
|
|
@ -182,7 +182,9 @@
|
|||
$response->assertViewHas('message', 'Session expired. Please try again.');
|
||||
});
|
||||
|
||||
test('user can connect multiple youtube accounts', function () {
|
||||
test('user can connect multiple youtube accounts in self-hosted mode', function () {
|
||||
config()->set('trypost.self_hosted', true);
|
||||
|
||||
SocialAccount::factory()->youtube()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform_user_id' => 'UC_channel_123',
|
||||
|
|
@ -301,3 +303,53 @@
|
|||
$response->assertViewHas('success', false);
|
||||
$response->assertViewHas('message', 'Session expired. Please try again.');
|
||||
});
|
||||
|
||||
test('youtube callback shows network_taken when the network is already connected', function () {
|
||||
config()->set('trypost.self_hosted', false);
|
||||
|
||||
SocialAccount::factory()->youtube()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform_user_id' => 'UC_existing',
|
||||
]);
|
||||
|
||||
session([
|
||||
'social_connect_workspace' => $this->workspace->id,
|
||||
]);
|
||||
|
||||
$socialiteUser = Mockery::mock(SocialiteUser::class);
|
||||
$socialiteUser->shouldReceive('getId')->andReturn('google_user_123');
|
||||
$socialiteUser->token = 'test-access-token';
|
||||
$socialiteUser->refreshToken = 'test-refresh-token';
|
||||
$socialiteUser->expiresIn = 3600;
|
||||
|
||||
Socialite::shouldReceive('driver')
|
||||
->with('google')
|
||||
->andReturn(Mockery::mock([
|
||||
'user' => $socialiteUser,
|
||||
]));
|
||||
|
||||
Http::fake([
|
||||
'https://www.googleapis.com/youtube/v3/channels*' => Http::response([
|
||||
'items' => [
|
||||
[
|
||||
'id' => 'UC_channel_123',
|
||||
'snippet' => [
|
||||
'title' => 'My YouTube Channel',
|
||||
'customUrl' => '@mychannel',
|
||||
'thumbnails' => ['default' => ['url' => null]],
|
||||
],
|
||||
'statistics' => ['subscriberCount' => 1000],
|
||||
],
|
||||
],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('app.social.youtube.callback'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertViewIs('auth.social-callback');
|
||||
$response->assertViewHas('success', false);
|
||||
$response->assertViewHas('message', __('accounts.popup_callback.network_taken'));
|
||||
|
||||
expect($this->workspace->socialAccounts()->where('platform', Platform::YouTube)->count())->toBe(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -109,3 +109,17 @@
|
|||
|
||||
expect($second->exists)->toBeTrue();
|
||||
});
|
||||
|
||||
test('blocks a same-id account connected via a different network variant', function () {
|
||||
SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Instagram,
|
||||
'platform_user_id' => 'shared-ig-id',
|
||||
]);
|
||||
|
||||
expect(fn () => SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::InstagramFacebook,
|
||||
'platform_user_id' => 'shared-ig-id',
|
||||
]))->toThrow(NetworkAlreadyConnectedException::class);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue