create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'manager', ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); return [$workspace, $user]; } it('allows workspace managers to save and reset workspace slice settings', function (): void { [$workspace, $user] = workspaceManagerUser(); $this->actingAs($user) ->get(WorkspaceSettings::getUrl(panel: 'admin')) ->assertSuccessful(); $component = Livewire::actingAs($user) ->test(WorkspaceSettings::class) ->assertSet('data.backup_retention_keep_last_default', null) ->assertSet('data.backup_retention_min_floor', null) ->assertSet('data.drift_severity_mapping', []) ->assertSet('data.findings_sla_critical', null) ->assertSet('data.findings_sla_high', null) ->assertSet('data.findings_sla_medium', null) ->assertSet('data.findings_sla_low', null) ->assertSet('data.operations_operation_run_retention_days', null) ->assertSet('data.operations_stuck_run_threshold_minutes', null) ->set('data.backup_retention_keep_last_default', 55) ->set('data.backup_retention_min_floor', 12) ->set('data.drift_severity_mapping', ['drift' => 'critical']) ->set('data.findings_sla_critical', 2) ->set('data.findings_sla_high', 5) ->set('data.findings_sla_medium', 10) ->set('data.findings_sla_low', 20) ->set('data.operations_operation_run_retention_days', 120) ->set('data.operations_stuck_run_threshold_minutes', 60) ->callAction('save') ->assertHasNoErrors() ->assertSet('data.backup_retention_keep_last_default', 55) ->assertSet('data.backup_retention_min_floor', 12) ->assertSet('data.findings_sla_critical', 2) ->assertSet('data.findings_sla_high', 5) ->assertSet('data.findings_sla_medium', 10) ->assertSet('data.findings_sla_low', 20) ->assertSet('data.operations_operation_run_retention_days', 120) ->assertSet('data.operations_stuck_run_threshold_minutes', 60); expect(WorkspaceSetting::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('domain', 'backup') ->where('key', 'retention_keep_last_default') ->exists())->toBeTrue(); expect(app(SettingsResolver::class)->resolveValue($workspace, 'backup', 'retention_keep_last_default')) ->toBe(55); expect(app(SettingsResolver::class)->resolveValue($workspace, 'backup', 'retention_min_floor')) ->toBe(12); expect(app(SettingsResolver::class)->resolveValue($workspace, 'drift', 'severity_mapping')) ->toBe(['drift' => 'critical']); expect(app(SettingsResolver::class)->resolveValue($workspace, 'findings', 'sla_days')) ->toBe([ 'critical' => 2, 'high' => 5, 'medium' => 10, 'low' => 20, ]); expect(app(SettingsResolver::class)->resolveValue($workspace, 'operations', 'operation_run_retention_days')) ->toBe(120); expect(app(SettingsResolver::class)->resolveValue($workspace, 'operations', 'stuck_run_threshold_minutes')) ->toBe(60); $component ->set('data.backup_retention_keep_last_default', '') ->callAction('save') ->assertHasNoErrors() ->assertSet('data.backup_retention_keep_last_default', null); expect(WorkspaceSetting::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('domain', 'backup') ->where('key', 'retention_keep_last_default') ->exists())->toBeFalse(); $component ->mountFormComponentAction('operations_operation_run_retention_days', 'reset_operations_operation_run_retention_days', [], 'content') ->callMountedFormComponentAction() ->assertHasNoErrors() ->assertSet('data.operations_operation_run_retention_days', null); expect(WorkspaceSetting::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('domain', 'operations') ->where('key', 'operation_run_retention_days') ->exists())->toBeFalse(); }); it('treats an empty KeyValue row as unset and still allows saving other fields', function (): void { [$workspace, $user] = workspaceManagerUser(); $this->actingAs($user) ->get(WorkspaceSettings::getUrl(panel: 'admin')) ->assertSuccessful(); Livewire::actingAs($user) ->test(WorkspaceSettings::class) ->set('data.drift_severity_mapping', ['' => '']) ->set('data.backup_retention_keep_last_default', 10) ->callAction('save') ->assertHasNoErrors(); expect(app(SettingsResolver::class)->resolveValue($workspace, 'drift', 'severity_mapping')) ->toBe([]); expect(WorkspaceSetting::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('domain', 'drift') ->where('key', 'severity_mapping') ->exists())->toBeFalse(); expect(app(SettingsResolver::class)->resolveValue($workspace, 'backup', 'retention_keep_last_default')) ->toBe(10); }); it('accepts Filament KeyValue row-shaped state when saving drift severity mapping', function (): void { [$workspace, $user] = workspaceManagerUser(); $this->actingAs($user) ->get(WorkspaceSettings::getUrl(panel: 'admin')) ->assertSuccessful(); Livewire::actingAs($user) ->test(WorkspaceSettings::class) ->set('data.drift_severity_mapping', [ ['key' => 'drift', 'value' => 'critical'], ['key' => '', 'value' => ''], ]) ->callAction('save') ->assertHasNoErrors(); expect(app(SettingsResolver::class)->resolveValue($workspace, 'drift', 'severity_mapping')) ->toBe(['drift' => 'critical']); }); it('clearing a KeyValue mapping via an empty row resets the existing override', function (): void { [$workspace, $user] = workspaceManagerUser(); WorkspaceSetting::query()->create([ 'workspace_id' => (int) $workspace->getKey(), 'domain' => 'drift', 'key' => 'severity_mapping', 'value' => ['drift' => 'low'], 'updated_by_user_id' => (int) $user->getKey(), ]); Livewire::actingAs($user) ->test(WorkspaceSettings::class) ->assertSet('data.drift_severity_mapping', ['drift' => 'low']) ->set('data.drift_severity_mapping', ['' => '']) ->callAction('save') ->assertHasNoErrors(); expect(app(SettingsResolver::class)->resolveValue($workspace, 'drift', 'severity_mapping')) ->toBe([]); expect(WorkspaceSetting::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('domain', 'drift') ->where('key', 'severity_mapping') ->exists())->toBeFalse(); }); it('rejects unknown setting keys and does not persist or audit changes', function (): void { [$workspace, $user] = workspaceManagerUser(); $writer = app(SettingsWriter::class); expect(fn () => $writer->updateWorkspaceSetting($user, $workspace, 'backup', 'unknown_setting_key', 25)) ->toThrow(ValidationException::class); expect(WorkspaceSetting::query()->count())->toBe(0); expect(AuditLog::query()->count())->toBe(0); }); it('rejects invalid backup settings bounds and does not persist or audit changes', function (): void { [$workspace, $user] = workspaceManagerUser(); $writer = app(SettingsWriter::class); expect(fn () => $writer->updateWorkspaceSetting($user, $workspace, 'backup', 'retention_keep_last_default', 'not-an-integer')) ->toThrow(ValidationException::class); expect(fn () => $writer->updateWorkspaceSetting($user, $workspace, 'backup', 'retention_keep_last_default', 0)) ->toThrow(ValidationException::class); expect(fn () => $writer->updateWorkspaceSetting($user, $workspace, 'backup', 'retention_keep_last_default', 366)) ->toThrow(ValidationException::class); expect(fn () => $writer->updateWorkspaceSetting($user, $workspace, 'backup', 'retention_min_floor', 0)) ->toThrow(ValidationException::class); expect(fn () => $writer->updateWorkspaceSetting($user, $workspace, 'backup', 'retention_min_floor', 366)) ->toThrow(ValidationException::class); expect(WorkspaceSetting::query()->count())->toBe(0); expect(AuditLog::query()->count())->toBe(0); }); it('rejects malformed drift severity mapping values on save', function (): void { [$workspace, $user] = workspaceManagerUser(); $this->actingAs($user) ->get(WorkspaceSettings::getUrl(panel: 'admin')) ->assertSuccessful(); Livewire::actingAs($user) ->test(WorkspaceSettings::class) ->set('data.drift_severity_mapping', ['drift' => 'urgent']) ->callAction('save') ->assertHasErrors(['data.drift_severity_mapping']); expect(WorkspaceSetting::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('domain', 'drift') ->where('key', 'severity_mapping') ->exists())->toBeFalse(); }); it('saves partial findings sla days without auto-filling unset severities', function (): void { [$workspace, $user] = workspaceManagerUser(); $this->actingAs($user) ->get(WorkspaceSettings::getUrl(panel: 'admin')) ->assertSuccessful(); Livewire::actingAs($user) ->test(WorkspaceSettings::class) ->set('data.findings_sla_critical', 2) ->callAction('save') ->assertHasNoErrors() ->assertSet('data.findings_sla_critical', 2) ->assertSet('data.findings_sla_high', null) ->assertSet('data.findings_sla_medium', null) ->assertSet('data.findings_sla_low', null); $stored = WorkspaceSetting::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('domain', 'findings') ->where('key', 'sla_days') ->first(); expect($stored)->not->toBeNull(); $storedValue = $stored->getAttribute('value'); expect($storedValue)->toBe(['critical' => 2]); expect(app(SettingsResolver::class)->resolveValue($workspace, 'findings', 'sla_days')) ->toBe([ 'critical' => 2, 'high' => 7, 'medium' => 14, 'low' => 30, ]); }); it('rejects invalid drift severity mapping shape and values', function (): void { [$workspace, $user] = workspaceManagerUser(); $writer = app(SettingsWriter::class); expect(fn () => $writer->updateWorkspaceSetting( actor: $user, workspace: $workspace, domain: 'drift', key: 'severity_mapping', value: [123 => 'low'], ))->toThrow(ValidationException::class); expect(fn () => $writer->updateWorkspaceSetting( actor: $user, workspace: $workspace, domain: 'drift', key: 'severity_mapping', value: ['drift' => 'urgent'], ))->toThrow(ValidationException::class); $writer->updateWorkspaceSetting( actor: $user, workspace: $workspace, domain: 'drift', key: 'severity_mapping', value: ['drift' => 'CRITICAL'], ); expect(app(SettingsResolver::class)->resolveValue($workspace, 'drift', 'severity_mapping')) ->toBe(['drift' => 'critical']); }); it('saves and resets operations settings keys', function (): void { [$workspace, $user] = workspaceManagerUser(); $this->actingAs($user) ->get(WorkspaceSettings::getUrl(panel: 'admin')) ->assertSuccessful(); Livewire::actingAs($user) ->test(WorkspaceSettings::class) ->set('data.operations_operation_run_retention_days', 365) ->set('data.operations_stuck_run_threshold_minutes', 45) ->callAction('save') ->assertHasNoErrors() ->assertSet('data.operations_operation_run_retention_days', 365) ->assertSet('data.operations_stuck_run_threshold_minutes', 45) ->mountFormComponentAction('operations_stuck_run_threshold_minutes', 'reset_operations_stuck_run_threshold_minutes', [], 'content') ->callMountedFormComponentAction() ->assertHasNoErrors() ->assertSet('data.operations_stuck_run_threshold_minutes', null); expect(app(SettingsResolver::class)->resolveValue($workspace, 'operations', 'operation_run_retention_days')) ->toBe(365); expect(app(SettingsResolver::class)->resolveValue($workspace, 'operations', 'stuck_run_threshold_minutes')) ->toBe(0); }); it('requires confirmation for each per-setting reset action', function (): void { [$workspace, $user] = workspaceManagerUser(); WorkspaceSetting::query()->create([ 'workspace_id' => (int) $workspace->getKey(), 'domain' => 'backup', 'key' => 'retention_keep_last_default', 'value' => 40, 'updated_by_user_id' => (int) $user->getKey(), ]); WorkspaceSetting::query()->create([ 'workspace_id' => (int) $workspace->getKey(), 'domain' => 'backup', 'key' => 'retention_min_floor', 'value' => 5, 'updated_by_user_id' => (int) $user->getKey(), ]); WorkspaceSetting::query()->create([ 'workspace_id' => (int) $workspace->getKey(), 'domain' => 'drift', 'key' => 'severity_mapping', 'value' => ['drift' => 'low'], 'updated_by_user_id' => (int) $user->getKey(), ]); WorkspaceSetting::query()->create([ 'workspace_id' => (int) $workspace->getKey(), 'domain' => 'operations', 'key' => 'operation_run_retention_days', 'value' => 120, 'updated_by_user_id' => (int) $user->getKey(), ]); WorkspaceSetting::query()->create([ 'workspace_id' => (int) $workspace->getKey(), 'domain' => 'operations', 'key' => 'stuck_run_threshold_minutes', 'value' => 30, 'updated_by_user_id' => (int) $user->getKey(), ]); WorkspaceSetting::query()->create([ 'workspace_id' => (int) $workspace->getKey(), 'domain' => 'findings', 'key' => 'sla_days', 'value' => [ 'critical' => 3, 'high' => 7, 'medium' => 14, 'low' => 30, ], 'updated_by_user_id' => (int) $user->getKey(), ]); $component = Livewire::actingAs($user)->test(WorkspaceSettings::class); $component ->mountFormComponentAction('backup_retention_keep_last_default', 'reset_backup_retention_keep_last_default', [], 'content'); expect($component->instance()->getMountedAction()?->isConfirmationRequired())->toBeTrue(); $component->unmountFormComponentAction(); $component ->mountFormComponentAction('backup_retention_min_floor', 'reset_backup_retention_min_floor', [], 'content'); expect($component->instance()->getMountedAction()?->isConfirmationRequired())->toBeTrue(); $component->unmountFormComponentAction(); $component ->mountFormComponentAction('drift_severity_mapping', 'reset_drift_severity_mapping', [], 'content'); expect($component->instance()->getMountedAction()?->isConfirmationRequired())->toBeTrue(); $component->unmountFormComponentAction(); $component ->mountAction(TestAction::make('reset_findings_sla_days')->schemaComponent('findings_section')); expect($component->instance()->getMountedAction()?->isConfirmationRequired())->toBeTrue(); $component->unmountAction(); $component ->mountFormComponentAction('operations_operation_run_retention_days', 'reset_operations_operation_run_retention_days', [], 'content'); expect($component->instance()->getMountedAction()?->isConfirmationRequired())->toBeTrue(); $component->unmountFormComponentAction(); $component ->mountFormComponentAction('operations_stuck_run_threshold_minutes', 'reset_operations_stuck_run_threshold_minutes', [], 'content'); expect($component->instance()->getMountedAction()?->isConfirmationRequired())->toBeTrue(); $component->unmountFormComponentAction(); }); it('emits one audit entry per key changed when saving multiple settings at once', function (): void { [$workspace, $user] = workspaceManagerUser(); $this->actingAs($user) ->get(WorkspaceSettings::getUrl(panel: 'admin')) ->assertSuccessful(); Livewire::actingAs($user) ->test(WorkspaceSettings::class) ->set('data.backup_retention_keep_last_default', 50) ->set('data.backup_retention_min_floor', 10) ->set('data.operations_operation_run_retention_days', 120) ->callAction('save') ->assertHasNoErrors(); $updatedEvents = AuditLog::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('action', AuditActionId::WorkspaceSettingUpdated->value) ->get(); expect($updatedEvents)->toHaveCount(3); $keys = $updatedEvents ->map(fn (AuditLog $auditLog): ?string => data_get($auditLog->metadata, 'key')) ->filter(fn (?string $key): bool => is_string($key)) ->values() ->all(); expect($keys)->toEqualCanonicalizing([ 'retention_keep_last_default', 'retention_min_floor', 'operation_run_retention_days', ]); });