1
0
Files
breadcrumb-the-shire/tests/Service/Import/ImportServiceTest.php
fs 0c351f6aff 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

217 lines
7.3 KiB
PHP

<?php
namespace MintyPHP\Tests\Service\Import;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Audit\ImportAuditInterface;
use MintyPHP\Service\Import\CsvReaderService;
use MintyPHP\Service\Import\ImportService;
use MintyPHP\Service\Import\ImportStateStoreService;
use MintyPHP\Service\Import\ImportTempFileService;
use MintyPHP\Service\Import\Profile\ImportProfileInterface;
use MintyPHP\Service\Settings\SettingsDefaultsGateway;
use MintyPHP\Service\Tenant\TenantScopeService;
use PHPUnit\Framework\TestCase;
class ImportServiceTest extends TestCase
{
public function testAnalyzeUploadBuildsTokenStateAndAutoMapping(): void
{
$profile = new ImportServiceTestProfile('users', false);
$csvReader = $this->createMock(CsvReaderService::class);
$csvReader->expects($this->once())
->method('analyze')
->willReturn([
'ok' => true,
'delimiter' => ';',
'headers' => ['E-Mail'],
'rows_total' => 2,
]);
$tempFileService = $this->createMock(ImportTempFileService::class);
$tempFileService->expects($this->once())
->method('storeUploadedFile')
->willReturn(['ok' => true, 'path' => '/tmp/import.csv']);
$stateStore = $this->createMock(ImportStateStoreService::class);
$stateStore->expects($this->once())
->method('setState')
->with(
$this->isString(),
$this->callback(function (array $state): bool {
return (string) ($state['path'] ?? '') === '/tmp/import.csv'
&& (string) ($state['type'] ?? '') === 'users'
&& is_int($state['created_at'] ?? null);
})
);
$service = $this->newImportService($profile, $csvReader, $tempFileService, $stateStore);
$result = $service->analyzeUpload([
'tmp_name' => '/tmp/php-upload',
'name' => 'users.csv',
'size' => 10,
'error' => UPLOAD_ERR_OK,
], 'users', 7);
$this->assertTrue($result['ok']);
$this->assertSame(';', $result['delimiter']);
$this->assertSame(['E-Mail' => 'email'], $result['auto_mapping']);
}
public function testPreviewFailsWhenRequiredMappingIsMissing(): void
{
$profile = new ImportServiceTestProfile('users', false);
$stateStore = $this->createMock(ImportStateStoreService::class);
$stateStore->method('getState')->willReturn([
'token' => 'tok',
'type' => 'users',
'headers' => ['email'],
'path' => '/tmp/import.csv',
'delimiter' => ',',
'user_id' => 5,
'created_at' => time(),
]);
$service = $this->newImportService($profile, null, null, $stateStore);
$result = $service->preview('tok', ['mapping' => []], 5);
$this->assertFalse($result['ok']);
$this->assertSame('mapping_required_missing', $result['code']);
}
public function testPreviewFailsWhenAssignmentPermissionIsMissing(): void
{
$profile = new ImportServiceTestProfile('users', true);
$stateStore = $this->createMock(ImportStateStoreService::class);
$stateStore->method('getState')->willReturn([
'token' => 'tok',
'type' => 'users',
'headers' => ['email', 'tenant'],
'path' => '/tmp/import.csv',
'delimiter' => ',',
'user_id' => 0,
'created_at' => time(),
]);
$service = $this->newImportService($profile, null, null, $stateStore);
$result = $service->preview('tok', ['mapping' => ['email' => 'email', 'tenant' => 'tenant']], 0);
$this->assertFalse($result['ok']);
$this->assertSame('assignment_permission_required', $result['code']);
}
public function testMessageForCodeUsesKnownMappingAndFallback(): void
{
$service = $this->newImportService(new ImportServiceTestProfile('users', false));
$this->assertSame('Invalid file type', $service->messageForCode('invalid_file_type'));
$this->assertSame('Unexpected error during import', $service->messageForCode('something_unknown'));
}
private function newImportService(
ImportProfileInterface $profile,
?CsvReaderService $csvReader = null,
?ImportTempFileService $tempFileService = null,
?ImportStateStoreService $stateStore = null
): ImportService {
$csvReader = $csvReader ?? $this->createMock(CsvReaderService::class);
$tempFileService = $tempFileService ?? $this->createMock(ImportTempFileService::class);
$auditService = $this->createMock(ImportAuditInterface::class);
$stateStore = $stateStore ?? $this->createMock(ImportStateStoreService::class);
$permissionService = $this->createMock(PermissionService::class);
$permissionService->method('userHas')->willReturn(false);
$settingsDefaultsGateway = $this->createMock(SettingsDefaultsGateway::class);
$tenantScopeService = $this->createMock(TenantScopeService::class);
$tenantScopeService->method('hasGlobalAccess')->willReturn(false);
$tenantScopeService->method('getUserTenantIds')->willReturn([]);
$sessionStore = $this->createMock(SessionStoreInterface::class);
$sessionStore->method('get')->willReturn([]);
return new ImportService(
$csvReader,
$tempFileService,
$auditService,
$stateStore,
[$profile->key() => $profile],
$permissionService,
$settingsDefaultsGateway,
$tenantScopeService,
$sessionStore
);
}
}
class ImportServiceTestProfile implements ImportProfileInterface
{
public function __construct(private readonly string $key, private readonly bool $requiresAssignmentPermission)
{
}
public function key(): string
{
return $this->key;
}
public function label(): string
{
return 'Test';
}
public function allowedTargets(): array
{
return [
'email' => 'Email',
'tenant' => 'Tenant',
];
}
public function requiredTargets(): array
{
return ['email'];
}
public function autoMapAliases(): array
{
return [
'email' => ['e-mail', 'email'],
'tenant' => ['tenant'],
];
}
public function supportsAssignments(): bool
{
return true;
}
public function requiresAssignmentPermission(): bool
{
return $this->requiresAssignmentPermission;
}
public function validateMappedRow(array $mapped, array $context): array
{
return ['ok' => true, 'normalized' => $mapped, 'errors' => []];
}
public function dryRunRow(array $normalized, array $context): array
{
return ['status' => 'would_create'];
}
public function commitRow(array $normalized, array $context): array
{
return ['status' => 'created'];
}
public function inFileDuplicateKey(array $normalized): ?string
{
return null;
}
public function rowIdentifier(array $normalized): string
{
return (string) ($normalized['email'] ?? '');
}
}