Added tests for DTOConverter

This commit is contained in:
Jan Böhmer 2023-07-15 18:18:35 +02:00
parent de82249d8d
commit 8ea92ef330
11 changed files with 429 additions and 11 deletions

View file

@ -23,12 +23,20 @@ declare(strict_types=1);
namespace App\Services\InfoProviderSystem\DTOs; 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 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( public function __construct(
/** The URL where to get this file */
public readonly string $url, public readonly string $url,
/** Optionally the name of this file */
public readonly ?string $name = null, public readonly ?string $name = null,
) {} ) {}
} }

View file

@ -23,6 +23,10 @@ declare(strict_types=1);
namespace App\Services\InfoProviderSystem\DTOs; 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 class ParameterDTO
{ {
public function __construct( public function __construct(

View file

@ -24,8 +24,10 @@ declare(strict_types=1);
namespace App\Services\InfoProviderSystem\DTOs; namespace App\Services\InfoProviderSystem\DTOs;
use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\ManufacturingStatus;
use Hoa\Zformat\Parameter;
/**
* This DTO represents a part with all its details.
*/
class PartDetailDTO extends SearchResultDTO class PartDetailDTO extends SearchResultDTO
{ {
public function __construct( public function __construct(
@ -43,6 +45,8 @@ class PartDetailDTO extends SearchResultDTO
public readonly ?string $notes = null, public readonly ?string $notes = null,
/** @var FileDTO[]|null */ /** @var FileDTO[]|null */
public readonly ?array $datasheets = null, public readonly ?array $datasheets = null,
/** @var FileDTO[]|null */
public readonly ?array $images = null,
/** @var ParameterDTO[]|null */ /** @var ParameterDTO[]|null */
public readonly ?array $parameters = null, public readonly ?array $parameters = null,
/** @var PurchaseInfoDTO[]|null */ /** @var PurchaseInfoDTO[]|null */

View file

@ -46,6 +46,10 @@ class PriceDTO
$this->price_as_big_decimal = BigDecimal::of($this->price); $this->price_as_big_decimal = BigDecimal::of($this->price);
} }
/**
* Gets the price as BigDecimal
* @return BigDecimal
*/
public function getPriceAsBigDecimal(): BigDecimal public function getPriceAsBigDecimal(): BigDecimal
{ {
return $this->price_as_big_decimal; return $this->price_as_big_decimal;

View file

@ -23,6 +23,9 @@ declare(strict_types=1);
namespace App\Services\InfoProviderSystem\DTOs; namespace App\Services\InfoProviderSystem\DTOs;
/**
* This DTO represents a purchase information for a part (supplier name, order number and prices).
*/
class PurchaseInfoDTO class PurchaseInfoDTO
{ {
public function __construct( public function __construct(

View file

@ -25,6 +25,9 @@ namespace App\Services\InfoProviderSystem\DTOs;
use App\Entity\Parts\ManufacturingStatus; use App\Entity\Parts\ManufacturingStatus;
/**
* This DTO represents a search result for a part.
*/
class SearchResultDTO class SearchResultDTO
{ {
public function __construct( public function __construct(

View file

@ -47,13 +47,21 @@ use Doctrine\ORM\EntityManagerInterface;
/** /**
* This class converts DTOs to entities which can be persisted in the DB * 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) 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 public function convertParameter(ParameterDTO $dto, PartParameter $entity = new PartParameter()): PartParameter
{ {
$entity->setName($dto->name); $entity->setName($dto->name);
@ -68,6 +76,12 @@ class DTOtoEntityConverter
return $entity; 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 public function convertPrice(PriceDTO $dto, Pricedetail $entity = new Pricedetail()): Pricedetail
{ {
$entity->setMinDiscountQuantity($dto->minimum_discount_amount); $entity->setMinDiscountQuantity($dto->minimum_discount_amount);
@ -84,6 +98,9 @@ class DTOtoEntityConverter
return $entity; return $entity;
} }
/**
* Converts the given DTO to an orderdetail entity.
*/
public function convertPurchaseInfo(PurchaseInfoDTO $dto, Orderdetail $entity = new Orderdetail()): Orderdetail public function convertPurchaseInfo(PurchaseInfoDTO $dto, Orderdetail $entity = new Orderdetail()): Orderdetail
{ {
$entity->setSupplierpartnr($dto->order_number); $entity->setSupplierpartnr($dto->order_number);
@ -97,10 +114,19 @@ class DTOtoEntityConverter
return $entity; 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->setURL($dto->url);
$entity->setAttachmentType($type);
//If no name is given, try to extract the name from the URL //If no name is given, try to extract the name from the URL
if (empty($dto->name)) { if (empty($dto->name)) {
$entity->setName(basename($dto->url)); $entity->setName(basename($dto->url));
@ -137,8 +163,9 @@ class DTOtoEntityConverter
} }
//Add datasheets //Add datasheets
$datasheet_type = $this->getDatasheetType();
foreach ($dto->datasheets ?? [] as $datasheet) { foreach ($dto->datasheets ?? [] as $datasheet) {
$entity->addAttachment($this->convertFile($datasheet)); $entity->addAttachment($this->convertFile($datasheet, $datasheet_type));
} }
//Add orderdetails and prices //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 * @template T of AbstractStructuralDBElement
* @param string $class * @param string $class
* @phpstan-param class-string<T> $class * @phpstan-param class-string<T> $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 * @template T of AbstractStructuralDBElement
* @param string $class The class of the entity to create * @param string $class The class of the entity to create
* @phpstan-param class-string<T> $class * @phpstan-param class-string<T> $class
@ -180,6 +210,11 @@ class DTOtoEntityConverter
return $this->em->getRepository($class)->findOrCreateForInfoProvider($name); 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 private function getCurrency(string $iso_code): ?Currency
{ {
//Check if the currency is the base currency (then we can just return null) //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); 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;
}
} }

View file

@ -25,24 +25,24 @@ namespace App\Services\InfoProviderSystem;
use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; 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 * @var InfoProviderInterface[] The info providers index by their keys
* @psalm-var array * @phpstan-var array<string, InfoProviderInterface>
*/ */
private array $providers_by_name = []; private array $providers_by_name = [];
/** /**
* @var InfoProviderInterface[] The enabled providers indexed by their keys * @var InfoProviderInterface[] The enabled providers indexed by their keys
* @psalm-var array
*/ */
private array $providers_active = []; private array $providers_active = [];
/** /**
* @var InfoProviderInterface[] The disabled providers indexed by their keys * @var InfoProviderInterface[] The disabled providers indexed by their keys
* @psalm-var array
*/ */
private array $providers_disabled = []; private array $providers_disabled = [];

View file

@ -0,0 +1,34 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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()]);
}
}

View file

@ -0,0 +1,181 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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());
}
}

View file

@ -0,0 +1,108 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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'),
]);
}
}