From 9c9e3ca43cd8b7d8cd35f0919bd1802f6bf8ebdb Mon Sep 17 00:00:00 2001 From: = Date: Mon, 22 Jun 2026 11:20:49 +0000 Subject: [PATCH] Feat: Update App details from view page - Implemented the `show` page for managing URL apps with functionalities like updating name, logo, access URL, and connection status. - Added `UrlAppViewTest` for feature testing, ensuring role-based access, and validation of edit actions. - Updated navigation to route URL apps to their respective management views. - Enhanced `OidcService` to handle updated app settings and redirect URIs. --- app/Models/ConnectedApp.php | 26 +- .../Applications/OIDC/OidcConfigResolver.php | 5 +- .../Applications/OIDC/OidcService.php | 25 +- .../views/pages/apps/oidc/⚡show.blade.php | 341 ++++++++++++----- .../views/pages/apps/url/⚡show.blade.php | 347 ++++++++++++++++++ resources/views/pages/apps/⚡index.blade.php | 2 +- routes/apps.php | 11 +- tests/Feature/OidcAppViewTest.php | 73 ++++ tests/Feature/UrlAppViewTest.php | 102 +++++ 9 files changed, 812 insertions(+), 120 deletions(-) create mode 100644 resources/views/pages/apps/url/⚡show.blade.php create mode 100644 tests/Feature/UrlAppViewTest.php diff --git a/app/Models/ConnectedApp.php b/app/Models/ConnectedApp.php index 6ebd9af..7ae426c 100644 --- a/app/Models/ConnectedApp.php +++ b/app/Models/ConnectedApp.php @@ -15,6 +15,7 @@ * @property int $id * @property string $name * @property AppRole $pivot + * @property array $settings */ #[Fillable([ 'name', @@ -23,23 +24,12 @@ 'connection_provider_id', 'connection_status_id', 'settings', + 'logo', ])] class ConnectedApp extends Model { use HasFactory, LogsActivity, SoftDeletes; - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts(): array - { - return [ - 'settings' => 'array', - ]; - } - /** * @return HasOne */ @@ -87,4 +77,16 @@ public function roles(): BelongsToMany ->using(AppRole::class) ->withPivot('duration'); } + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'settings' => 'array', + ]; + } } diff --git a/app/Services/Applications/OIDC/OidcConfigResolver.php b/app/Services/Applications/OIDC/OidcConfigResolver.php index 627db3f..26bb2c9 100644 --- a/app/Services/Applications/OIDC/OidcConfigResolver.php +++ b/app/Services/Applications/OIDC/OidcConfigResolver.php @@ -5,6 +5,7 @@ namespace App\Services\Applications\OIDC; use App\Enums\ApplicationTypeEnum; +use Laravel\Passport\Client; class OidcConfigResolver { @@ -34,12 +35,12 @@ public function needSignOutUris(): bool // return ApplicationTypeEnum::Machine !== $this->applicationType; } - public function isConfidential(\Laravel\Passport\Client $client): bool + public function isConfidential(Client $client): bool { return $client->confidential(); } - public function requiresPkce(\Laravel\Passport\Client $client): bool + public function requiresPkce(Client $client): bool { return ! $client->confidential(); } diff --git a/app/Services/Applications/OIDC/OidcService.php b/app/Services/Applications/OIDC/OidcService.php index e1c49a4..11a1ffa 100644 --- a/app/Services/Applications/OIDC/OidcService.php +++ b/app/Services/Applications/OIDC/OidcService.php @@ -90,19 +90,20 @@ public function generateNewSecret(Client $client): string return $plainSecret; } - public function toggleStatus(ConnectedApp $app): void - { - $statusEnum = $app->status->name === ConnectionStatusEnum::Connected->value - ? ConnectionStatusEnum::Disconnected - : ConnectionStatusEnum::Connected; - - $newStatus = ConnectionStatus::where('name', $statusEnum->value)->firstOrFail(); - $app->connection_status_id = $newStatus->id; - $app->save(); - } - public function getSignOutUris(ConnectedApp $app): array { - return data_get($app, 'settings.sign_out_uris', []); + return data_get($app->settings, 'signOutUris', []); + } + + public function updateRedirectUris(ConnectedApp $app, Client $client, array $signInUris, array $signOutUris): void + { + $mergedUris = array_merge($signInUris, $signOutUris); + /** @phpstan-ignore-next-line */ + $client->redirect_uris = $mergedUris; + $client->save(); + $settings = $app->settings ?? []; + $settings['signOutUris'] = $signOutUris; + $app->settings = $settings; + $app->save(); } } diff --git a/resources/views/pages/apps/oidc/⚡show.blade.php b/resources/views/pages/apps/oidc/⚡show.blade.php index a220792..cf6c424 100644 --- a/resources/views/pages/apps/oidc/⚡show.blade.php +++ b/resources/views/pages/apps/oidc/⚡show.blade.php @@ -2,15 +2,21 @@ use App\Concerns\HandlesOperations; use App\Enums\Permissions\AppPermissionEnum; +use App\Enums\ConnectionStatusEnum; use App\Models\{ConnectedApp, ConnectionStatus}; use Livewire\Component; use Livewire\Attributes\Title; +use Livewire\WithFileUploads; use Laravel\Passport\Client; use App\Services\Applications\OIDC\{OidcService, OidcConfigResolver}; +use App\Services\ConnectedAppService; +use App\Services\ApplicationLogoUploader; +use App\Data\ConnectedApp\ConnectAppRequest; +use App\Livewire\Concerns\WithRepeaterFields; new #[Title('OIDC Application Details')] class extends Component { - use HandlesOperations; + use HandlesOperations, WithFileUploads, WithRepeaterFields; public int $appId; public ConnectedApp $app; @@ -19,13 +25,35 @@ class extends Component { public string $selectedTab = 'general-tab'; public ?string $newSecretPlain = null; + // Edit fields + public bool $editGeneralModal = false; + public string $editName = ''; + public $editLogo = null; + + public bool $editRedirectsMode = false; + public array $editSignInUris = []; + public array $editSignOutUris = []; + private OidcService $oidcService; private OidcConfigResolver $configResolver; + private ConnectedAppService $connectedAppService; + private ApplicationLogoUploader $logoUploader; - public function boot(OidcService $oidcService, OidcConfigResolver $configResolver): void - { + public function boot( + OidcService $oidcService, + OidcConfigResolver $configResolver, + ConnectedAppService $connectedAppService, + ApplicationLogoUploader $logoUploader + ): void { $this->oidcService = $oidcService; $this->configResolver = $configResolver; + $this->connectedAppService = $connectedAppService; + $this->logoUploader = $logoUploader; + } + + public function requiresPkce(Client $client): bool + { + return $this->configResolver->requiresPkce($client); } public function mount(int $id): void @@ -63,13 +91,23 @@ public function generateNewSecret(): void ); } - public function toggleStatus(): void + public function changeStatus(string $statusName): void { $this->authorize(AppPermissionEnum::Update->value); $this->attempt( - action: function () { - $this->oidcService->toggleStatus($this->app); + action: function () use ($statusName) { + $statusId = ConnectionStatus::where('name', $statusName)->firstOrFail()->id; + $request = new ConnectAppRequest( + name: $this->app->name, + connectionProtocolId: $this->app->connection_protocol_id, + connectionProviderId: $this->app->connection_provider_id, + slug: $this->app->slug, + connectionStatusId: $statusId, + settings: $this->app->settings, + logo: $this->app->logo, + ); + $this->connectedAppService->update($this->app->id, $request); $this->loadAppData(); }, successMessage: 'Application status updated successfully!' @@ -81,14 +119,91 @@ public function goBack(): void $this->redirectRoute('apps.index', navigate: true); } - public function editApp(): void + public function openEditGeneralModal(): void { - $this->redirectRoute('apps.edit', ['id' => $this->appId], navigate: true); + $this->editName = $this->app->name; + $this->editLogo = null; + $this->editGeneralModal = true; } - public function requiresPkce(Client $client) + public function saveGeneralInfo(): void { - return $this->configResolver->requiresPkce($client); + $this->authorize(AppPermissionEnum::Update->value); + + $this->validate([ + 'editName' => 'required|string|max:255', + 'editLogo' => 'nullable|image|max:2048', + ]); + + $this->attempt( + action: function () { + $logoPath = $this->editLogo + ? $this->logoUploader->store($this->editLogo) + : $this->app->logo; + + $request = new ConnectAppRequest( + name: $this->editName, + connectionProtocolId: $this->app->connection_protocol_id, + connectionProviderId: $this->app->connection_provider_id, + slug: \Illuminate\Support\Str::slug($this->editName), + connectionStatusId: $this->app->connection_status_id, + settings: $this->app->settings, + logo: $logoPath, + ); + + $this->connectedAppService->update($this->app->id, $request); + + if ($this->passportClient && $this->passportClient->name !== $this->editName) { + $this->passportClient->name = $this->editName; + $this->passportClient->save(); + } + + $this->editGeneralModal = false; + $this->success('Application updated successfully!'); + $this->loadAppData(); + } + ); + } + + public function startEditRedirects(): void + { + $clientUris = $this->passportClient->redirect_uris ?? []; + $this->editSignOutUris = $this->signOutUris; + + $this->editSignInUris = array_values(array_filter( + $clientUris, + fn($uri) => !in_array($uri, $this->editSignOutUris, true) + )); + + $this->editRedirectsMode = true; + } + + public function cancelEditRedirects(): void + { + $this->editRedirectsMode = false; + } + + + public function saveRedirectUris(): void + { + $this->authorize(AppPermissionEnum::Update->value); + + if (!$this->passportClient) { + $this->error('No corresponding Passport Client found to update.'); + return; + } + + $signInUris = array_values(array_filter(array_map('trim', $this->editSignInUris))); + $signOutUris = array_values(array_filter(array_map('trim', $this->editSignOutUris))); + + $this->attempt( + action: function () use ($signInUris, $signOutUris) { + $this->oidcService->updateRedirectUris($this->app, $this->passportClient, $signInUris, $signOutUris); + $this->editRedirectsMode = false; + $this->success('Redirect URIs updated successfully!'); + $this->loadAppData(); + } + ); } }; ?> @@ -104,16 +219,11 @@ public function requiresPkce(Client $client) :placeholder="$app->logo ? null : 'lucide.settings'" class="w-16 h-16 rounded-xl border border-gray-200 p-2 shadow-xs bg-gray-50 group-hover:opacity-85 transition-opacity" /> -

