Implements the 074 verification checklist framework. Highlights: - Versioned verification report contract stored in operation_runs.context.verification_report (DB-only viewer). - Strict sanitizer/redaction (evidence pointers only; no tokens/headers/payloads) + schema validation. - Centralized BADGE-001 semantics for check status, severity, and overall report outcome. - Deterministic start (dedupe while active) via shared StartVerification service; capability-first authorization (non-member 404, member missing capability 403). - Completion audit event (verification.completed) with redacted metadata. - Integrations: OperationRun detail viewer, onboarding wizard verification step, provider connection start surfaces. Tests: - vendor/bin/sail artisan test --compact tests/Feature/Verification tests/Unit/Badges/VerificationBadgesTest.php - vendor/bin/sail bin pint --dirty Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box> Reviewed-on: #89
166 lines
5.4 KiB
PHP
166 lines
5.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Jobs\ProviderConnectionHealthCheckJob;
|
|
use App\Models\AuditLog;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ProviderCredential;
|
|
use App\Services\Graph\GraphClientInterface;
|
|
use App\Services\Graph\GraphResponse;
|
|
use App\Services\OperationRunService;
|
|
use App\Services\Providers\MicrosoftProviderHealthCheck;
|
|
use App\Support\Audit\AuditActionId;
|
|
use App\Support\Verification\VerificationReportSchema;
|
|
|
|
it('writes a sanitized verification report for failed provider connection checks', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'operator');
|
|
|
|
$connection = ProviderConnection::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => fake()->uuid(),
|
|
]);
|
|
|
|
ProviderCredential::factory()->create([
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'payload' => [
|
|
'tenant_id' => (string) $connection->entra_tenant_id,
|
|
'client_id' => fake()->uuid(),
|
|
'client_secret' => fake()->sha1(),
|
|
],
|
|
]);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'user_id' => (int) $user->getKey(),
|
|
'type' => 'provider.connection.check',
|
|
'status' => 'running',
|
|
'outcome' => 'pending',
|
|
'context' => [
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
],
|
|
]);
|
|
|
|
$this->mock(GraphClientInterface::class, function ($mock): void {
|
|
$mock->shouldReceive('getOrganization')
|
|
->once()
|
|
->andReturn(new GraphResponse(false, [], 401, ['Bearer super-secret-token']));
|
|
});
|
|
|
|
$job = new ProviderConnectionHealthCheckJob(
|
|
tenantId: (int) $tenant->getKey(),
|
|
userId: (int) $user->getKey(),
|
|
providerConnectionId: (int) $connection->getKey(),
|
|
operationRun: $run,
|
|
);
|
|
|
|
$job->handle(
|
|
healthCheck: app(MicrosoftProviderHealthCheck::class),
|
|
runs: app(OperationRunService::class),
|
|
);
|
|
|
|
$run = $run->fresh();
|
|
|
|
expect($run)->not->toBeNull();
|
|
expect($run->status)->toBe('completed');
|
|
expect($run->outcome)->toBe('failed');
|
|
|
|
$context = is_array($run->context) ? $run->context : [];
|
|
$report = $context['verification_report'] ?? null;
|
|
|
|
expect($report)->toBeArray();
|
|
expect(VerificationReportSchema::isValidReport($report))->toBeTrue();
|
|
expect(json_encode($report))->not->toContain('Bearer ');
|
|
expect($report['checks'][0]['reason_code'] ?? null)->toBe('authentication_failed');
|
|
|
|
foreach (($report['checks'] ?? []) as $check) {
|
|
expect($check)->toBeArray();
|
|
|
|
foreach (($check['evidence'] ?? []) as $pointer) {
|
|
expect($pointer)->toBeArray();
|
|
expect(array_keys($pointer))->toEqualCanonicalizing(['kind', 'value']);
|
|
}
|
|
}
|
|
|
|
$audit = AuditLog::query()
|
|
->where('workspace_id', (int) $tenant->workspace_id)
|
|
->where('action', AuditActionId::VerificationCompleted->value)
|
|
->latest('id')
|
|
->first();
|
|
|
|
expect($audit)->not->toBeNull();
|
|
expect($audit?->metadata)->toMatchArray([
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
]);
|
|
});
|
|
|
|
it('writes a verification report for successful provider connection checks', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'operator');
|
|
|
|
$connection = ProviderConnection::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => fake()->uuid(),
|
|
]);
|
|
|
|
ProviderCredential::factory()->create([
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'payload' => [
|
|
'tenant_id' => (string) $connection->entra_tenant_id,
|
|
'client_id' => fake()->uuid(),
|
|
'client_secret' => fake()->sha1(),
|
|
],
|
|
]);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'user_id' => (int) $user->getKey(),
|
|
'type' => 'provider.connection.check',
|
|
'status' => 'running',
|
|
'outcome' => 'pending',
|
|
'context' => [
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
],
|
|
]);
|
|
|
|
$this->mock(GraphClientInterface::class, function ($mock): void {
|
|
$mock->shouldReceive('getOrganization')
|
|
->once()
|
|
->andReturn(new GraphResponse(true, [
|
|
'id' => 'org_123',
|
|
'displayName' => 'Org 123',
|
|
], 200));
|
|
});
|
|
|
|
$job = new ProviderConnectionHealthCheckJob(
|
|
tenantId: (int) $tenant->getKey(),
|
|
userId: (int) $user->getKey(),
|
|
providerConnectionId: (int) $connection->getKey(),
|
|
operationRun: $run,
|
|
);
|
|
|
|
$job->handle(
|
|
healthCheck: app(MicrosoftProviderHealthCheck::class),
|
|
runs: app(OperationRunService::class),
|
|
);
|
|
|
|
$run = $run->fresh();
|
|
|
|
expect($run)->not->toBeNull();
|
|
expect($run->status)->toBe('completed');
|
|
expect($run->outcome)->toBe('succeeded');
|
|
|
|
$context = is_array($run->context) ? $run->context : [];
|
|
$report = $context['verification_report'] ?? null;
|
|
|
|
expect($report)->toBeArray();
|
|
expect(VerificationReportSchema::isValidReport($report))->toBeTrue();
|
|
expect($report['summary']['counts'] ?? [])->toMatchArray([
|
|
'total' => 1,
|
|
'pass' => 1,
|
|
'fail' => 0,
|
|
]);
|
|
});
|