TenantAtlas/tests/Unit/Audit/AuditRecorderTest.php
ahmido 28cfe38ba4 feat: lay audit log foundation (#163)
## Summary
- turn the Monitoring audit log placeholder into a real workspace-scoped audit review surface
- introduce a shared audit recorder, richer audit value objects, and additive audit log schema evolution
- add audit outcome and actor badges, permission-aware related navigation, and durable audit retention coverage

## Included
- canonical `/admin/audit-log` list and detail inspection UI
- audit model helpers, taxonomy expansion, actor/target snapshots, and recorder/builder services
- operation terminal audit writes and purge command retention changes
- spec 134 design artifacts and focused Pest coverage for audit foundation behavior

## Validation
- `vendor/bin/sail bin pint --dirty --format agent`
- `vendor/bin/sail artisan test --compact tests/Unit/Audit tests/Unit/Badges/AuditBadgesTest.php tests/Feature/Filament/AuditLogPageTest.php tests/Feature/Filament/AuditLogDetailInspectionTest.php tests/Feature/Filament/AuditLogAuthorizationTest.php tests/Feature/Monitoring/AuditCoverageGovernanceTest.php tests/Feature/Monitoring/AuditCoverageOperationsTest.php tests/Feature/Console/TenantpilotPurgeNonPersistentDataTest.php`

## Notes
- Livewire v4.0+ compliance is preserved within the existing Filament v5 application.
- No provider registration changes were needed; panel provider registration remains in `bootstrap/providers.php`.
- No new globally searchable resource was introduced.
- The audit page remains read-only; no destructive actions were added.
- No new asset pipeline changes were introduced; existing deploy-time `php artisan filament:assets` behavior remains unchanged.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #163
2026-03-11 09:39:37 +00:00

80 lines
3.0 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\OperationRun;
use App\Services\Audit\AuditRecorder;
use App\Support\Audit\AuditActorSnapshot;
use App\Support\Audit\AuditActorType;
use App\Support\Audit\AuditOutcome;
use App\Support\Audit\AuditTargetSnapshot;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('records workspace and tenant audit rows through the shared recorder', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$run = OperationRun::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'user_id' => (int) $user->getKey(),
'type' => 'backup_create',
]);
$audit = app(AuditRecorder::class)->record(
action: 'backup.created',
context: [
'item_count' => 3,
'client_secret' => 'super-secret',
],
workspace: $tenant->workspace,
tenant: $tenant,
actor: AuditActorSnapshot::human($user),
target: new AuditTargetSnapshot(
type: 'backup_set',
id: '42',
label: 'Nightly backup',
),
outcome: 'completed',
summary: null,
operationRunId: (int) $run->getKey(),
)->refresh();
expect((int) $audit->workspace_id)->toBe((int) $tenant->workspace_id)
->and((int) $audit->tenant_id)->toBe((int) $tenant->getKey())
->and($audit->actorSnapshot()->type)->toBe(AuditActorType::Human)
->and($audit->actorDisplayLabel())->toBe($user->name)
->and($audit->resource_type)->toBe('backup_set')
->and((string) $audit->resource_id)->toBe('42')
->and($audit->targetDisplayLabel())->toBe('Nightly backup')
->and($audit->summaryText())->toBe('Backup set created for Nightly backup')
->and($audit->normalizedOutcome())->toBe(AuditOutcome::Success)
->and((int) $audit->operation_run_id)->toBe((int) $run->getKey())
->and(data_get($audit->metadata, 'client_secret'))->toBe('[REDACTED]')
->and(data_get($audit->metadata, 'item_count'))->toBe(3);
});
it('infers actor kind, target identity, and normalized outcome when snapshots are omitted', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$audit = app(AuditRecorder::class)->record(
action: 'backup_schedule.run_failed',
context: [
'_actor_type' => 'scheduled',
'backup_schedule_id' => 123,
'status' => 'failed',
'token' => 'top-secret',
],
workspace: $tenant->workspace,
tenant: $tenant,
)->refresh();
expect($audit->actorSnapshot()->type)->toBe(AuditActorType::Scheduled)
->and($audit->resource_type)->toBe('backup_schedule')
->and((string) $audit->resource_id)->toBe('123')
->and($audit->targetDisplayLabel())->toBe('Backup schedule #123')
->and($audit->normalizedOutcome())->toBe(AuditOutcome::Failed)
->and(data_get($audit->metadata, 'token'))->toBe('[REDACTED]');
});