TenantAtlas/tests/Unit/VerificationAssistViewModelBuilderTest.php
2026-03-14 02:59:06 +01:00

341 lines
12 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\Tenant;
use App\Services\Intune\TenantRequiredPermissionsViewModelBuilder;
use App\Support\Links\RequiredPermissionsLinks;
use App\Support\Providers\ProviderNextStepsRegistry;
use App\Support\Providers\ProviderReasonCodes;
use App\Support\Verification\VerificationAssistViewModelBuilder;
use App\Support\Verification\VerificationReportOverall;
use App\Support\Verification\VerificationReportWriter;
use Carbon\CarbonImmutable;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('derives blocked assist visibility and action availability from the stored diagnostics', function (): void {
$tenant = Tenant::factory()->create([
'external_id' => 'tenant-assist-blocked-a',
'name' => 'Blocked Tenant',
]);
$permissionsBuilder = Mockery::mock(TenantRequiredPermissionsViewModelBuilder::class);
$permissionsBuilder
->shouldReceive('build')
->twice()
->withArgs(function (Tenant $passedTenant, array $filters) use ($tenant): bool {
return (int) $passedTenant->getKey() === (int) $tenant->getKey()
&& ($filters['status'] ?? null) === 'all'
&& ($filters['type'] ?? null) === 'all'
&& ($filters['features'] ?? null) === []
&& ($filters['search'] ?? null) === '';
})
->andReturn([
'tenant' => [
'id' => (int) $tenant->getKey(),
'external_id' => (string) $tenant->external_id,
'name' => (string) $tenant->name,
],
'overview' => [
'overall' => VerificationReportOverall::Blocked->value,
'counts' => [
'missing_application' => 1,
'missing_delegated' => 1,
'present' => 3,
'error' => 0,
],
'freshness' => [
'last_refreshed_at' => now()->toIso8601String(),
'is_stale' => false,
],
],
'permissions' => [
[
'key' => 'DeviceManagementConfiguration.Read.All',
'type' => 'application',
'description' => 'Read device configurations',
'features' => ['inventory'],
'status' => 'missing',
'details' => null,
],
[
'key' => 'User.Read.All',
'type' => 'delegated',
'description' => 'Read users',
'features' => ['directory'],
'status' => 'missing',
'details' => null,
],
[
'key' => 'Group.Read.All',
'type' => 'application',
'description' => 'Read groups',
'features' => ['directory'],
'status' => 'granted',
'details' => null,
],
],
'copy' => [
'application' => "DeviceManagementConfiguration.Read.All\nPolicy.Read.All",
'delegated' => 'User.Read.All',
],
]);
$builder = new VerificationAssistViewModelBuilder(
$permissionsBuilder,
app(ProviderNextStepsRegistry::class),
);
$report = VerificationReportWriter::build('provider.connection.check', [
[
'key' => 'permissions.admin_consent',
'title' => 'Required application permissions',
'status' => 'fail',
'severity' => 'critical',
'blocking' => true,
'reason_code' => ProviderReasonCodes::ProviderConsentMissing,
'message' => 'Grant admin consent before continuing.',
'evidence' => [],
'next_steps' => [],
],
]);
$visibility = $builder->visibility($tenant, $report);
$assist = $builder->build(
tenant: $tenant,
verificationReport: $report,
verificationStatus: 'blocked',
isVerificationStale: false,
staleReason: null,
canAccessProviderConnectionDiagnostics: true,
);
expect($visibility)->toMatchArray([
'is_visible' => true,
'reason' => 'permission_blocked',
]);
expect($assist)->toMatchArray([
'tenant' => [
'id' => (int) $tenant->getKey(),
'external_id' => (string) $tenant->external_id,
'name' => (string) $tenant->name,
],
'verification' => [
'overall' => VerificationReportOverall::Blocked->value,
'status' => 'blocked',
'is_stale' => false,
'stale_reason' => null,
],
'copy' => [
'application' => "DeviceManagementConfiguration.Read.All\nPolicy.Read.All",
'delegated' => 'User.Read.All',
],
]);
expect($assist['overview'])->toMatchArray([
'overall' => VerificationReportOverall::Blocked->value,
'counts' => [
'missing_application' => 1,
'missing_delegated' => 1,
'present' => 3,
'error' => 0,
],
]);
expect($assist['overview']['freshness']['is_stale'])->toBeFalse();
expect($assist['missing_permissions']['application'])->toHaveCount(1)
->and($assist['missing_permissions']['delegated'])->toHaveCount(1)
->and($assist['actions']['full_page'])->toMatchArray([
'label' => 'Open full page',
'url' => RequiredPermissionsLinks::requiredPermissions($tenant),
'opens_in_new_tab' => true,
'available' => true,
'is_secondary' => true,
])
->and($assist['actions']['copy_application'])->toMatchArray([
'label' => 'Copy missing application permissions',
'available' => true,
])
->and($assist['actions']['copy_delegated'])->toMatchArray([
'label' => 'Copy missing delegated permissions',
'available' => true,
])
->and($assist['actions']['grant_admin_consent'])->toMatchArray([
'label' => 'Grant admin consent',
'url' => RequiredPermissionsLinks::adminConsentGuideUrl(),
'opens_in_new_tab' => true,
'available' => true,
])
->and($assist['fallback'])->toMatchArray([
'has_incomplete_detail' => false,
'message' => null,
]);
expect($assist['actions']['manage_provider_connection'])->toMatchArray([
'label' => 'Manage Provider Connections',
'opens_in_new_tab' => true,
'available' => true,
]);
expect((string) $assist['actions']['manage_provider_connection']['url'])->toContain('/admin/provider-connections');
});
it('hides the assist when the verification report is ready and permission diagnostics are healthy', function (): void {
$tenant = Tenant::factory()->create([
'external_id' => 'tenant-assist-ready-a',
]);
$permissionsBuilder = Mockery::mock(TenantRequiredPermissionsViewModelBuilder::class);
$permissionsBuilder
->shouldReceive('build')
->once()
->andReturn([
'tenant' => [
'id' => (int) $tenant->getKey(),
'external_id' => (string) $tenant->external_id,
'name' => (string) $tenant->name,
],
'overview' => [
'overall' => VerificationReportOverall::Ready->value,
'counts' => [
'missing_application' => 0,
'missing_delegated' => 0,
'present' => 15,
'error' => 0,
],
'freshness' => [
'last_refreshed_at' => now()->toIso8601String(),
'is_stale' => false,
],
],
'permissions' => [],
'copy' => [
'application' => '',
'delegated' => '',
],
]);
$builder = new VerificationAssistViewModelBuilder(
$permissionsBuilder,
app(ProviderNextStepsRegistry::class),
);
$report = VerificationReportWriter::build('provider.connection.check', [
[
'key' => 'provider.connection.check',
'title' => 'Provider connection check',
'status' => 'pass',
'severity' => 'info',
'blocking' => false,
'reason_code' => 'ok',
'message' => 'Connection is healthy.',
'evidence' => [],
'next_steps' => [],
],
]);
expect($builder->visibility($tenant, $report))->toMatchArray([
'is_visible' => false,
'reason' => 'hidden_ready',
]);
});
it('builds degraded fallback messaging when permission detail is stale or incomplete', function (): void {
$tenant = Tenant::factory()->create([
'external_id' => 'tenant-assist-degraded-a',
]);
$staleAt = CarbonImmutable::now()->subDays(45)->toIso8601String();
$permissionsBuilder = Mockery::mock(TenantRequiredPermissionsViewModelBuilder::class);
$permissionsBuilder
->shouldReceive('build')
->twice()
->andReturn([
'tenant' => [
'id' => (int) $tenant->getKey(),
'external_id' => (string) $tenant->external_id,
'name' => (string) $tenant->name,
],
'overview' => [
'overall' => VerificationReportOverall::NeedsAttention->value,
'counts' => [
'missing_application' => 0,
'missing_delegated' => 0,
'present' => 14,
'error' => 1,
],
'freshness' => [
'last_refreshed_at' => $staleAt,
'is_stale' => true,
],
],
'permissions' => [
[
'key' => 'DeviceManagementManagedDevices.Read.All',
'type' => 'application',
'description' => 'Stored row is incomplete',
'features' => ['inventory'],
'status' => 'error',
'details' => ['reason_code' => 'permission_denied'],
],
],
'copy' => [
'application' => '',
'delegated' => '',
],
]);
$builder = new VerificationAssistViewModelBuilder(
$permissionsBuilder,
app(ProviderNextStepsRegistry::class),
);
$report = VerificationReportWriter::build('provider.connection.check', [
[
'key' => 'permissions.admin_consent',
'title' => 'Required application permissions',
'status' => 'warn',
'severity' => 'medium',
'blocking' => false,
'reason_code' => ProviderReasonCodes::ProviderPermissionRefreshFailed,
'message' => 'Stored permission data needs review.',
'evidence' => [],
'next_steps' => [],
],
]);
$visibility = $builder->visibility($tenant, $report);
$assist = $builder->build(
tenant: $tenant,
verificationReport: $report,
verificationStatus: 'needs_attention',
isVerificationStale: true,
staleReason: 'The selected provider connection has changed since this verification run.',
canAccessProviderConnectionDiagnostics: false,
);
expect($visibility)->toMatchArray([
'is_visible' => true,
'reason' => 'permission_attention',
]);
expect($assist['verification'])->toMatchArray([
'overall' => VerificationReportOverall::NeedsAttention->value,
'status' => 'needs_attention',
'is_stale' => true,
'stale_reason' => 'The selected provider connection has changed since this verification run.',
]);
expect($assist['actions']['copy_application']['available'])->toBeFalse()
->and($assist['actions']['copy_delegated']['available'])->toBeFalse()
->and($assist['actions']['grant_admin_consent']['available'])->toBeFalse()
->and($assist['actions']['manage_provider_connection']['available'])->toBeFalse()
->and($assist['fallback']['has_incomplete_detail'])->toBeTrue()
->and($assist['fallback']['message'])->not->toBeNull();
});