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('linkedin page connect redirects to oauth provider', function () { $driverMock = Mockery::mock(); $driverMock->shouldReceive('scopes')->andReturnSelf(); $driverMock->shouldReceive('with')->andReturnSelf(); $driverMock->shouldReceive('redirect')->andReturn(Mockery::mock([ 'getTargetUrl' => 'https://www.linkedin.com/oauth/v2/authorization?test=1', ])); Socialite::shouldReceive('driver') ->with('linkedin-openid') ->andReturn($driverMock); $response = $this->actingAs($this->user) ->withHeader('X-Inertia', 'true') ->get(route('app.social.linkedin-page.connect')); $response->assertStatus(409); // Inertia::location returns 409 with X-Inertia header expect(session('social_connect_workspace'))->toBe($this->workspace->id); }); test('linkedin page oauth callback fetches organizations', function () { session([ 'social_connect_workspace' => $this->workspace->id, ]); $socialiteUser = Mockery::mock(SocialiteUser::class); $socialiteUser->shouldReceive('getId')->andReturn('user123'); $socialiteUser->shouldReceive('getName')->andReturn('John Doe'); $socialiteUser->shouldReceive('getAvatar')->andReturn(null); $socialiteUser->token = 'test-access-token'; $socialiteUser->refreshToken = 'test-refresh-token'; $socialiteUser->expiresIn = 5184000; $socialiteMock = Mockery::mock(); $socialiteMock->shouldReceive('scopes')->andReturn($socialiteMock); $socialiteMock->shouldReceive('with')->andReturn($socialiteMock); $socialiteMock->shouldReceive('user')->andReturn($socialiteUser); Socialite::shouldReceive('driver') ->with('linkedin-openid') ->andReturn($socialiteMock); Http::fake([ 'https://api.linkedin.com/v2/organizationAcls*' => Http::response([ 'elements' => [ [ 'organization~' => [ 'id' => 123456, 'localizedName' => 'Test Company', 'vanityName' => 'testcompany', ], ], ], ], 200), ]); $response = $this->actingAs($this->user)->get(route('app.social.linkedin-page.callback')); $response->assertRedirect(route('app.social.linkedin-page.select-page')); expect(session('linkedin_page_pending'))->not->toBeNull(); expect(session('linkedin_page_pending.organizations'))->toHaveCount(1); }); test('linkedin page callback fails when user has no organizations', function () { session([ 'social_connect_workspace' => $this->workspace->id, ]); $socialiteUser = Mockery::mock(SocialiteUser::class); $socialiteUser->shouldReceive('getId')->andReturn('user123'); $socialiteUser->shouldReceive('getName')->andReturn('John Doe'); $socialiteUser->shouldReceive('getAvatar')->andReturn(null); $socialiteUser->token = 'test-access-token'; $socialiteUser->refreshToken = 'test-refresh-token'; $socialiteUser->expiresIn = 5184000; $socialiteMock = Mockery::mock(); $socialiteMock->shouldReceive('scopes')->andReturn($socialiteMock); $socialiteMock->shouldReceive('with')->andReturn($socialiteMock); $socialiteMock->shouldReceive('user')->andReturn($socialiteUser); Socialite::shouldReceive('driver') ->with('linkedin-openid') ->andReturn($socialiteMock); Http::fake([ 'https://api.linkedin.com/v2/organizationAcls*' => Http::response([ 'elements' => [], ], 200), ]); $response = $this->actingAs($this->user)->get(route('app.social.linkedin-page.callback')); $response->assertOk(); $response->assertViewHas('success', false); $response->assertViewHas('message', 'You are not an administrator of any LinkedIn page.'); }); test('linkedin page callback fails with expired session', function () { // No session data - simulating expired session $response = $this->actingAs($this->user)->get(route('app.social.linkedin-page.callback')); $response->assertOk(); $response->assertViewHas('success', false); $response->assertViewHas('message', 'Session expired. Please try again.'); }); test('linkedin page select creates account', function () { 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', true); $this->assertDatabaseHas('social_accounts', [ 'workspace_id' => $this->workspace->id, 'platform' => Platform::LinkedInPage->value, 'platform_user_id' => 123456, 'username' => 'testcompany', 'display_name' => 'Test Company', 'status' => Status::Connected->value, ]); }); test('linkedin page select splits comma-separated approvedScopes before saving', function () { 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, // Simulates what Socialite returns for LinkedIn (CSV-joined into // a single array element because the provider splits on space). 'approved_scopes' => ['email,openid,profile,w_organization_social,r_organization_social,rw_organization_admin,w_member_social'], 'organizations' => [ ['id' => 999888, 'name' => 'Scope Company', 'vanity_name' => 'scopeco', 'logo' => null], ], ], ]); $this->actingAs($this->user)->post(route('app.social.linkedin-page.select'), [ 'organization_id' => 999888, 'organization_name' => 'Scope Company', 'organization_vanity_name' => 'scopeco', 'organization_logo' => null, ]); $account = SocialAccount::where('platform_user_id', 999888)->first(); expect($account->scopes)->toEqualCanonicalizing([ 'email', 'openid', 'profile', 'w_organization_social', 'r_organization_social', 'rw_organization_admin', 'w_member_social', ]); }); test('linkedin page select fails with expired session', function () { // No session data $response = $this->actingAs($this->user)->post(route('app.social.linkedin-page.select'), [ 'organization_id' => 123456, 'organization_name' => 'Test Company', 'organization_vanity_name' => 'testcompany', ]); $response->assertOk(); $response->assertViewHas('success', false); $response->assertViewHas('message', 'Session expired. Please try again.'); }); test('user can connect multiple linkedin pages', function () { SocialAccount::factory()->linkedinPage()->create([ 'workspace_id' => $this->workspace->id, 'platform_user_id' => '123456', ]); session([ 'linkedin_page_pending' => [ 'workspace_id' => $this->workspace->id, 'user_id' => 'user123', 'name' => 'John Doe', 'avatar' => null, 'token' => 'new-access-token', 'refresh_token' => 'new-refresh-token', 'expires_in' => 5184000, 'organizations' => [ ['id' => 789012, 'name' => 'Another Company', 'vanity_name' => 'anothercompany', 'logo' => null], ], ], ]); $response = $this->actingAs($this->user)->post(route('app.social.linkedin-page.select'), [ 'organization_id' => 789012, 'organization_name' => 'Another Company', 'organization_vanity_name' => 'anothercompany', 'organization_logo' => null, ]); $response->assertOk(); $response->assertViewHas('success', true); expect($this->workspace->socialAccounts()->where('platform', Platform::LinkedInPage)->count())->toBe(2); }); test('linkedin page callback handles oauth errors gracefully', function () { session([ 'social_connect_workspace' => $this->workspace->id, ]); $socialiteMock = Mockery::mock(); $socialiteMock->shouldReceive('scopes')->andReturn($socialiteMock); $socialiteMock->shouldReceive('with')->andReturn($socialiteMock); $socialiteMock->shouldReceive('user')->andThrow(new Exception('OAuth error')); Socialite::shouldReceive('driver') ->with('linkedin-openid') ->andReturn($socialiteMock); $response = $this->actingAs($this->user)->get(route('app.social.linkedin-page.callback')); $response->assertOk(); $response->assertViewHas('success', false); $response->assertViewHas('message', 'Error connecting account. Please try again.'); });