test: add PHPUnit coverage for 7 critical Tier 1 services (GR-TEST-001)
199 new tests across 9 files covering auth, user lifecycle, and settings:
- ApiTokenService: create/validate/rotate/revoke, timing-safe hash, tenant scope
- ApiTokenEndpointService: pagination, scope filtering, tenant resolution, expiry
- SsoUserLinkService: 2-phase identity lookup, provisioning, profile+avatar sync
- UserAssignmentService: sync methods, assignable-role freeze, transactions
- UserLifecycleAuditService: encrypted snapshots, enum normalization, retention
- UserLifecycleRestoreService: full restore flow, 9 error exits, rollback
- AdminSettingsService: API/lifecycle, Microsoft/telemetry, color/SMTP validation
Workflow: TEST-TIER1-001 (Analyst → Planner → Executor → Code Review → Security Review → Acceptance → Finalizer — all pass)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 23:29:07 +01:00
|
|
|
<?php
|
|
|
|
|
|
refactor(audit): extract audit domain into self-contained module
Move the entire audit subsystem (system audit, API audit, import audit,
user lifecycle audit, frontend telemetry) from core into modules/audit/.
Core decoupling via interface-based injection:
- AuditRecorderInterface replaces SystemAuditService in 10+ core services
- UserLifecycleAuditInterface / ImportAuditInterface for specialized flows
- NullAuditRecorder fallback when audit module is disabled
- ApiBootstrap/ApiResponse use null-safe callable resolvers
Module structure (modules/audit/):
- Manifest with routes, permissions, scheduler jobs, authorization policy
- 9 services, 8 repositories, 6 domain enums, 4 job handlers
- 33 page files, 4 JS files, 8 test files, migration scripts, i18n
Core cleanup:
- OperationsAuthorizationPolicy, UiCapabilityMap, PermissionService
surgically cleaned of audit-specific constants
- Sidebar template cleared of hardcoded audit navigation
- AuditModuleIsolationContractTest ensures no future core→module coupling
All quality gates pass: 1346 tests (19,276 assertions), PHPStan level 5
clean, architecture contracts verified.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:12:49 +01:00
|
|
|
namespace MintyPHP\Tests\Module\Audit\Service;
|
test: add PHPUnit coverage for 7 critical Tier 1 services (GR-TEST-001)
199 new tests across 9 files covering auth, user lifecycle, and settings:
- ApiTokenService: create/validate/rotate/revoke, timing-safe hash, tenant scope
- ApiTokenEndpointService: pagination, scope filtering, tenant resolution, expiry
- SsoUserLinkService: 2-phase identity lookup, provisioning, profile+avatar sync
- UserAssignmentService: sync methods, assignable-role freeze, transactions
- UserLifecycleAuditService: encrypted snapshots, enum normalization, retention
- UserLifecycleRestoreService: full restore flow, 9 error exits, rollback
- AdminSettingsService: API/lifecycle, Microsoft/telemetry, color/SMTP validation
Workflow: TEST-TIER1-001 (Analyst → Planner → Executor → Code Review → Security Review → Acceptance → Finalizer — all pass)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 23:29:07 +01:00
|
|
|
|
refactor(audit): extract audit domain into self-contained module
Move the entire audit subsystem (system audit, API audit, import audit,
user lifecycle audit, frontend telemetry) from core into modules/audit/.
Core decoupling via interface-based injection:
- AuditRecorderInterface replaces SystemAuditService in 10+ core services
- UserLifecycleAuditInterface / ImportAuditInterface for specialized flows
- NullAuditRecorder fallback when audit module is disabled
- ApiBootstrap/ApiResponse use null-safe callable resolvers
Module structure (modules/audit/):
- Manifest with routes, permissions, scheduler jobs, authorization policy
- 9 services, 8 repositories, 6 domain enums, 4 job handlers
- 33 page files, 4 JS files, 8 test files, migration scripts, i18n
Core cleanup:
- OperationsAuthorizationPolicy, UiCapabilityMap, PermissionService
surgically cleaned of audit-specific constants
- Sidebar template cleared of hardcoded audit navigation
- AuditModuleIsolationContractTest ensures no future core→module coupling
All quality gates pass: 1346 tests (19,276 assertions), PHPStan level 5
clean, architecture contracts verified.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:12:49 +01:00
|
|
|
use MintyPHP\Module\Audit\Repository\UserLifecycleAuditRepositoryInterface;
|
|
|
|
|
use MintyPHP\Module\Audit\Service\UserLifecycleAuditService;
|
test: add PHPUnit coverage for 7 critical Tier 1 services (GR-TEST-001)
199 new tests across 9 files covering auth, user lifecycle, and settings:
- ApiTokenService: create/validate/rotate/revoke, timing-safe hash, tenant scope
- ApiTokenEndpointService: pagination, scope filtering, tenant resolution, expiry
- SsoUserLinkService: 2-phase identity lookup, provisioning, profile+avatar sync
- UserAssignmentService: sync methods, assignable-role freeze, transactions
- UserLifecycleAuditService: encrypted snapshots, enum normalization, retention
- UserLifecycleRestoreService: full restore flow, 9 error exits, rollback
- AdminSettingsService: API/lifecycle, Microsoft/telemetry, color/SMTP validation
Workflow: TEST-TIER1-001 (Analyst → Planner → Executor → Code Review → Security Review → Acceptance → Finalizer — all pass)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 23:29:07 +01:00
|
|
|
use MintyPHP\Support\Crypto;
|
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
|
|
|
|
|
|
class UserLifecycleAuditServiceTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
private const SNAPSHOT_FIELDS = [
|
|
|
|
|
'uuid',
|
|
|
|
|
'first_name',
|
|
|
|
|
'last_name',
|
|
|
|
|
'display_name',
|
|
|
|
|
'email',
|
|
|
|
|
'profile_description',
|
|
|
|
|
'job_title',
|
|
|
|
|
'phone',
|
|
|
|
|
'mobile',
|
|
|
|
|
'short_dial',
|
|
|
|
|
'address',
|
|
|
|
|
'postal_code',
|
|
|
|
|
'city',
|
|
|
|
|
'region',
|
|
|
|
|
'country',
|
|
|
|
|
'hire_date',
|
|
|
|
|
'locale',
|
|
|
|
|
'theme',
|
|
|
|
|
'email_verified_at',
|
|
|
|
|
'last_login_at',
|
|
|
|
|
'last_login_provider',
|
|
|
|
|
'primary_tenant_id',
|
|
|
|
|
'current_tenant_id',
|
|
|
|
|
'created',
|
|
|
|
|
'modified',
|
|
|
|
|
'active_changed_at',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
private static bool $cryptoAvailable = false;
|
|
|
|
|
|
|
|
|
|
public static function setUpBeforeClass(): void
|
|
|
|
|
{
|
|
|
|
|
if (!defined('APP_CRYPTO_KEY')) {
|
|
|
|
|
define('APP_CRYPTO_KEY', str_repeat('ab', 32));
|
|
|
|
|
}
|
|
|
|
|
self::$cryptoAvailable = Crypto::isConfigured();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── logDeactivate ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public function testLogDeactivateCallsRepoCreateWithCorrectRow(): void
|
|
|
|
|
{
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->expects($this->once())
|
|
|
|
|
->method('create')
|
|
|
|
|
->with($this->callback(static function (array $row): bool {
|
|
|
|
|
return $row['action'] === 'deactivate'
|
|
|
|
|
&& $row['trigger_type'] === 'manual'
|
|
|
|
|
&& $row['status'] === 'success'
|
|
|
|
|
&& $row['run_uuid'] === 'run-1'
|
|
|
|
|
&& $row['target_user_id'] === 5
|
|
|
|
|
&& $row['target_user_uuid'] === 'user-uuid-5'
|
|
|
|
|
&& $row['target_user_email'] === 'test@example.com'
|
|
|
|
|
&& $row['actor_user_id'] === 99
|
|
|
|
|
&& $row['policy_deactivate_days'] === 30
|
|
|
|
|
&& $row['policy_delete_days'] === 60
|
|
|
|
|
&& $row['snapshot_enc'] === null
|
|
|
|
|
&& $row['snapshot_version'] === 1;
|
|
|
|
|
}))
|
|
|
|
|
->willReturn(1);
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$result = $service->logDeactivate(
|
|
|
|
|
'run-1',
|
|
|
|
|
'manual',
|
|
|
|
|
['deactivate_days' => 30, 'delete_days' => 60],
|
|
|
|
|
99,
|
|
|
|
|
['id' => 5, 'uuid' => 'user-uuid-5', 'email' => 'test@example.com'],
|
|
|
|
|
'success',
|
|
|
|
|
null
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertTrue($result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testLogDeactivateReturnsFalseOnRepoException(): void
|
|
|
|
|
{
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->method('create')->willThrowException(new \RuntimeException('db error'));
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$result = $service->logDeactivate('run-1', 'manual', [], null, []);
|
|
|
|
|
|
|
|
|
|
$this->assertFalse($result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testLogDeactivateReturnsFalseWhenRepoReturnsFalse(): void
|
|
|
|
|
{
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->method('create')->willReturn(false);
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$result = $service->logDeactivate('run-1', 'manual', [], null, []);
|
|
|
|
|
|
|
|
|
|
$this->assertFalse($result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── logDeleteWithSnapshot ───────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public function testLogDeleteWithSnapshotCreatesEncryptedSnapshot(): void
|
|
|
|
|
{
|
|
|
|
|
if (!self::$cryptoAvailable) {
|
|
|
|
|
$this->markTestSkipped('Crypto not configured');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$targetUser = [];
|
|
|
|
|
foreach (self::SNAPSHOT_FIELDS as $field) {
|
|
|
|
|
$targetUser[$field] = 'val_' . $field;
|
|
|
|
|
}
|
|
|
|
|
$targetUser['id'] = 5;
|
|
|
|
|
$targetUser['extra_field'] = 'should_be_excluded';
|
|
|
|
|
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->expects($this->once())
|
|
|
|
|
->method('create')
|
|
|
|
|
->with($this->callback(static function (array $row): bool {
|
|
|
|
|
return $row['action'] === 'delete'
|
|
|
|
|
&& $row['status'] === 'success'
|
|
|
|
|
&& $row['snapshot_version'] === 1
|
|
|
|
|
&& is_string($row['snapshot_enc'])
|
|
|
|
|
&& $row['snapshot_enc'] !== '';
|
|
|
|
|
}))
|
|
|
|
|
->willReturn(7);
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$result = $service->logDeleteWithSnapshot('run-2', 'cron', ['deactivate_days' => 30, 'delete_days' => 90], null, $targetUser);
|
|
|
|
|
|
|
|
|
|
$this->assertSame(7, $result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testLogDeleteWithSnapshotContainsOnlySnapshotFields(): void
|
|
|
|
|
{
|
|
|
|
|
if (!self::$cryptoAvailable) {
|
|
|
|
|
$this->markTestSkipped('Crypto not configured');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$targetUser = ['id' => 5, 'uuid' => 'u5', 'email' => 'e@example.com', 'extra' => 'no'];
|
|
|
|
|
|
|
|
|
|
$capturedRow = null;
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->expects($this->once())
|
|
|
|
|
->method('create')
|
|
|
|
|
->with($this->callback(static function (array $row) use (&$capturedRow): bool {
|
|
|
|
|
$capturedRow = $row;
|
|
|
|
|
return true;
|
|
|
|
|
}))
|
|
|
|
|
->willReturn(1);
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$service->logDeleteWithSnapshot('run-3', 'manual', [], null, $targetUser);
|
|
|
|
|
|
|
|
|
|
$this->assertNotNull($capturedRow);
|
|
|
|
|
$decryptedJson = Crypto::decryptString($capturedRow['snapshot_enc']);
|
|
|
|
|
$snapshot = json_decode($decryptedJson, true);
|
|
|
|
|
$this->assertIsArray($snapshot);
|
|
|
|
|
$this->assertSame(self::SNAPSHOT_FIELDS, array_keys($snapshot));
|
|
|
|
|
$this->assertArrayNotHasKey('extra', $snapshot);
|
|
|
|
|
$this->assertArrayNotHasKey('id', $snapshot);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testLogDeleteWithSnapshotReturnsFalseOnCryptoFailure(): void
|
|
|
|
|
{
|
|
|
|
|
// Simulate a scenario where Crypto throws by passing a target user that will
|
|
|
|
|
// cause the try/catch to fire. We cannot easily override static Crypto, but we
|
|
|
|
|
// know that if APP_CRYPTO_KEY were missing, encryptString would throw.
|
|
|
|
|
// Since we defined a valid key, let's test the repo exception path instead.
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->method('create')->willThrowException(new \RuntimeException('db error'));
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$result = $service->logDeleteWithSnapshot('run-4', 'manual', [], null, ['id' => 1, 'uuid' => 'u']);
|
|
|
|
|
|
|
|
|
|
$this->assertFalse($result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── logRestore ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public function testLogRestoreCallsRepoWithRestoreAction(): void
|
|
|
|
|
{
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->expects($this->once())
|
|
|
|
|
->method('create')
|
|
|
|
|
->with($this->callback(static function (array $row): bool {
|
|
|
|
|
return $row['action'] === 'restore'
|
|
|
|
|
&& $row['trigger_type'] === 'manual'
|
|
|
|
|
&& $row['status'] === 'success';
|
|
|
|
|
}))
|
|
|
|
|
->willReturn(10);
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$result = $service->logRestore('run-5', 'manual', [], 99, ['id' => 5, 'uuid' => 'u5', 'email' => 'e@x.com']);
|
|
|
|
|
|
|
|
|
|
$this->assertTrue($result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testLogRestoreReturnsFalseOnException(): void
|
|
|
|
|
{
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->method('create')->willThrowException(new \RuntimeException('fail'));
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$result = $service->logRestore('run-5', 'manual', [], null, []);
|
|
|
|
|
|
|
|
|
|
$this->assertFalse($result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── decryptSnapshot ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public function testDecryptSnapshotWithValidPayload(): void
|
|
|
|
|
{
|
|
|
|
|
if (!self::$cryptoAvailable) {
|
|
|
|
|
$this->markTestSkipped('Crypto not configured');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$original = ['uuid' => 'test-uuid', 'email' => 'test@example.com'];
|
|
|
|
|
$encrypted = Crypto::encryptString(json_encode($original));
|
|
|
|
|
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
|
|
|
|
|
$result = $service->decryptSnapshot(['snapshot_enc' => $encrypted]);
|
|
|
|
|
$this->assertSame($original, $result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDecryptSnapshotReturnsNullForEmptyPayload(): void
|
|
|
|
|
{
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
|
|
|
|
|
$this->assertNull($service->decryptSnapshot([]));
|
|
|
|
|
$this->assertNull($service->decryptSnapshot(['snapshot_enc' => '']));
|
|
|
|
|
$this->assertNull($service->decryptSnapshot(['snapshot_enc' => ' ']));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDecryptSnapshotReturnsNullForInvalidPayload(): void
|
|
|
|
|
{
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
|
|
|
|
|
$result = $service->decryptSnapshot(['snapshot_enc' => 'not-valid-encrypted-data']);
|
|
|
|
|
$this->assertNull($result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── markDeleteEventRestored ─────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public function testMarkDeleteEventRestoredDelegatesToRepo(): void
|
|
|
|
|
{
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->expects($this->once())
|
|
|
|
|
->method('markRestored')
|
|
|
|
|
->with(1, 99, 200)
|
|
|
|
|
->willReturn(true);
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$this->assertTrue($service->markDeleteEventRestored(1, 99, 200));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testMarkDeleteEventRestoredReturnsFalseOnException(): void
|
|
|
|
|
{
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->method('markRestored')->willThrowException(new \RuntimeException('fail'));
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$this->assertFalse($service->markDeleteEventRestored(1, 99, 200));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── purgeExpired ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public function testPurgeExpiredDelegatesToRepoWith365Days(): void
|
|
|
|
|
{
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->expects($this->once())
|
|
|
|
|
->method('purgeOlderThanDays')
|
|
|
|
|
->with(365)
|
|
|
|
|
->willReturn(5);
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$this->assertSame(5, $service->purgeExpired());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── buildBaseRow enum normalization ──────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public function testBuildBaseRowNormalizesInvalidTriggerTypeToDefault(): void
|
|
|
|
|
{
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->expects($this->once())
|
|
|
|
|
->method('create')
|
|
|
|
|
->with($this->callback(static function (array $row): bool {
|
|
|
|
|
return $row['trigger_type'] === 'system'; // fallback for invalid trigger
|
|
|
|
|
}))
|
|
|
|
|
->willReturn(1);
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$service->logDeactivate('run-x', 'INVALID_TRIGGER', [], null, []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testBuildBaseRowNormalizesInvalidStatusToFailed(): void
|
|
|
|
|
{
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->expects($this->once())
|
|
|
|
|
->method('create')
|
|
|
|
|
->with($this->callback(static function (array $row): bool {
|
|
|
|
|
return $row['status'] === 'failed'; // fallback for invalid status
|
|
|
|
|
}))
|
|
|
|
|
->willReturn(1);
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$service->logDeactivate('run-x', 'manual', [], null, [], 'INVALID_STATUS');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testBuildBaseRowNormalizesNullActorToNull(): void
|
|
|
|
|
{
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->expects($this->once())
|
|
|
|
|
->method('create')
|
|
|
|
|
->with($this->callback(static function (array $row): bool {
|
|
|
|
|
return $row['actor_user_id'] === null;
|
|
|
|
|
}))
|
|
|
|
|
->willReturn(1);
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$service->logDeactivate('run-x', 'manual', [], null, []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testBuildBaseRowNormalizesZeroActorToNull(): void
|
|
|
|
|
{
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->expects($this->once())
|
|
|
|
|
->method('create')
|
|
|
|
|
->with($this->callback(static function (array $row): bool {
|
|
|
|
|
return $row['actor_user_id'] === null;
|
|
|
|
|
}))
|
|
|
|
|
->willReturn(1);
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$service->logDeactivate('run-x', 'manual', [], 0, []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testBuildBaseRowSetsNegativePolicyDaysToZero(): void
|
|
|
|
|
{
|
|
|
|
|
$repo = $this->createMock(UserLifecycleAuditRepositoryInterface::class);
|
|
|
|
|
$repo->expects($this->once())
|
|
|
|
|
->method('create')
|
|
|
|
|
->with($this->callback(static function (array $row): bool {
|
|
|
|
|
return $row['policy_deactivate_days'] === 0
|
|
|
|
|
&& $row['policy_delete_days'] === 0;
|
|
|
|
|
}))
|
|
|
|
|
->willReturn(1);
|
|
|
|
|
|
|
|
|
|
$service = new UserLifecycleAuditService($repo);
|
|
|
|
|
$service->logDeactivate('run-x', 'manual', ['deactivate_days' => -10, 'delete_days' => -5], null, []);
|
|
|
|
|
}
|
|
|
|
|
}
|