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', 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.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.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, '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('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 JSON 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', '{invalid-json}') ->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('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(), ]); $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 ->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', ]); });