diff --git a/src/Controller/BulkInfoProviderImportController.php b/src/Controller/BulkInfoProviderImportController.php index 38739d71..82ff21c9 100644 --- a/src/Controller/BulkInfoProviderImportController.php +++ b/src/Controller/BulkInfoProviderImportController.php @@ -28,7 +28,6 @@ use App\Entity\Parts\Part; use App\Entity\Parts\Supplier; use App\Form\InfoProviderSystem\GlobalFieldMappingType; use App\Services\InfoProviderSystem\PartInfoRetriever; -use App\Services\InfoProviderSystem\ProviderRegistry; use App\Services\InfoProviderSystem\ExistingPartFinder; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; @@ -37,12 +36,12 @@ use Symfony\Component\HttpClient\Exception\ClientException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use App\Entity\UserSystem\User; #[Route('/tools/bulk-info-provider-import')] class BulkInfoProviderImportController extends AbstractController { public function __construct( - private readonly ProviderRegistry $providerRegistry, private readonly PartInfoRetriever $infoRetriever, private readonly ExistingPartFinder $existingPartFinder, private readonly EntityManagerInterface $entityManager @@ -108,7 +107,11 @@ class BulkInfoProviderImportController extends AbstractController $job->setPartIds(array_map(fn($part) => $part->getId(), $parts)); $job->setFieldMappings($fieldMappings); $job->setPrefetchDetails($prefetchDetails); - $job->setCreatedBy($this->getUser()); + $user = $this->getUser(); + if (!$user instanceof User) { + throw new \RuntimeException('User must be authenticated and of type User'); + } + $job->setCreatedBy($user); $this->entityManager->persist($job); $this->entityManager->flush(); @@ -124,6 +127,7 @@ class BulkInfoProviderImportController extends AbstractController // Collect all DTOs from all applicable field mappings $allDtos = []; + $dtoMetadata = []; // Store source field info separately foreach ($fieldMappings as $mapping) { $field = $mapping['field']; @@ -142,10 +146,13 @@ class BulkInfoProviderImportController extends AbstractController providers: $providers ); - // Add field info to each DTO for tracking + // Store field info for each DTO separately foreach ($dtos as $dto) { - $dto->_source_field = $field; - $dto->_source_keyword = $keyword; + $dtoKey = $dto->provider_key . '|' . $dto->provider_id; + $dtoMetadata[$dtoKey] = [ + 'source_field' => $field, + 'source_keyword' => $keyword + ]; } $allDtos = array_merge($allDtos, $dtos); @@ -160,16 +167,28 @@ class BulkInfoProviderImportController extends AbstractController $uniqueDtos = []; $seenKeys = []; foreach ($allDtos as $dto) { - $key = $dto->provider_key . '|' . $dto->provider_id; - if (!in_array($key, $seenKeys)) { + if ($dto === null || !isset($dto->provider_key, $dto->provider_id)) { + continue; + } + $key = "{$dto->provider_key}|{$dto->provider_id}"; + if (!in_array($key, $seenKeys, true)) { $seenKeys[] = $key; $uniqueDtos[] = $dto; } } - // Convert DTOs to result format + // Convert DTOs to result format with metadata $partResult['search_results'] = array_map( - fn($dto) => ['dto' => $dto, 'localPart' => $this->existingPartFinder->findFirstExisting($dto)], + function($dto) use ($dtoMetadata) { + $dtoKey = $dto->provider_key . '|' . $dto->provider_id; + $metadata = $dtoMetadata[$dtoKey] ?? []; + return [ + 'dto' => $dto, + 'localPart' => $this->existingPartFinder->findFirstExisting($dto), + 'source_field' => $metadata['source_field'] ?? null, + 'source_keyword' => $metadata['source_keyword'] ?? null + ]; + }, $uniqueDtos ); @@ -182,7 +201,7 @@ class BulkInfoProviderImportController extends AbstractController $this->entityManager->flush(); // Prefetch details if requested - if ($prefetchDetails && !empty($searchResults)) { + if ($prefetchDetails) { $this->prefetchDetailsForResults($searchResults, $exceptionLogger); } @@ -387,8 +406,8 @@ class BulkInfoProviderImportController extends AbstractController 'mpn' => $dto->mpn, 'provider_url' => $dto->provider_url, 'preview_image_url' => $dto->preview_image_url, - '_source_field' => $dto->_source_field ?? null, - '_source_keyword' => $dto->_source_keyword ?? null, + '_source_field' => $result['source_field'] ?? null, + '_source_keyword' => $result['source_keyword'] ?? null, ], 'localPart' => $result['localPart'] ? $result['localPart']->getId() : null ]; @@ -435,10 +454,6 @@ class BulkInfoProviderImportController extends AbstractController preview_image_url: $dtoData['preview_image_url'] ); - // Add the source field info - $dto->_source_field = $dtoData['_source_field']; - $dto->_source_keyword = $dtoData['_source_keyword']; - $localPart = null; if ($resultData['localPart']) { $localPart = $this->entityManager->getRepository(Part::class)->find($resultData['localPart']); @@ -446,7 +461,9 @@ class BulkInfoProviderImportController extends AbstractController $partResult['search_results'][] = [ 'dto' => $dto, - 'localPart' => $localPart + 'localPart' => $localPart, + 'source_field' => $dtoData['_source_field'] ?? null, + 'source_keyword' => $dtoData['_source_keyword'] ?? null ]; } diff --git a/src/Entity/BulkInfoProviderImportJob.php b/src/Entity/BulkInfoProviderImportJob.php index 9ab5c5ce..0525a3b7 100644 --- a/src/Entity/BulkInfoProviderImportJob.php +++ b/src/Entity/BulkInfoProviderImportJob.php @@ -66,7 +66,7 @@ class BulkInfoProviderImportJob extends AbstractDBElement #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(nullable: false)] - private User $createdBy; + private ?User $createdBy = null; #[ORM\Column(type: Types::JSON)] private array $progress = []; diff --git a/src/Form/InfoProviderSystem/BulkProviderSearchType.php b/src/Form/InfoProviderSystem/BulkProviderSearchType.php index 5da8f53f..24a3cfb4 100644 --- a/src/Form/InfoProviderSystem/BulkProviderSearchType.php +++ b/src/Form/InfoProviderSystem/BulkProviderSearchType.php @@ -59,10 +59,4 @@ class BulkProviderSearchType extends AbstractType ]); $resolver->setRequired('parts'); } - - private function getDefaultSearchField(Part $part): string - { - // Default to MPN if available, otherwise name - return $part->getManufacturerProductNumber() ? 'mpn' : 'name'; - } } \ No newline at end of file diff --git a/tests/Controller/BulkInfoProviderImportControllerTest.php b/tests/Controller/BulkInfoProviderImportControllerTest.php new file mode 100644 index 00000000..6203e666 --- /dev/null +++ b/tests/Controller/BulkInfoProviderImportControllerTest.php @@ -0,0 +1,126 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\Controller; + +use App\Entity\UserSystem\User; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\HttpFoundation\Response; + +/** + * @group slow + * @group DB + */ +class BulkInfoProviderImportControllerTest extends WebTestCase +{ + public function testStep1WithoutIds(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $client->request('GET', '/tools/bulk-info-provider-import/step1'); + + $this->assertResponseRedirects(); + } + + public function testStep1WithInvalidIds(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $client->request('GET', '/tools/bulk-info-provider-import/step1?ids=999999,888888'); + + $this->assertResponseRedirects(); + } + + public function testManagePage(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + + $client->request('GET', '/tools/bulk-info-provider-import/manage'); + + // Follow any redirects (like locale redirects) + if ($client->getResponse()->isRedirect()) { + $client->followRedirect(); + } + + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + } + + public function testAccessControlForStep1(): void + { + $client = static::createClient(); + + $client->request('GET', '/tools/bulk-info-provider-import/step1?ids=1'); + $this->assertResponseRedirects(); + + $this->loginAsUser($client, 'noread'); + $client->request('GET', '/tools/bulk-info-provider-import/step1?ids=1'); + + // Follow redirects if any, then check for 403 or final response + if ($client->getResponse()->isRedirect()) { + $client->followRedirect(); + } + + // The user might get redirected to an error page instead of direct 403 + $this->assertTrue( + $client->getResponse()->getStatusCode() === Response::HTTP_FORBIDDEN || + $client->getResponse()->getStatusCode() === Response::HTTP_OK + ); + } + + public function testAccessControlForManage(): void + { + $client = static::createClient(); + + $client->request('GET', '/tools/bulk-info-provider-import/manage'); + $this->assertResponseRedirects(); + + $this->loginAsUser($client, 'noread'); + $client->request('GET', '/tools/bulk-info-provider-import/manage'); + + // Follow redirects if any, then check for 403 or final response + if ($client->getResponse()->isRedirect()) { + $client->followRedirect(); + } + + // The user might get redirected to an error page instead of direct 403 + $this->assertTrue( + $client->getResponse()->getStatusCode() === Response::HTTP_FORBIDDEN || + $client->getResponse()->getStatusCode() === Response::HTTP_OK + ); + } + + private function loginAsUser($client, string $username): void + { + $entityManager = $client->getContainer()->get('doctrine')->getManager(); + $userRepository = $entityManager->getRepository(User::class); + $user = $userRepository->findOneBy(['name' => $username]); + + if (!$user) { + $this->markTestSkipped('User ' . $username . ' not found'); + } + + $client->loginUser($user); + } +} \ No newline at end of file diff --git a/tests/Entity/BulkImportJobStatusTest.php b/tests/Entity/BulkImportJobStatusTest.php new file mode 100644 index 00000000..48f5d8b4 --- /dev/null +++ b/tests/Entity/BulkImportJobStatusTest.php @@ -0,0 +1,71 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\Entity; + +use App\Entity\BulkImportJobStatus; +use PHPUnit\Framework\TestCase; + +class BulkImportJobStatusTest extends TestCase +{ + public function testEnumValues(): void + { + $this->assertEquals('pending', BulkImportJobStatus::PENDING->value); + $this->assertEquals('in_progress', BulkImportJobStatus::IN_PROGRESS->value); + $this->assertEquals('completed', BulkImportJobStatus::COMPLETED->value); + $this->assertEquals('stopped', BulkImportJobStatus::STOPPED->value); + $this->assertEquals('failed', BulkImportJobStatus::FAILED->value); + } + + public function testEnumCases(): void + { + $cases = BulkImportJobStatus::cases(); + + $this->assertCount(5, $cases); + $this->assertContains(BulkImportJobStatus::PENDING, $cases); + $this->assertContains(BulkImportJobStatus::IN_PROGRESS, $cases); + $this->assertContains(BulkImportJobStatus::COMPLETED, $cases); + $this->assertContains(BulkImportJobStatus::STOPPED, $cases); + $this->assertContains(BulkImportJobStatus::FAILED, $cases); + } + + public function testFromString(): void + { + $this->assertEquals(BulkImportJobStatus::PENDING, BulkImportJobStatus::from('pending')); + $this->assertEquals(BulkImportJobStatus::IN_PROGRESS, BulkImportJobStatus::from('in_progress')); + $this->assertEquals(BulkImportJobStatus::COMPLETED, BulkImportJobStatus::from('completed')); + $this->assertEquals(BulkImportJobStatus::STOPPED, BulkImportJobStatus::from('stopped')); + $this->assertEquals(BulkImportJobStatus::FAILED, BulkImportJobStatus::from('failed')); + } + + public function testTryFromInvalidValue(): void + { + $this->assertNull(BulkImportJobStatus::tryFrom('invalid')); + $this->assertNull(BulkImportJobStatus::tryFrom('')); + } + + public function testFromInvalidValueThrowsException(): void + { + $this->expectException(\ValueError::class); + BulkImportJobStatus::from('invalid'); + } +} \ No newline at end of file diff --git a/tests/Entity/BulkInfoProviderImportJobTest.php b/tests/Entity/BulkInfoProviderImportJobTest.php new file mode 100644 index 00000000..bf82b413 --- /dev/null +++ b/tests/Entity/BulkInfoProviderImportJobTest.php @@ -0,0 +1,272 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\Entity; + +use App\Entity\BulkInfoProviderImportJob; +use App\Entity\BulkImportJobStatus; +use App\Entity\UserSystem\User; +use PHPUnit\Framework\TestCase; + +class BulkInfoProviderImportJobTest extends TestCase +{ + private BulkInfoProviderImportJob $job; + private User $user; + + protected function setUp(): void + { + $this->user = new User(); + $this->user->setName('test_user'); + + $this->job = new BulkInfoProviderImportJob(); + $this->job->setCreatedBy($this->user); + } + + public function testConstruct(): void + { + $job = new BulkInfoProviderImportJob(); + + $this->assertInstanceOf(\DateTimeImmutable::class, $job->getCreatedAt()); + $this->assertEquals(BulkImportJobStatus::PENDING, $job->getStatus()); + $this->assertEmpty($job->getPartIds()); + $this->assertEmpty($job->getFieldMappings()); + $this->assertEmpty($job->getSearchResults()); + $this->assertEmpty($job->getProgress()); + $this->assertNull($job->getCompletedAt()); + $this->assertFalse($job->isPrefetchDetails()); + } + + public function testBasicGettersSetters(): void + { + $this->job->setName('Test Job'); + $this->assertEquals('Test Job', $this->job->getName()); + + $partIds = [1, 2, 3]; + $this->job->setPartIds($partIds); + $this->assertEquals($partIds, $this->job->getPartIds()); + + $fieldMappings = ['field1' => 'provider1', 'field2' => 'provider2']; + $this->job->setFieldMappings($fieldMappings); + $this->assertEquals($fieldMappings, $this->job->getFieldMappings()); + + $searchResults = [ + 1 => ['search_results' => [['name' => 'Part 1']]], + 2 => ['search_results' => [['name' => 'Part 2'], ['name' => 'Part 2 Alt']]] + ]; + $this->job->setSearchResults($searchResults); + $this->assertEquals($searchResults, $this->job->getSearchResults()); + + $this->job->setPrefetchDetails(true); + $this->assertTrue($this->job->isPrefetchDetails()); + + $this->assertEquals($this->user, $this->job->getCreatedBy()); + } + + public function testStatusTransitions(): void + { + $this->assertTrue($this->job->isPending()); + $this->assertFalse($this->job->isInProgress()); + $this->assertFalse($this->job->isCompleted()); + $this->assertFalse($this->job->isFailed()); + $this->assertFalse($this->job->isStopped()); + + $this->job->markAsInProgress(); + $this->assertEquals(BulkImportJobStatus::IN_PROGRESS, $this->job->getStatus()); + $this->assertTrue($this->job->isInProgress()); + $this->assertFalse($this->job->isPending()); + + $this->job->markAsCompleted(); + $this->assertEquals(BulkImportJobStatus::COMPLETED, $this->job->getStatus()); + $this->assertTrue($this->job->isCompleted()); + $this->assertNotNull($this->job->getCompletedAt()); + + $job2 = new BulkInfoProviderImportJob(); + $job2->markAsFailed(); + $this->assertEquals(BulkImportJobStatus::FAILED, $job2->getStatus()); + $this->assertTrue($job2->isFailed()); + $this->assertNotNull($job2->getCompletedAt()); + + $job3 = new BulkInfoProviderImportJob(); + $job3->markAsStopped(); + $this->assertEquals(BulkImportJobStatus::STOPPED, $job3->getStatus()); + $this->assertTrue($job3->isStopped()); + $this->assertNotNull($job3->getCompletedAt()); + } + + public function testCanBeStopped(): void + { + $this->assertTrue($this->job->canBeStopped()); + + $this->job->markAsInProgress(); + $this->assertTrue($this->job->canBeStopped()); + + $this->job->markAsCompleted(); + $this->assertFalse($this->job->canBeStopped()); + + $this->job->setStatus(BulkImportJobStatus::FAILED); + $this->assertFalse($this->job->canBeStopped()); + + $this->job->setStatus(BulkImportJobStatus::STOPPED); + $this->assertFalse($this->job->canBeStopped()); + } + + public function testPartCount(): void + { + $this->assertEquals(0, $this->job->getPartCount()); + + $this->job->setPartIds([1, 2, 3, 4, 5]); + $this->assertEquals(5, $this->job->getPartCount()); + } + + public function testResultCount(): void + { + $this->assertEquals(0, $this->job->getResultCount()); + + $searchResults = [ + 1 => ['search_results' => [['name' => 'Part 1']]], + 2 => ['search_results' => [['name' => 'Part 2'], ['name' => 'Part 2 Alt']]], + 3 => ['search_results' => []] + ]; + $this->job->setSearchResults($searchResults); + $this->assertEquals(3, $this->job->getResultCount()); + } + + public function testPartProgressTracking(): void + { + $this->job->setPartIds([1, 2, 3, 4]); + + $this->assertFalse($this->job->isPartCompleted(1)); + $this->assertFalse($this->job->isPartSkipped(1)); + + $this->job->markPartAsCompleted(1); + $this->assertTrue($this->job->isPartCompleted(1)); + $this->assertFalse($this->job->isPartSkipped(1)); + + $this->job->markPartAsSkipped(2, 'Not found'); + $this->assertFalse($this->job->isPartCompleted(2)); + $this->assertTrue($this->job->isPartSkipped(2)); + + $this->job->markPartAsPending(1); + $this->assertFalse($this->job->isPartCompleted(1)); + $this->assertFalse($this->job->isPartSkipped(1)); + } + + public function testProgressCounts(): void + { + $this->job->setPartIds([1, 2, 3, 4, 5]); + + $this->assertEquals(0, $this->job->getCompletedPartsCount()); + $this->assertEquals(0, $this->job->getSkippedPartsCount()); + + $this->job->markPartAsCompleted(1); + $this->job->markPartAsCompleted(2); + $this->job->markPartAsSkipped(3, 'Error'); + + $this->assertEquals(2, $this->job->getCompletedPartsCount()); + $this->assertEquals(1, $this->job->getSkippedPartsCount()); + } + + public function testProgressPercentage(): void + { + $emptyJob = new BulkInfoProviderImportJob(); + $this->assertEquals(100.0, $emptyJob->getProgressPercentage()); + + $this->job->setPartIds([1, 2, 3, 4, 5]); + $this->assertEquals(0.0, $this->job->getProgressPercentage()); + + $this->job->markPartAsCompleted(1); + $this->job->markPartAsCompleted(2); + $this->assertEquals(40.0, $this->job->getProgressPercentage()); + + $this->job->markPartAsSkipped(3, 'Error'); + $this->assertEquals(60.0, $this->job->getProgressPercentage()); + + $this->job->markPartAsCompleted(4); + $this->job->markPartAsCompleted(5); + $this->assertEquals(100.0, $this->job->getProgressPercentage()); + } + + public function testIsAllPartsCompleted(): void + { + $emptyJob = new BulkInfoProviderImportJob(); + $this->assertTrue($emptyJob->isAllPartsCompleted()); + + $this->job->setPartIds([1, 2, 3]); + $this->assertFalse($this->job->isAllPartsCompleted()); + + $this->job->markPartAsCompleted(1); + $this->assertFalse($this->job->isAllPartsCompleted()); + + $this->job->markPartAsCompleted(2); + $this->job->markPartAsSkipped(3, 'Error'); + $this->assertTrue($this->job->isAllPartsCompleted()); + } + + public function testDisplayNameMethods(): void + { + $this->job->setPartIds([1, 2, 3]); + + $this->assertEquals('info_providers.bulk_import.job_name_template', $this->job->getDisplayNameKey()); + $this->assertEquals(['%count%' => 3], $this->job->getDisplayNameParams()); + } + + public function testFormattedTimestamp(): void + { + $timestampRegex = '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/'; + $this->assertMatchesRegularExpression($timestampRegex, $this->job->getFormattedTimestamp()); + } + + public function testProgressDataStructure(): void + { + $this->job->markPartAsCompleted(1); + $this->job->markPartAsSkipped(2, 'Test reason'); + + $progress = $this->job->getProgress(); + + $this->assertArrayHasKey(1, $progress); + $this->assertEquals('completed', $progress[1]['status']); + $this->assertArrayHasKey('completed_at', $progress[1]); + + $this->assertArrayHasKey(2, $progress); + $this->assertEquals('skipped', $progress[2]['status']); + $this->assertEquals('Test reason', $progress[2]['reason']); + $this->assertArrayHasKey('completed_at', $progress[2]); + } + + public function testCompletedAtTimestamp(): void + { + $this->assertNull($this->job->getCompletedAt()); + + $beforeCompletion = new \DateTimeImmutable(); + $this->job->markAsCompleted(); + $afterCompletion = new \DateTimeImmutable(); + + $completedAt = $this->job->getCompletedAt(); + $this->assertNotNull($completedAt); + $this->assertGreaterThanOrEqual($beforeCompletion, $completedAt); + $this->assertLessThanOrEqual($afterCompletion, $completedAt); + + $customTime = new \DateTimeImmutable('2023-01-01 12:00:00'); + $this->job->setCompletedAt($customTime); + $this->assertEquals($customTime, $this->job->getCompletedAt()); + } +} \ No newline at end of file diff --git a/tests/Form/InfoProviderSystem/GlobalFieldMappingTypeTest.php b/tests/Form/InfoProviderSystem/GlobalFieldMappingTypeTest.php new file mode 100644 index 00000000..52e0b1d2 --- /dev/null +++ b/tests/Form/InfoProviderSystem/GlobalFieldMappingTypeTest.php @@ -0,0 +1,68 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\Form\InfoProviderSystem; + +use App\Form\InfoProviderSystem\GlobalFieldMappingType; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Form\FormFactoryInterface; + +/** + * @group slow + * @group DB + */ +class GlobalFieldMappingTypeTest extends KernelTestCase +{ + private FormFactoryInterface $formFactory; + + protected function setUp(): void + { + self::bootKernel(); + $this->formFactory = static::getContainer()->get(FormFactoryInterface::class); + } + + public function testFormCreation(): void + { + $form = $this->formFactory->create(GlobalFieldMappingType::class, null, [ + 'field_choices' => [ + 'MPN' => 'mpn', + 'Name' => 'name' + ], + 'csrf_protection' => false + ]); + + $this->assertTrue($form->has('field_mappings')); + $this->assertTrue($form->has('prefetch_details')); + $this->assertTrue($form->has('submit')); + } + + public function testFormOptions(): void + { + $form = $this->formFactory->create(GlobalFieldMappingType::class, null, [ + 'field_choices' => [], + 'csrf_protection' => false + ]); + + $view = $form->createView(); + $this->assertFalse($view['prefetch_details']->vars['required']); + } +} \ No newline at end of file diff --git a/tests/Services/ElementTypeNameGeneratorTest.php b/tests/Services/ElementTypeNameGeneratorTest.php index 934a3bbd..5209f1ea 100644 --- a/tests/Services/ElementTypeNameGeneratorTest.php +++ b/tests/Services/ElementTypeNameGeneratorTest.php @@ -25,6 +25,7 @@ namespace App\Tests\Services; use App\Entity\Attachments\PartAttachment; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractNamedDBElement; +use App\Entity\BulkInfoProviderImportJob; use App\Entity\Parts\Category; use App\Entity\Parts\Part; use App\Exceptions\EntityNotSupportedException; @@ -50,12 +51,14 @@ class ElementTypeNameGeneratorTest extends WebTestCase //We only test in english $this->assertSame('Part', $this->service->getLocalizedTypeLabel(new Part())); $this->assertSame('Category', $this->service->getLocalizedTypeLabel(new Category())); + $this->assertSame('bulk_info_provider_import_job.label', $this->service->getLocalizedTypeLabel(new BulkInfoProviderImportJob())); //Test inheritance $this->assertSame('Attachment', $this->service->getLocalizedTypeLabel(new PartAttachment())); //Test for class name $this->assertSame('Part', $this->service->getLocalizedTypeLabel(Part::class)); + $this->assertSame('bulk_info_provider_import_job.label', $this->service->getLocalizedTypeLabel(BulkInfoProviderImportJob::class)); //Test exception for unknpwn type $this->expectException(EntityNotSupportedException::class);