{{ $app->name }}

-
@@ -127,30 +237,27 @@ class="font-semibold text-gray-700 ">{{ strtoupper($app->protocol->name) }} - - -
-
-

Client Credentials

-
+
@@ -262,7 +366,7 @@ class="text-xs font-medium text-gray-500">Proof Key for Code Exchange (PKCE) requiresPkce($passportClient)) class="checkbox checkbox-xs" /> @@ -272,22 +376,21 @@ class="text-xs text-gray-600">Require PKCE as additional verification
-
+ -
-
-

Client Secrets

- @if($passportClient) + + @if($passportClient) + - @endif -
+ + @endif
@@ -323,61 +426,104 @@ class="badge {{ !$passportClient->revoked ? 'badge-soft badge-success' : 'badge-
-
+
-
-
-

Sign In Redirect URIs

-
-
- @if(!empty($passportClient?->redirect_uris)) - @foreach($passportClient->redirect_uris as $uri) -
- - {{ $uri }} -
- @endforeach + + + @if($editRedirectsMode) +
+ + +
@else - No Sign In Redirect URIs configured + + @endif +
+ +
+ @if($editRedirectsMode) + + @else + @php + $onlySignInUris = array_values(array_filter( + $passportClient?->redirect_uris ?? [], + fn($uri) => !in_array($uri, $signOutUris, true) + )); + @endphp + @if(!empty($onlySignInUris)) + @foreach($onlySignInUris as $uri) +
+ + {{ $uri }} +
+ @endforeach + @else + No Sign In Redirect URIs configured + @endif @endif
-
+ -
-
-

