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'] ?? ''); } }