341 lines
12 KiB
PHP
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();
|
|
});
|