Sign Out Redirect URIs

-
+
- @if(!empty($signOutUris)) - @foreach($signOutUris as $uri) -
- - {{ $uri }} -
- @endforeach + @if($editRedirectsMode) + @else - No Sign Out Redirect URIs configured + @if(!empty($signOutUris)) + @foreach($signOutUris as $uri) +
+ + {{ $uri }} +
+ @endforeach + @else + No Sign Out Redirect URIs configured + @endif @endif
-
+
-
-
-

Assigned Roles

-
+
@@ -400,7 +546,7 @@ class="flex items-center gap-2 p-2 border border-gray-150 rounded-lg font-mono t SSO Access - + @empty @@ -414,18 +560,14 @@ class="badge badge-soft badge-primary font-semibold text-xs px-2.5 py-1">
-
+
- - + +
-
-
-

Granted OAuth/OIDC - Scopes

-
+
@@ -470,10 +612,33 @@ class="badge badge-soft badge-primary font-semibold text-xs px-2.5 py-1">
-
+
+ + + + + + + + + + + + +
diff --git a/resources/views/pages/apps/url/⚡show.blade.php b/resources/views/pages/apps/url/⚡show.blade.php new file mode 100644 index 0000000..ae87d95 --- /dev/null +++ b/resources/views/pages/apps/url/⚡show.blade.php @@ -0,0 +1,347 @@ +appService = $service; + $this->logoUploader = $logoUploader; + } + + public function mount(int $id): void + { + $this->authorize(AppPermissionEnum::Read->value); + $this->appId = $id; + $this->loadAppData(); + } + + public function loadAppData(): void + { + $this->app = ConnectedApp::with(['protocol', 'status', 'roles'])->findOrFail($this->appId); + } + + public function changeStatus(string $statusName): void + { + $this->authorize(AppPermissionEnum::Update->value); + + $this->attempt( + action: function () use ($statusName) { + $statusId = ConnectionStatus::where('name', $statusName)->firstOrFail()->id; + $request = new ConnectAppRequest( + name: $this->app->name, + connectionProtocolId: $this->app->connection_protocol_id, + connectionProviderId: $this->app->connection_provider_id, + slug: $this->app->slug, + connectionStatusId: $statusId, + settings: $this->app->settings, + logo: $this->app->logo, + ); + $this->appService->update($this->app->id, $request); + $this->loadAppData(); + }, + successMessage: 'Application status updated successfully!' + ); + } + + public function goBack(): void + { + $this->redirectRoute('apps.index', navigate: true); + } + + public function openEditGeneralModal(): void + { + $this->editName = $this->app->name; + $this->editLogo = null; + $this->editGeneralModal = true; + } + + public function saveGeneralInfo(): void + { + $this->authorize(AppPermissionEnum::Update->value); + + $this->validate([ + 'editName' => 'required|string|max:255', + 'editLogo' => 'nullable|image|max:2048', + ]); + + $this->attempt( + action: function () { + $logoPath = $this->editLogo + ? $this->logoUploader->store($this->editLogo) + : $this->app->logo; + + $request = new ConnectAppRequest( + name: $this->editName, + connectionProtocolId: $this->app->connection_protocol_id, + connectionProviderId: $this->app->connection_provider_id, + slug: \Illuminate\Support\Str::slug($this->editName), + connectionStatusId: $this->app->connection_status_id, + settings: $this->app->settings, + logo: $logoPath, + ); + + $this->appService->update($this->app->id, $request); + + $this->editGeneralModal = false; + $this->success('Application updated successfully!'); + $this->loadAppData(); + } + ); + } + + public function startEditAccessUrl(): void + { + $this->editAccessUrl = data_get($this->app, 'settings.access_url', ''); + $this->editAccessUrlMode = true; + } + + public function cancelEditAccessUrl(): void + { + $this->editAccessUrlMode = false; + } + + public function saveAccessUrl(): void + { + $this->authorize(AppPermissionEnum::Update->value); + + $this->validate([ + 'editAccessUrl' => 'required|url|max:255', + ]); + + $this->attempt( + action: function () { + $settings = $this->app->settings ?? []; + $settings['access_url'] = $this->editAccessUrl; + + $request = new ConnectAppRequest( + name: $this->app->name, + connectionProtocolId: $this->app->connection_protocol_id, + connectionProviderId: $this->app->connection_provider_id, + slug: $this->app->slug, + connectionStatusId: $this->app->connection_status_id, + settings: $settings, + logo: $this->app->logo, + ); + + $this->appService->update($this->app->id, $request); + + $this->editAccessUrlMode = false; + $this->success('Access URL updated successfully!'); + $this->loadAppData(); + } + ); + } +}; +?> + +
+ + +
+
+
+ +
+
+
+

