where('name', 'url')->first(); if (! $protocol) { $protocol = new App\Models\ConnectionProtocol(); $protocol->name = 'url'; $protocol->save(); } $provider = App\Models\ConnectionProvider::query()->where('slug', 'keka-hr')->first(); if (! $provider) { $provider = new App\Models\ConnectionProvider(); $provider->name = 'Keka HR'; $provider->slug = 'keka-hr'; $provider->save(); } $status = App\Models\ConnectionStatus::query()->where('name', 'connected')->first(); if (! $status) { $status = new App\Models\ConnectionStatus(); $status->name = 'connected'; $status->save(); } $app = App\Models\ConnectedApp::query()->create([ 'name' => 'Keka HR Portal', 'slug' => 'keka-hr-portal', 'connection_protocol_id' => $protocol->id, 'connection_provider_id' => $provider->id, 'connection_status_id' => $status->id, 'settings' => [ 'keka' => [ 'keka_url' => 'https://sentientgeeks.keka.com', 'callback_path' => 'signin-oidc', 'provider' => 'Office365', ], ], ]); $role = App\Models\Role::findOrCreate('Keka User'); $role->update(['priority' => 10]); $role->apps()->attach($app->id, ['duration' => now()->addYear()->toDateTimeString()]); $user->assignRole($role); } it('redirects unauthenticated users to login when connecting', function (): void { $this->get(route('keka.connect')) ->assertRedirect(route('login')); }); it('returns 403 forbidden if user does not have permission for keka-hr app', function (): void { $user = User::factory()->create(); $this->actingAs($user) ->get(route('keka.connect')) ->assertStatus(403); }); it('redirects to Keka login page on connect when authorized', function (): void { $user = User::factory()->create(); assignKekaHrAccess($user); $response = $this->actingAs($user) ->get(route('keka.connect')); $response->assertRedirect('https://login.keka.com/Account/ExternalLogin?provider=Office365&companyId=sentientgeeks'); }); it('stores token and redirects to Keka callback on successful callback when authorized', function (): void { $user = User::factory()->create(); assignKekaHrAccess($user); // Create a microsoft token first to check if callback copies it OauthToken::create([ 'user_id' => $user->id, 'provider' => 'microsoft', 'access_token' => 'ms-access-token', 'refresh_token' => 'ms-refresh-token', 'token_type' => 'Bearer', 'expires_at' => now()->addHour(), ]); $response = $this->actingAs($user) ->get(route('keka.callback', [ 'code' => 'keka-code-value', 'state' => 'keka-state-value', ])); $response->assertRedirect('https://login.keka.com/signin-oidc?code=keka-code-value&state=keka-state-value'); $this->assertDatabaseHas('oauth_tokens', [ 'user_id' => $user->id, 'provider' => 'keka', 'access_token' => 'ms-access-token', 'refresh_token' => 'ms-refresh-token', ]); }); it('stores placeholder token and redirects to Keka callback when no microsoft token exists', function (): void { $user = User::factory()->create(); assignKekaHrAccess($user); $response = $this->actingAs($user) ->get(route('keka.callback', [ 'code' => 'keka-code-value2', 'state' => 'keka-state-value2', ])); $response->assertRedirect('https://login.keka.com/signin-oidc?code=keka-code-value2&state=keka-state-value2'); $this->assertDatabaseHas('oauth_tokens', [ 'user_id' => $user->id, 'provider' => 'keka', 'access_token' => 'keka-code-value2', ]); }); it('redirects to dashboard with error on callback if code is missing', function (): void { $user = User::factory()->create(); assignKekaHrAccess($user); $response = $this->actingAs($user) ->get(route('keka.callback', [ 'state' => 'keka-state-value', ])); $response->assertRedirect(route('dashboard.user')); $response->assertSessionHas('keka_error'); }); it('disconnects and removes the token when authorized', function (): void { $user = User::factory()->create(); assignKekaHrAccess($user); OauthToken::create([ 'user_id' => $user->id, 'provider' => 'keka', 'access_token' => 'fake-keka-token', 'refresh_token' => 'fake-keka-refresh', 'token_type' => 'Bearer', 'expires_at' => now()->addHour(), ]); $response = $this->actingAs($user) ->delete(route('keka.disconnect')); $response->assertRedirect(route('dashboard.user')); $response->assertSessionHas('keka_success', 'Keka HR disconnected.'); $this->assertDatabaseMissing('oauth_tokens', [ 'user_id' => $user->id, 'provider' => 'keka', ]); }); it('requires a valid kekaUrl when creating a Keka HR connected app', function (): void { $this->seed(Database\Seeders\ConnectionProtocolSeeder::class); $this->seed(Database\Seeders\ConnectionProviderSeeder::class); $this->seed(Database\Seeders\ConnectionStatusSeeder::class); $admin = User::factory()->create(); $admin->assignRole(App\Models\Role::findOrCreate('Admin')); $protocol = App\Models\ConnectionProtocol::query()->where('name', 'url')->firstOrFail(); $provider = App\Models\ConnectionProvider::query()->where('slug', 'keka-hr')->firstOrFail(); $status = App\Models\ConnectionStatus::query()->where('name', 'connected')->firstOrFail(); $this->actingAs($admin); // Test validation failure Livewire::test('pages::connected-apps.create') ->set('form.name', 'My Keka App') ->set('form.protocolId', $protocol->id) ->set('form.providerId', $provider->id) ->set('form.statusId', $status->id) ->set('form.kekaUrl', 'not-a-valid-url') ->call('save') ->assertHasErrors(['form.kekaUrl' => 'url']); // Test validation success Livewire::test('pages::connected-apps.create') ->set('form.name', 'My Keka App') ->set('form.protocolId', $protocol->id) ->set('form.providerId', $provider->id) ->set('form.statusId', $status->id) ->set('form.kekaUrl', 'https://sentientgeeks.keka.com') ->call('save') ->assertHasNoErrors(); $this->assertDatabaseHas('connected_apps', [ 'name' => 'My Keka App', 'connection_protocol_id' => $protocol->id, 'connection_provider_id' => $provider->id, ]); $app = App\Models\ConnectedApp::query()->where('name', 'My Keka App')->firstOrFail(); expect(data_get($app->settings, 'keka_url'))->toBe('https://sentientgeeks.keka.com'); expect(data_get($app->settings, 'keka.provider'))->toBe('Office365'); }); it('successfully edits a Keka HR connected app', function (): void { $this->seed(Database\Seeders\ConnectionProtocolSeeder::class); $this->seed(Database\Seeders\ConnectionProviderSeeder::class); $this->seed(Database\Seeders\ConnectionStatusSeeder::class); $admin = User::factory()->create(); $admin->assignRole(App\Models\Role::findOrCreate('Admin')); $protocol = App\Models\ConnectionProtocol::query()->where('name', 'url')->firstOrFail(); $provider = App\Models\ConnectionProvider::query()->where('slug', 'keka-hr')->firstOrFail(); $status = App\Models\ConnectionStatus::query()->where('name', 'connected')->firstOrFail(); $app = App\Models\ConnectedApp::query()->create([ 'name' => 'My Keka App', 'slug' => 'my-keka-app', 'connection_protocol_id' => $protocol->id, 'connection_provider_id' => $provider->id, 'connection_status_id' => $status->id, 'settings' => [ 'keka_url' => 'https://old.keka.com', 'keka' => [ 'keka_url' => 'https://old.keka.com', 'provider' => 'OpenIdConnect', ], ], ]); $this->actingAs($admin); Livewire::test('pages::connected-apps.edit', ['id' => $app->id]) ->set('form.kekaUrl', 'https://new.keka.com') ->call('save') ->assertHasNoErrors(); $app->refresh(); expect(data_get($app->settings, 'keka_url'))->toBe('https://new.keka.com'); expect(data_get($app->settings, 'keka.provider'))->toBe('Office365'); }); it('forwards POST callback response via an auto-submitting HTML form', function (): void { $user = User::factory()->create(); assignKekaHrAccess($user); $response = $this->actingAs($user) ->post(route('keka.callback'), [ 'code' => 'post-code-value', 'state' => 'post-state-value', 'session_state' => 'session-state-value', ]); $response->assertStatus(200); $response->assertSee('action="https://login.keka.com/signin-oidc"', false); $response->assertSee('name="code" value="post-code-value"', false); $response->assertSee('name="state" value="post-state-value"', false); $response->assertSee('name="session_state" value="session-state-value"', false); });