test: cover required permissions hardening routes and freshness
This commit is contained in:
parent
43dff0f2f4
commit
b990181bab
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Workspace;
|
||||||
|
use App\Models\WorkspaceMembership;
|
||||||
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
|
|
||||||
|
it('returns 200 for tenant-entitled readonly members on the canonical required permissions route', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'readonly');
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||||
|
->assertOk();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 for workspace members without tenant entitlement on the canonical route', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$workspace = Workspace::factory()->create();
|
||||||
|
$tenant = Tenant::factory()->create([
|
||||||
|
'workspace_id' => (int) $workspace->getKey(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
WorkspaceMembership::factory()->create([
|
||||||
|
'workspace_id' => (int) $workspace->getKey(),
|
||||||
|
'user_id' => (int) $user->getKey(),
|
||||||
|
'role' => 'owner',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->withSession([
|
||||||
|
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
|
||||||
|
])
|
||||||
|
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||||
|
->assertNotFound();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 for users who are not workspace members', function (): void {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$workspace = Workspace::factory()->create();
|
||||||
|
$tenant = Tenant::factory()->create([
|
||||||
|
'workspace_id' => (int) $workspace->getKey(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->withSession([
|
||||||
|
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
|
||||||
|
])
|
||||||
|
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||||
|
->assertNotFound();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 when the route tenant is invalid instead of falling back to the current tenant context', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'readonly');
|
||||||
|
|
||||||
|
Tenant::query()->whereKey((int) $tenant->getKey())->update(['is_current' => true]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get('/admin/tenants/invalid-tenant-id/required-permissions')
|
||||||
|
->assertNotFound();
|
||||||
|
});
|
||||||
@ -13,7 +13,7 @@
|
|||||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->get("/admin/t/{$tenant->external_id}/required-permissions")
|
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('Guidance')
|
->assertSee('Guidance')
|
||||||
->assertSee('Who can fix this?', false)
|
->assertSee('Who can fix this?', false)
|
||||||
|
|||||||
@ -1,13 +1,21 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
it('renders the required permissions page without Graph or outbound HTTP calls', function (): void {
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Queue;
|
||||||
|
|
||||||
|
it('renders the canonical required permissions page without Graph, outbound HTTP, or queue dispatches', function (): void {
|
||||||
bindFailHardGraphClient();
|
bindFailHardGraphClient();
|
||||||
|
|
||||||
[$user, $tenant] = createUserWithTenant(role: 'readonly');
|
[$user, $tenant] = createUserWithTenant(role: 'readonly');
|
||||||
|
|
||||||
|
Queue::fake();
|
||||||
|
|
||||||
assertNoOutboundHttp(function () use ($user, $tenant): void {
|
assertNoOutboundHttp(function () use ($user, $tenant): void {
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->get("/admin/t/{$tenant->external_id}/required-permissions")
|
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||||
->assertSuccessful();
|
->assertSuccessful();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Queue::assertNothingPushed();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
it('renders the no-data state with a canonical start verification link when no stored permission data exists', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'readonly');
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertSee('Keine Daten verfügbar')
|
||||||
|
->assertSee('/admin/onboarding', false)
|
||||||
|
->assertSee('Start verification');
|
||||||
|
});
|
||||||
@ -51,7 +51,7 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$missingResponse = $this->actingAs($user)
|
$missingResponse = $this->actingAs($user)
|
||||||
->get("/admin/t/{$tenant->external_id}/required-permissions")
|
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('All required permissions are present', false);
|
->assertSee('All required permissions are present', false);
|
||||||
|
|
||||||
@ -61,7 +61,7 @@
|
|||||||
->assertDontSee('data-permission-key="Gamma.Manage.All"', false);
|
->assertDontSee('data-permission-key="Gamma.Manage.All"', false);
|
||||||
|
|
||||||
$presentResponse = $this->actingAs($user)
|
$presentResponse = $this->actingAs($user)
|
||||||
->get("/admin/t/{$tenant->external_id}/required-permissions?status=present")
|
->get("/admin/tenants/{$tenant->external_id}/required-permissions?status=present")
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('wire:model.live="status"', false);
|
->assertSee('wire:model.live="status"', false);
|
||||||
|
|
||||||
@ -71,7 +71,7 @@
|
|||||||
->assertSee('data-permission-key="Gamma.Manage.All"', false);
|
->assertSee('data-permission-key="Gamma.Manage.All"', false);
|
||||||
|
|
||||||
$delegatedResponse = $this->actingAs($user)
|
$delegatedResponse = $this->actingAs($user)
|
||||||
->get("/admin/t/{$tenant->external_id}/required-permissions?status=present&type=delegated")
|
->get("/admin/tenants/{$tenant->external_id}/required-permissions?status=present&type=delegated")
|
||||||
->assertSuccessful();
|
->assertSuccessful();
|
||||||
|
|
||||||
$delegatedResponse
|
$delegatedResponse
|
||||||
@ -85,7 +85,7 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$featureResponse = $this->actingAs($user)
|
$featureResponse = $this->actingAs($user)
|
||||||
->get("/admin/t/{$tenant->external_id}/required-permissions?{$featureQuery}")
|
->get("/admin/tenants/{$tenant->external_id}/required-permissions?{$featureQuery}")
|
||||||
->assertSuccessful();
|
->assertSuccessful();
|
||||||
|
|
||||||
$featureResponse
|
$featureResponse
|
||||||
@ -94,7 +94,7 @@
|
|||||||
->assertDontSee('data-permission-key="Beta.Read.All"', false);
|
->assertDontSee('data-permission-key="Beta.Read.All"', false);
|
||||||
|
|
||||||
$searchResponse = $this->actingAs($user)
|
$searchResponse = $this->actingAs($user)
|
||||||
->get("/admin/t/{$tenant->external_id}/required-permissions?status=all&search=delegated")
|
->get("/admin/tenants/{$tenant->external_id}/required-permissions?status=all&search=delegated")
|
||||||
->assertSuccessful();
|
->assertSuccessful();
|
||||||
|
|
||||||
$searchResponse
|
$searchResponse
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
it('returns 404 for the legacy tenant-plane required permissions route', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'readonly');
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get("/admin/t/{$tenant->external_id}/required-permissions")
|
||||||
|
->assertNotFound();
|
||||||
|
});
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
it('renders re-run verification and next-step links using canonical manage surfaces only', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'readonly');
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertSee('Re-run verification')
|
||||||
|
->assertSee('/admin/onboarding', false)
|
||||||
|
->assertDontSee('/admin/t/', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders sections in summary-issues-passed-technical order and keeps technical details collapsed by default', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'readonly');
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertSeeInOrder(['Summary', 'Issues', 'Passed', 'Technical details'])
|
||||||
|
->assertSee('<details data-testid="technical-details"', false)
|
||||||
|
->assertDontSee('data-testid="technical-details" open', false);
|
||||||
|
});
|
||||||
@ -26,7 +26,7 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->get("/admin/t/{$tenant->external_id}/required-permissions")
|
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('Blocked', false)
|
->assertSee('Blocked', false)
|
||||||
->assertSee('applyFeatureFilter', false)
|
->assertSee('applyFeatureFilter', false)
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use App\Models\Tenant;
|
|
||||||
use App\Services\Auth\CapabilityResolver;
|
|
||||||
use App\Support\Auth\Capabilities;
|
|
||||||
|
|
||||||
it('returns 404 for non-members accessing required permissions', function (): void {
|
|
||||||
[$user, $tenant] = createUserWithTenant(role: 'readonly');
|
|
||||||
|
|
||||||
$otherTenant = Tenant::factory()->create([
|
|
||||||
'workspace_id' => (int) $tenant->workspace_id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->get("/admin/t/{$otherTenant->external_id}/required-permissions")
|
|
||||||
->assertNotFound();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 403 for members without tenant.view capability accessing required permissions', function (): void {
|
|
||||||
[$user, $tenant] = createUserWithTenant(role: 'readonly');
|
|
||||||
|
|
||||||
$this->mock(CapabilityResolver::class, function ($mock): void {
|
|
||||||
$mock->shouldReceive('isMember')
|
|
||||||
->andReturn(true);
|
|
||||||
|
|
||||||
$mock->shouldReceive('can')
|
|
||||||
->andReturnUsing(fn ($user, $tenant, $capability): bool => $capability !== Capabilities::TENANT_VIEW);
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->get("/admin/t/{$tenant->external_id}/required-permissions")
|
|
||||||
->assertForbidden();
|
|
||||||
});
|
|
||||||
34
tests/Unit/TenantRequiredPermissionsFreshnessTest.php
Normal file
34
tests/Unit/TenantRequiredPermissionsFreshnessTest.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Services\Intune\TenantRequiredPermissionsViewModelBuilder;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
|
||||||
|
it('marks freshness as stale when last refreshed is missing', function (): void {
|
||||||
|
$freshness = TenantRequiredPermissionsViewModelBuilder::deriveFreshness(
|
||||||
|
null,
|
||||||
|
CarbonImmutable::parse('2026-02-08 12:00:00'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($freshness['last_refreshed_at'])->toBeNull()
|
||||||
|
->and($freshness['is_stale'])->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks freshness as stale when last refreshed is older than 30 days', function (): void {
|
||||||
|
$freshness = TenantRequiredPermissionsViewModelBuilder::deriveFreshness(
|
||||||
|
CarbonImmutable::parse('2026-01-08 11:59:59'),
|
||||||
|
CarbonImmutable::parse('2026-02-08 12:00:00'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($freshness['is_stale'])->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks freshness as not stale when last refreshed is exactly 30 days old', function (): void {
|
||||||
|
$freshness = TenantRequiredPermissionsViewModelBuilder::deriveFreshness(
|
||||||
|
CarbonImmutable::parse('2026-01-09 12:00:00'),
|
||||||
|
CarbonImmutable::parse('2026-02-08 12:00:00'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($freshness['is_stale'])->toBeFalse();
|
||||||
|
});
|
||||||
@ -98,3 +98,27 @@
|
|||||||
expect(TenantRequiredPermissionsViewModelBuilder::deriveOverallStatus($rows))
|
expect(TenantRequiredPermissionsViewModelBuilder::deriveOverallStatus($rows))
|
||||||
->toBe(VerificationReportOverall::Ready->value);
|
->toBe(VerificationReportOverall::Ready->value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('maps overall to needs_attention when freshness is stale without explicit permission gaps', function (): void {
|
||||||
|
$rows = [
|
||||||
|
[
|
||||||
|
'key' => 'A',
|
||||||
|
'type' => 'application',
|
||||||
|
'description' => null,
|
||||||
|
'features' => ['backup'],
|
||||||
|
'status' => 'granted',
|
||||||
|
'details' => null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'B',
|
||||||
|
'type' => 'delegated',
|
||||||
|
'description' => null,
|
||||||
|
'features' => ['backup'],
|
||||||
|
'status' => 'granted',
|
||||||
|
'details' => null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(TenantRequiredPermissionsViewModelBuilder::deriveOverallStatus($rows, true))
|
||||||
|
->toBe(VerificationReportOverall::NeedsAttention->value);
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user