{{ $app->name }}

+ +
+

Protocol: {{ strtoupper($app->protocol->name) }} +

+
+
+ +
+ + + + +
+
+
+ + + + + + + +
+ + + @if($editAccessUrlMode) +
+ + +
+ @else + + @endif +
+
+ @if($editAccessUrlMode) + + @else + + @endif +
+
+
+
+ + + +
+ +
+ + + + + + + + + + @forelse($app->roles as $role) + + + + + + @empty + + + + @endforelse + +
Role NamePriorityAccess Type
+ {{ $role->name }} + + {{ $role->priority }} + + + SSO Access + +
+ No specific roles assigned. Accessible by everyone depending on user access + setting. +
+
+
+
+
+
+
+ + + + + + + + + + + + + +
diff --git a/resources/views/pages/apps/⚡index.blade.php b/resources/views/pages/apps/⚡index.blade.php index a4dc812..6f5cf4a 100644 --- a/resources/views/pages/apps/⚡index.blade.php +++ b/resources/views/pages/apps/⚡index.blade.php @@ -70,7 +70,7 @@ public function viewApp(int $appId): void if ($protocol === 'oidc') { $this->redirectRoute('apps.oidc.show', ['id' => $appId], navigate: true); } else { - $this->redirectRoute('apps.edit', ['id' => $appId], navigate: true); + $this->redirectRoute('apps.url.show', ['id' => $appId], navigate: true); } } diff --git a/routes/apps.php b/routes/apps.php index 30e9f43..4eb4ee3 100644 --- a/routes/apps.php +++ b/routes/apps.php @@ -18,11 +18,17 @@ ->name(ConnectionProtocolEnum::OIDC->value.'.') ->group(function (): void { Route::livewire('create', 'pages::apps.oidc.create')->name('create'); + Route::livewire( + '{id}/view', + 'pages::apps.oidc.show' + )->middleware(PermissionMiddleware::using(AppPermissionEnum::Read))->name('show'); + }); Route::prefix(ConnectionProtocolEnum::URL->value) ->name(ConnectionProtocolEnum::URL->value.'.') ->group(function (): void { Route::livewire('create', 'pages::apps.url.create')->name('create'); + Route::livewire('{id}/view', 'pages::apps.url.show')->name('show'); }); }); @@ -31,11 +37,6 @@ 'pages::apps.edit' )->middleware(PermissionMiddleware::using(AppPermissionEnum::Update))->name('edit'); - Route::livewire( - 'oidc/{id}/view', - 'pages::apps.oidc.show' - )->middleware(PermissionMiddleware::using(AppPermissionEnum::Read))->name('oidc.show'); - Route::livewire('/', 'pages::apps.index')->name('index'); }); diff --git a/tests/Feature/OidcAppViewTest.php b/tests/Feature/OidcAppViewTest.php index 4939806..73b1f07 100644 --- a/tests/Feature/OidcAppViewTest.php +++ b/tests/Feature/OidcAppViewTest.php @@ -87,3 +87,76 @@ $this->passportClient->refresh(); expect(Hash::check($newSecretPlain, $this->passportClient->secret))->toBeTrue(); }); + +it('allows updating app name and logo', function (): void { + $admin = User::factory()->create(); + $admin->assignRole('admin'); + + $this->actingAs($admin); + + $logo = Illuminate\Http\UploadedFile::fake()->image('logo.png'); + + Livewire::test('pages::apps.oidc.show', ['id' => $this->connectedApp->id]) + ->call('openEditGeneralModal') + ->assertSet('editName', 'Test OIDC App') + ->set('editName', 'Updated OIDC App Name') + ->set('editLogo', $logo) + ->call('saveGeneralInfo') + ->assertHasNoErrors(); + + // Verify it updated in Database + $this->connectedApp->refresh(); + expect($this->connectedApp->name)->toBe('Updated OIDC App Name') + ->and($this->connectedApp->logo)->not->toBeNull(); + + // Verify Passport client name was updated + $this->passportClient->refresh(); + expect($this->passportClient->name)->toBe('Updated OIDC App Name'); +}); + +it('allows updating connection status via changeStatus', function (): void { + $admin = User::factory()->create(); + $admin->assignRole('admin'); + + $this->actingAs($admin); + + // Initial status should be 'connected' + expect($this->connectedApp->status->name)->toBe('connected'); + + Livewire::test('pages::apps.oidc.show', ['id' => $this->connectedApp->id]) + ->call('changeStatus', 'disconnected') + ->assertHasNoErrors(); + + // Verify status updated in DB + $this->connectedApp->refresh(); + expect($this->connectedApp->status->name)->toBe('disconnected'); +}); + +it('allows updating redirect URIs', function (): void { + $admin = User::factory()->create(); + $admin->assignRole('admin'); + + $this->actingAs($admin); + + Livewire::test('pages::apps.oidc.show', ['id' => $this->connectedApp->id]) + ->call('startEditRedirects') + ->assertSet('editSignInUris', ['https://test-oidc-app.example.com/callback']) + ->assertSet('editSignOutUris', ['https://test-oidc-app.example.com/logout']) + // Add a new sign-in URI + ->call('addRepeaterItem', 'editSignInUris') + ->set('editSignInUris.1', 'https://test-oidc-app.example.com/callback2') + // Remove a sign-out URI + ->call('removeRepeaterItem', 'editSignOutUris', 0) + ->call('saveRedirectUris') + ->assertHasNoErrors(); + + // Verify changes in Passport Client and Connected App settings + $this->passportClient->refresh(); + expect($this->passportClient->redirect_uris)->toBe([ + 'https://test-oidc-app.example.com/callback', + 'https://test-oidc-app.example.com/callback2', + ]); + + $this->connectedApp->refresh(); + expect($this->connectedApp->settings['signOutUris'] ?? [])->toBe([]); +}); diff --git a/tests/Feature/UrlAppViewTest.php b/tests/Feature/UrlAppViewTest.php new file mode 100644 index 0000000..f8fd68a --- /dev/null +++ b/tests/Feature/UrlAppViewTest.php @@ -0,0 +1,102 @@ +seed(Database\Seeders\PermissionSeeder::class); + $this->seed(Database\Seeders\DefaultRoleWithPermissionSeeder::class); + $this->seed(Database\Seeders\ConnectionProtocolSeeder::class); + $this->seed(Database\Seeders\ConnectionProviderSeeder::class); + $this->seed(Database\Seeders\ConnectionStatusSeeder::class); + + $this->urlProtocol = ConnectionProtocol::query()->where('name', 'url')->firstOrFail(); + $this->status = ConnectionStatus::query()->where('name', 'connected')->firstOrFail(); + + $this->connectedApp = ConnectedApp::query()->create([ + 'name' => 'Test URL App', + 'slug' => 'test-url-app', + 'connection_protocol_id' => $this->urlProtocol->id, + 'connection_provider_id' => 0, + 'connection_status_id' => $this->status->id, + 'settings' => [ + 'access_url' => 'https://test-url-app.example.com', + 'is_connected_to_microsoft' => false, + ], + ]); +}); + +it('authorizes only admin users to view the URL show page', function (): void { + $user = User::factory()->create(); + $user->assignRole('user'); + + Livewire::actingAs($user) + ->test('pages::apps.url.show', ['id' => $this->connectedApp->id]) + ->assertForbidden(); + + $admin = User::factory()->create(); + $admin->assignRole('admin'); + + Livewire::actingAs($admin) + ->test('pages::apps.url.show', ['id' => $this->connectedApp->id]) + ->assertOk(); +}); + +it('allows updating name and logo', function (): void { + $admin = User::factory()->create(); + $admin->assignRole('admin'); + + $this->actingAs($admin); + + $logo = Illuminate\Http\UploadedFile::fake()->image('logo.png'); + + Livewire::test('pages::apps.url.show', ['id' => $this->connectedApp->id]) + ->call('openEditGeneralModal') + ->assertSet('editName', 'Test URL App') + ->set('editName', 'Updated URL App Name') + ->set('editLogo', $logo) + ->call('saveGeneralInfo') + ->assertHasNoErrors(); + + $this->connectedApp->refresh(); + expect($this->connectedApp->name)->toBe('Updated URL App Name') + ->and($this->connectedApp->logo)->not->toBeNull(); +}); + +it('allows updating access URL in place', function (): void { + $admin = User::factory()->create(); + $admin->assignRole('admin'); + + $this->actingAs($admin); + + Livewire::test('pages::apps.url.show', ['id' => $this->connectedApp->id]) + ->call('startEditAccessUrl') + ->assertSet('editAccessUrl', 'https://test-url-app.example.com') + ->set('editAccessUrl', 'https://updated-url-app.example.com') + ->call('saveAccessUrl') + ->assertHasNoErrors(); + + $this->connectedApp->refresh(); + expect(data_get($this->connectedApp->settings, 'access_url'))->toBe('https://updated-url-app.example.com'); +}); + +it('allows updating connection status via changeStatus', function (): void { + $admin = User::factory()->create(); + $admin->assignRole('admin'); + + $this->actingAs($admin); + + expect($this->connectedApp->status->name)->toBe('connected'); + + Livewire::test('pages::apps.url.show', ['id' => $this->connectedApp->id]) + ->call('changeStatus', 'disconnected') + ->assertHasNoErrors(); + + $this->connectedApp->refresh(); + expect($this->connectedApp->status->name)->toBe('disconnected'); +});