From 8ea92ef330ef87c43c9e76b9e36a2870a08d98c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 15 Jul 2023 18:18:35 +0200 Subject: [PATCH] Added tests for DTOConverter --- .../InfoProviderSystem/DTOs/FileDTO.php | 12 +- .../InfoProviderSystem/DTOs/ParameterDTO.php | 4 + .../InfoProviderSystem/DTOs/PartDetailDTO.php | 6 +- .../InfoProviderSystem/DTOs/PriceDTO.php | 4 + .../DTOs/PurchaseInfoDTO.php | 3 + .../DTOs/SearchResultDTO.php | 3 + .../DTOtoEntityConverter.php | 75 +++++++- .../InfoProviderSystem/ProviderRegistry.php | 10 +- .../DTOs/PurchaseInfoDTOTest.php | 34 ++++ .../DTOtoEntityConverterTest.php | 181 ++++++++++++++++++ .../ProviderRegistryTest.php | 108 +++++++++++ 11 files changed, 429 insertions(+), 11 deletions(-) create mode 100644 tests/Services/InfoProviderSystem/DTOs/PurchaseInfoDTOTest.php create mode 100644 tests/Services/InfoProviderSystem/DTOtoEntityConverterTest.php create mode 100644 tests/Services/InfoProviderSystem/ProviderRegistryTest.php diff --git a/src/Services/InfoProviderSystem/DTOs/FileDTO.php b/src/Services/InfoProviderSystem/DTOs/FileDTO.php index 7c005b9b..516ab949 100644 --- a/src/Services/InfoProviderSystem/DTOs/FileDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/FileDTO.php @@ -23,12 +23,20 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem\DTOs; +/** + * This DTO represents a file that can be downloaded from a URL. + * This could be a datasheet, a 3D model, a picture or similar. + */ class FileDTO { + /** + * @param string $url The URL where to get this file + * @param string|null $name Optionally the name of this file + */ public function __construct( - /** The URL where to get this file */ public readonly string $url, - /** Optionally the name of this file */ public readonly ?string $name = null, ) {} + + } \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/DTOs/ParameterDTO.php b/src/Services/InfoProviderSystem/DTOs/ParameterDTO.php index 6500164b..f2a0d978 100644 --- a/src/Services/InfoProviderSystem/DTOs/ParameterDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/ParameterDTO.php @@ -23,6 +23,10 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem\DTOs; +/** + * This DTO represents a parameter of a part (similar to the AbstractParameter entity). + * This could be a voltage, a current, a temperature or similar. + */ class ParameterDTO { public function __construct( diff --git a/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php b/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php index f24e220f..7a7a83ca 100644 --- a/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php @@ -24,8 +24,10 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem\DTOs; use App\Entity\Parts\ManufacturingStatus; -use Hoa\Zformat\Parameter; +/** + * This DTO represents a part with all its details. + */ class PartDetailDTO extends SearchResultDTO { public function __construct( @@ -43,6 +45,8 @@ class PartDetailDTO extends SearchResultDTO public readonly ?string $notes = null, /** @var FileDTO[]|null */ public readonly ?array $datasheets = null, + /** @var FileDTO[]|null */ + public readonly ?array $images = null, /** @var ParameterDTO[]|null */ public readonly ?array $parameters = null, /** @var PurchaseInfoDTO[]|null */ diff --git a/src/Services/InfoProviderSystem/DTOs/PriceDTO.php b/src/Services/InfoProviderSystem/DTOs/PriceDTO.php index e10166ef..8c563149 100644 --- a/src/Services/InfoProviderSystem/DTOs/PriceDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/PriceDTO.php @@ -46,6 +46,10 @@ class PriceDTO $this->price_as_big_decimal = BigDecimal::of($this->price); } + /** + * Gets the price as BigDecimal + * @return BigDecimal + */ public function getPriceAsBigDecimal(): BigDecimal { return $this->price_as_big_decimal; diff --git a/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php b/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php index 09bbfeb2..6073cc5f 100644 --- a/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php @@ -23,6 +23,9 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem\DTOs; +/** + * This DTO represents a purchase information for a part (supplier name, order number and prices). + */ class PurchaseInfoDTO { public function __construct( diff --git a/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php b/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php index 228944ab..355041bf 100644 --- a/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php @@ -25,6 +25,9 @@ namespace App\Services\InfoProviderSystem\DTOs; use App\Entity\Parts\ManufacturingStatus; +/** + * This DTO represents a search result for a part. + */ class SearchResultDTO { public function __construct( diff --git a/src/Services/InfoProviderSystem/DTOtoEntityConverter.php b/src/Services/InfoProviderSystem/DTOtoEntityConverter.php index 6456bb99..b0e10d6a 100644 --- a/src/Services/InfoProviderSystem/DTOtoEntityConverter.php +++ b/src/Services/InfoProviderSystem/DTOtoEntityConverter.php @@ -47,13 +47,21 @@ use Doctrine\ORM\EntityManagerInterface; /** * This class converts DTOs to entities which can be persisted in the DB */ -class DTOtoEntityConverter +final class DTOtoEntityConverter { + private const TYPE_DATASHEETS_NAME = 'Datasheet'; + private const TYPE_IMAGE_NAME = 'Image'; public function __construct(private readonly EntityManagerInterface $em, private readonly string $base_currency) { } + /** + * Converts the given DTO to a PartParameter entity. + * @param ParameterDTO $dto + * @param PartParameter $entity The entity to apply the DTO on. If null a new entity will be created + * @return PartParameter + */ public function convertParameter(ParameterDTO $dto, PartParameter $entity = new PartParameter()): PartParameter { $entity->setName($dto->name); @@ -68,6 +76,12 @@ class DTOtoEntityConverter return $entity; } + /** + * Converts the given DTO to a Pricedetail entity. + * @param PriceDTO $dto + * @param Pricedetail $entity + * @return Pricedetail + */ public function convertPrice(PriceDTO $dto, Pricedetail $entity = new Pricedetail()): Pricedetail { $entity->setMinDiscountQuantity($dto->minimum_discount_amount); @@ -84,6 +98,9 @@ class DTOtoEntityConverter return $entity; } + /** + * Converts the given DTO to an orderdetail entity. + */ public function convertPurchaseInfo(PurchaseInfoDTO $dto, Orderdetail $entity = new Orderdetail()): Orderdetail { $entity->setSupplierpartnr($dto->order_number); @@ -97,10 +114,19 @@ class DTOtoEntityConverter return $entity; } - public function convertFile(FileDTO $dto, PartAttachment $entity = new PartAttachment()): PartAttachment + /** + * Converts the given DTO to an Attachment entity. + * @param FileDTO $dto + * @param AttachmentType $type The type which should be used for the attachment + * @param PartAttachment $entity + * @return PartAttachment + */ + public function convertFile(FileDTO $dto, AttachmentType $type, PartAttachment $entity = new PartAttachment()): PartAttachment { $entity->setURL($dto->url); + $entity->setAttachmentType($type); + //If no name is given, try to extract the name from the URL if (empty($dto->name)) { $entity->setName(basename($dto->url)); @@ -137,8 +163,9 @@ class DTOtoEntityConverter } //Add datasheets + $datasheet_type = $this->getDatasheetType(); foreach ($dto->datasheets ?? [] as $datasheet) { - $entity->addAttachment($this->convertFile($datasheet)); + $entity->addAttachment($this->convertFile($datasheet, $datasheet_type)); } //Add orderdetails and prices @@ -150,6 +177,8 @@ class DTOtoEntityConverter } /** + * Get the existing entity of the given class with the given name or create it if it does not exist. + * If the name is null, null is returned. * @template T of AbstractStructuralDBElement * @param string $class * @phpstan-param class-string $class @@ -168,6 +197,7 @@ class DTOtoEntityConverter } /** + * Get the existing entity of the given class with the given name or create it if it does not exist. * @template T of AbstractStructuralDBElement * @param string $class The class of the entity to create * @phpstan-param class-string $class @@ -180,6 +210,11 @@ class DTOtoEntityConverter return $this->em->getRepository($class)->findOrCreateForInfoProvider($name); } + /** + * Returns the currency entity for the given ISO code or create it if it does not exist + * @param string $iso_code + * @return Currency|null + */ private function getCurrency(string $iso_code): ?Currency { //Check if the currency is the base currency (then we can just return null) @@ -190,4 +225,38 @@ class DTOtoEntityConverter return $this->em->getRepository(Currency::class)->findOrCreateByISOCode($iso_code); } + /** + * Returns the attachment type used for datasheets or creates it if it does not exist + * @return AttachmentType + */ + private function getDatasheetType(): AttachmentType + { + /** @var AttachmentType $tmp */ + $tmp = $this->em->getRepository(AttachmentType::class)->findOrCreateForInfoProvider(self::TYPE_DATASHEETS_NAME); + + //If the entity was newly created, set the file filter + if ($tmp->getId() === null) { + $tmp->setFiletypeFilter('application/pdf'); + } + + return $tmp; + } + + /** + * Returns the attachment type used for datasheets or creates it if it does not exist + * @return AttachmentType + */ + private function getImageType(): AttachmentType + { + /** @var AttachmentType $tmp */ + $tmp = $this->em->getRepository(AttachmentType::class)->findOrCreateForInfoProvider(self::TYPE_IMAGE_NAME); + + //If the entity was newly created, set the file filter + if ($tmp->getId() === null) { + $tmp->setFiletypeFilter('image/*'); + } + + return $tmp; + } + } \ No newline at end of file diff --git a/src/Services/InfoProviderSystem/ProviderRegistry.php b/src/Services/InfoProviderSystem/ProviderRegistry.php index 2b9ef43b..921430e0 100644 --- a/src/Services/InfoProviderSystem/ProviderRegistry.php +++ b/src/Services/InfoProviderSystem/ProviderRegistry.php @@ -25,24 +25,24 @@ namespace App\Services\InfoProviderSystem; use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; -class ProviderRegistry +/** + * This class keeps track of all registered info providers and allows to find them by their key + */ +final class ProviderRegistry { - /** * @var InfoProviderInterface[] The info providers index by their keys - * @psalm-var array + * @phpstan-var array */ private array $providers_by_name = []; /** * @var InfoProviderInterface[] The enabled providers indexed by their keys - * @psalm-var array */ private array $providers_active = []; /** * @var InfoProviderInterface[] The disabled providers indexed by their keys - * @psalm-var array */ private array $providers_disabled = []; diff --git a/tests/Services/InfoProviderSystem/DTOs/PurchaseInfoDTOTest.php b/tests/Services/InfoProviderSystem/DTOs/PurchaseInfoDTOTest.php new file mode 100644 index 00000000..0442a873 --- /dev/null +++ b/tests/Services/InfoProviderSystem/DTOs/PurchaseInfoDTOTest.php @@ -0,0 +1,34 @@ +. + */ + +namespace App\Tests\Services\InfoProviderSystem\DTOs; + +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use PHPUnit\Framework\TestCase; + +class PurchaseInfoDTOTest extends TestCase +{ + public function testThrowOnInvalidType(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The prices array must only contain PriceDTO instances'); + new PurchaseInfoDTO('test', 'test', [new \stdClass()]); + } +} diff --git a/tests/Services/InfoProviderSystem/DTOtoEntityConverterTest.php b/tests/Services/InfoProviderSystem/DTOtoEntityConverterTest.php new file mode 100644 index 00000000..d9544145 --- /dev/null +++ b/tests/Services/InfoProviderSystem/DTOtoEntityConverterTest.php @@ -0,0 +1,181 @@ +. + */ + +namespace App\Tests\Services\InfoProviderSystem; + +use App\Entity\Attachments\AttachmentType; +use App\Entity\Parts\ManufacturingStatus; +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use App\Services\InfoProviderSystem\DTOtoEntityConverter; +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +class DTOtoEntityConverterTest extends WebTestCase +{ + + private ?DTOtoEntityConverter $service = null; + + public function setUp(): void + { + self::bootKernel(); + $this->service = self::getContainer()->get(DTOtoEntityConverter::class); + } + + public function testConvertParameter(): void + { + $dto = new ParameterDTO( + name: 'TestParameter', + value_text: 'Text', + value_typ: 10.0, value_min: 0.0, value_max: 100.0, + unit: 'kg', symbol: 'TP', group: 'TestGroup' + ); + + $entity = $this->service->convertParameter($dto); + + $this->assertEquals($dto->name, $entity->getName()); + $this->assertEquals($dto->value_text, $entity->getValueText()); + $this->assertEquals($dto->value_typ, $entity->getValueTypical()); + $this->assertEquals($dto->value_min, $entity->getValueMin()); + $this->assertEquals($dto->value_max, $entity->getValueMax()); + $this->assertEquals($dto->unit, $entity->getUnit()); + $this->assertEquals($dto->symbol, $entity->getSymbol()); + $this->assertEquals($dto->group, $entity->getGroup()); + } + + public function testConvertPriceOtherCurrency(): void + { + $dto = new PriceDTO( + minimum_discount_amount: 5, + price: "10.0", + currency_iso_code: 'CNY', + includes_tax: true, + ); + + $entity = $this->service->convertPrice($dto); + $this->assertEquals($dto->minimum_discount_amount, $entity->getMinDiscountQuantity()); + $this->assertEquals((float) $dto->price, (float) (string) $entity->getPrice()); + + //For non-base currencies, a new currency entity is created + $currency = $entity->getCurrency(); + $this->assertEquals($dto->currency_iso_code, $currency->getIsoCode()); + } + + public function testConvertPriceBaseCurrency(): void + { + $dto = new PriceDTO( + minimum_discount_amount: 5, + price: "10.0", + currency_iso_code: 'EUR', + includes_tax: true, + ); + + $entity = $this->service->convertPrice($dto); + + //For base currencies, the currency field is null + $this->assertNull($entity->getCurrency()); + } + + public function testConvertPurchaseInfo(): void + { + $prices = [ + new PriceDTO(1, "10.0", 'EUR'), + new PriceDTO(5, "9.0", 'EUR'), + ]; + + $dto = new PurchaseInfoDTO( + distributor_name: 'TestDistributor', + order_number: 'TestOrderNumber', + prices: $prices, + product_url: 'https://example.com', + ); + + $entity = $this->service->convertPurchaseInfo($dto); + + $this->assertEquals($dto->distributor_name, $entity->getSupplier()->getName()); + $this->assertEquals($dto->order_number, $entity->getSupplierPartNr()); + $this->assertEquals($dto->product_url, $entity->getSupplierProductUrl()); + } + + public function testConvertFileWithName(): void + { + $dto = new FileDTO(url: 'https://invalid.com/file.pdf', name: 'TestFile'); + $type = new AttachmentType(); + + + $entity = $this->service->convertFile($dto, $type); + + $this->assertEquals($dto->name, $entity->getName()); + $this->assertEquals($dto->url, $entity->getUrl()); + $this->assertEquals($type, $entity->getAttachmentType()); + } + + public function testConvertFileWithoutName(): void + { + $dto = new FileDTO(url: 'https://invalid.invalid/file.pdf'); + $type = new AttachmentType(); + + + $entity = $this->service->convertFile($dto, $type); + + //If no name is given, the name is derived from the url + $this->assertEquals('file.pdf', $entity->getName()); + $this->assertEquals($dto->url, $entity->getUrl()); + $this->assertEquals($type, $entity->getAttachmentType()); + } + + public function testConvertPart() + { + $parameters = [new ParameterDTO('Test', 'Test')]; + $datasheets = [new FileDTO('https://invalid.invalid/file.pdf')]; + $images = [new FileDTO('https://invalid.invalid/image.png')]; + $shopping_infos = [new PurchaseInfoDTO('TestDistributor', 'TestOrderNumber', [new PriceDTO(1, "10.0", 'EUR')])]; + + $dto = new PartDetailDTO( + provider_key: 'test_provider', provider_id: 'test_id', provider_url: 'https://invalid.invalid/test_id', + name: 'TestPart', description: 'TestDescription', category: 'TestCategory', + manufacturer: 'TestManufacturer', mpn: 'TestMPN', manufacturing_status: ManufacturingStatus::EOL, + preview_image_url: 'https://invalid.invalid/image.png', + footprint: 'DIP8', notes: 'TestNotes', mass: 10.4, + parameters: $parameters, datasheets: $datasheets, vendor_infos: $shopping_infos, images: $images + ); + + $entity = $this->service->convertPart($dto); + + $this->assertSame($dto->name, $entity->getName()); + $this->assertSame($dto->description, $entity->getDescription()); + $this->assertSame($dto->notes, $entity->getComment()); + + $this->assertSame($dto->manufacturer, $entity->getManufacturer()->getName()); + $this->assertSame($dto->mpn, $entity->getManufacturerProductNumber()); + $this->assertSame($dto->manufacturing_status, $entity->getManufacturingStatus()); + + $this->assertEquals($dto->mass, $entity->getMass()); + $this->assertEquals($dto->footprint, $entity->getFootprint()); + + //We just check that the lenghts of parameters, datasheets, images and shopping infos are the same + //The actual content is tested in the corresponding tests + $this->assertCount(count($parameters), $entity->getParameters()); + $this->assertCount(count($shopping_infos), $entity->getOrderdetails()); + } +} diff --git a/tests/Services/InfoProviderSystem/ProviderRegistryTest.php b/tests/Services/InfoProviderSystem/ProviderRegistryTest.php new file mode 100644 index 00000000..f25d89e4 --- /dev/null +++ b/tests/Services/InfoProviderSystem/ProviderRegistryTest.php @@ -0,0 +1,108 @@ +. + */ + +namespace App\Tests\Services\InfoProviderSystem; + +use App\Services\InfoProviderSystem\ProviderRegistry; +use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; +use PHPUnit\Framework\TestCase; + +class ProviderRegistryTest extends TestCase +{ + + /** @var InfoProviderInterface[] */ + private array $providers = []; + + public function setUp(): void + { + //Create some mock providers + $this->providers = [ + $this->getMockProvider('test1'), + $this->getMockProvider('test2'), + $this->getMockProvider('test3', false), + ]; + } + + public function getMockProvider(string $key, bool $active = true): InfoProviderInterface + { + $mock = $this->createMock(InfoProviderInterface::class); + $mock->method('getProviderKey')->willReturn($key); + $mock->method('isActive')->willReturn($active); + + return $mock; + } + + public function testGetProviders(): void + { + $registry = new ProviderRegistry($this->providers); + + $this->assertEquals( + [ + 'test1' => $this->providers[0], + 'test2' => $this->providers[1], + 'test3' => $this->providers[2], + ], + $registry->getProviders()); + } + + public function testGetDisabledProviders(): void + { + $registry = new ProviderRegistry($this->providers); + + $this->assertEquals( + [ + 'test3' => $this->providers[2], + ], + $registry->getDisabledProviders()); + } + + public function testGetActiveProviders(): void + { + $registry = new ProviderRegistry($this->providers); + + $this->assertEquals( + [ + 'test1' => $this->providers[0], + 'test2' => $this->providers[1], + ], + $registry->getActiveProviders()); + } + + public function testGetProviderByKey(): void + { + $registry = new ProviderRegistry($this->providers); + + $this->assertEquals( + $this->providers[0], + $registry->getProviderByKey('test1') + ); + } + + public function testThrowOnDuplicateKeyOfProviders(): void + { + $this->expectException(\LogicException::class); + + $registry = new ProviderRegistry([ + $this->getMockProvider('test1'), + $this->getMockProvider('test2'), + $this->getMockProvider('test1'), + ]); + } +}