181 lines
6.8 KiB
PHP
181 lines
6.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\AuditLog;
|
|
use App\Models\OperationRun;
|
|
use App\Models\Tenant;
|
|
use App\Services\Verification\VerificationCheckAcknowledgementService;
|
|
use App\Support\Audit\AuditActionId;
|
|
use App\Support\Verification\VerificationReportFingerprint;
|
|
use Illuminate\Auth\Access\AuthorizationException;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
|
|
it('returns 404 for non-members on verification check acknowledgement', function (): void {
|
|
$tenant = Tenant::factory()->create();
|
|
$otherTenant = Tenant::factory()->create();
|
|
|
|
[$user] = createUserWithTenant($otherTenant, role: 'readonly');
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'provider.connection.check',
|
|
'status' => 'completed',
|
|
'outcome' => 'failed',
|
|
'context' => [
|
|
'verification_report' => json_decode(
|
|
(string) file_get_contents(base_path('specs/074-verification-checklist/contracts/examples/fail.json')),
|
|
true,
|
|
512,
|
|
JSON_THROW_ON_ERROR,
|
|
),
|
|
],
|
|
]);
|
|
|
|
expect(fn () => app(VerificationCheckAcknowledgementService::class)->acknowledge(
|
|
tenant: $tenant,
|
|
run: $run,
|
|
checkKey: 'provider_connection.token_acquisition',
|
|
ackReason: 'Known issue',
|
|
expiresAt: null,
|
|
actor: $user,
|
|
))->toThrow(NotFoundHttpException::class);
|
|
});
|
|
|
|
it('returns 403 for members without tenant_verification.acknowledge on verification check acknowledgement', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'operator');
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'provider.connection.check',
|
|
'status' => 'completed',
|
|
'outcome' => 'failed',
|
|
'context' => [
|
|
'verification_report' => json_decode(
|
|
(string) file_get_contents(base_path('specs/074-verification-checklist/contracts/examples/fail.json')),
|
|
true,
|
|
512,
|
|
JSON_THROW_ON_ERROR,
|
|
),
|
|
],
|
|
]);
|
|
|
|
expect(fn () => app(VerificationCheckAcknowledgementService::class)->acknowledge(
|
|
tenant: $tenant,
|
|
run: $run,
|
|
checkKey: 'provider_connection.token_acquisition',
|
|
ackReason: 'Known issue',
|
|
expiresAt: null,
|
|
actor: $user,
|
|
))->toThrow(AuthorizationException::class);
|
|
});
|
|
|
|
it('acknowledges a failing check (with optional expiry) and writes a minimal audit log (no ack_reason)', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'manager');
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'provider.connection.check',
|
|
'status' => 'completed',
|
|
'outcome' => 'failed',
|
|
'context' => [
|
|
'verification_report' => json_decode(
|
|
(string) file_get_contents(base_path('specs/074-verification-checklist/contracts/examples/fail.json')),
|
|
true,
|
|
512,
|
|
JSON_THROW_ON_ERROR,
|
|
),
|
|
],
|
|
]);
|
|
|
|
$reportBefore = $run->context['verification_report'] ?? null;
|
|
$reportBefore = is_array($reportBefore) ? $reportBefore : [];
|
|
|
|
$summaryBefore = is_array($reportBefore['summary'] ?? null) ? $reportBefore['summary'] : [];
|
|
$countsBefore = is_array($summaryBefore['counts'] ?? null) ? $summaryBefore['counts'] : [];
|
|
|
|
$checksBefore = is_array($reportBefore['checks'] ?? null) ? $reportBefore['checks'] : [];
|
|
$checksBeforeByKey = collect($checksBefore)
|
|
->filter(fn ($check): bool => is_array($check) && is_string($check['key'] ?? null))
|
|
->mapWithKeys(fn (array $check): array => [
|
|
(string) $check['key'] => [
|
|
'status' => $check['status'] ?? null,
|
|
'blocking' => $check['blocking'] ?? null,
|
|
'severity' => $check['severity'] ?? null,
|
|
'reason_code' => $check['reason_code'] ?? null,
|
|
],
|
|
])
|
|
->all();
|
|
|
|
$fingerprintBefore = VerificationReportFingerprint::forReport($reportBefore);
|
|
|
|
$expiresAt = now()->addDay()->toISOString();
|
|
|
|
$ack = app(VerificationCheckAcknowledgementService::class)->acknowledge(
|
|
tenant: $tenant,
|
|
run: $run,
|
|
checkKey: 'provider_connection.token_acquisition',
|
|
ackReason: 'Known issue',
|
|
expiresAt: $expiresAt,
|
|
actor: $user,
|
|
);
|
|
|
|
expect($ack->operation_run_id)->toBe((int) $run->getKey());
|
|
expect($ack->check_key)->toBe('provider_connection.token_acquisition');
|
|
expect($ack->ack_reason)->toBe('Known issue');
|
|
expect($ack->expires_at)->not->toBeNull();
|
|
|
|
$run->refresh();
|
|
|
|
$reportAfter = $run->context['verification_report'] ?? null;
|
|
$reportAfter = is_array($reportAfter) ? $reportAfter : [];
|
|
|
|
$summaryAfter = is_array($reportAfter['summary'] ?? null) ? $reportAfter['summary'] : [];
|
|
$countsAfter = is_array($summaryAfter['counts'] ?? null) ? $summaryAfter['counts'] : [];
|
|
|
|
expect($summaryAfter['overall'] ?? null)->toBe($summaryBefore['overall'] ?? null);
|
|
expect($countsAfter)->toBe($countsBefore);
|
|
|
|
$checksAfter = is_array($reportAfter['checks'] ?? null) ? $reportAfter['checks'] : [];
|
|
$checksAfterByKey = collect($checksAfter)
|
|
->filter(fn ($check): bool => is_array($check) && is_string($check['key'] ?? null))
|
|
->mapWithKeys(fn (array $check): array => [
|
|
(string) $check['key'] => [
|
|
'status' => $check['status'] ?? null,
|
|
'blocking' => $check['blocking'] ?? null,
|
|
'severity' => $check['severity'] ?? null,
|
|
'reason_code' => $check['reason_code'] ?? null,
|
|
],
|
|
])
|
|
->all();
|
|
|
|
expect($checksAfterByKey)->toBe($checksBeforeByKey);
|
|
|
|
$fingerprintAfter = VerificationReportFingerprint::forReport($reportAfter);
|
|
expect($fingerprintAfter)->toBe($fingerprintBefore);
|
|
|
|
$audit = AuditLog::query()
|
|
->where('workspace_id', (int) $tenant->workspace_id)
|
|
->where('action', AuditActionId::VerificationCheckAcknowledged->value)
|
|
->latest('id')
|
|
->first();
|
|
|
|
expect($audit)->not->toBeNull();
|
|
expect($audit?->metadata)->toMatchArray([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'operation_run_id' => (int) $run->getKey(),
|
|
'report_id' => (int) $run->getKey(),
|
|
'flow' => 'provider.connection.check',
|
|
'check_key' => 'provider_connection.token_acquisition',
|
|
'reason_code' => 'authentication_failed',
|
|
]);
|
|
|
|
$metadata = $audit?->metadata ?? [];
|
|
|
|
expect($metadata)->not->toHaveKey('ack_reason');
|
|
expect(json_encode($metadata))->not->toContain('Known issue');
|
|
});
|