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');
|
||||
|
||||
$this->actingAs($user)
|
||||
->get("/admin/t/{$tenant->external_id}/required-permissions")
|
||||
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||
->assertSuccessful()
|
||||
->assertSee('Guidance')
|
||||
->assertSee('Who can fix this?', false)
|
||||
|
||||
@ -1,13 +1,21 @@
|
||||
<?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();
|
||||
|
||||
[$user, $tenant] = createUserWithTenant(role: 'readonly');
|
||||
|
||||
Queue::fake();
|
||||
|
||||
assertNoOutboundHttp(function () use ($user, $tenant): void {
|
||||
$this->actingAs($user)
|
||||
->get("/admin/t/{$tenant->external_id}/required-permissions")
|
||||
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||
->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)
|
||||
->get("/admin/t/{$tenant->external_id}/required-permissions")
|
||||
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||
->assertSuccessful()
|
||||
->assertSee('All required permissions are present', false);
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
->assertDontSee('data-permission-key="Gamma.Manage.All"', false);
|
||||
|
||||
$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()
|
||||
->assertSee('wire:model.live="status"', false);
|
||||
|
||||
@ -71,7 +71,7 @@
|
||||
->assertSee('data-permission-key="Gamma.Manage.All"', false);
|
||||
|
||||
$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();
|
||||
|
||||
$delegatedResponse
|
||||
@ -85,7 +85,7 @@
|
||||
]);
|
||||
|
||||
$featureResponse = $this->actingAs($user)
|
||||
->get("/admin/t/{$tenant->external_id}/required-permissions?{$featureQuery}")
|
||||
->get("/admin/tenants/{$tenant->external_id}/required-permissions?{$featureQuery}")
|
||||
->assertSuccessful();
|
||||
|
||||
$featureResponse
|
||||
@ -94,7 +94,7 @@
|
||||
->assertDontSee('data-permission-key="Beta.Read.All"', false);
|
||||
|
||||
$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();
|
||||
|
||||
$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)
|
||||
->get("/admin/t/{$tenant->external_id}/required-permissions")
|
||||
->get("/admin/tenants/{$tenant->external_id}/required-permissions")
|
||||
->assertSuccessful()
|
||||
->assertSee('Blocked', 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))
|
||||
->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