partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

commit 1f16c65c144f1aa29cca6ca1a87a61b4d0b540c2
parent 95e518c03cfe58f3996b672b13a20bed00a5cceb
Author: Felicia Hummel <felicitus@felicitus.org>
Date:   Wed, 17 Aug 2016 14:11:45 +0200

Merge pull request #716 from partkeepr/A20160606

A20160606
Diffstat:
Mapp/SymfonyRequirements.php | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mapp/check.php | 8++++----
Mapp/config/config_test.yml | 3+++
Mapp/config/parameters.php.dist | 8++++++++
Msrc/PartKeepr/DoctrineReflectionBundle/Filter/Sorter.php | 4+---
Asrc/PartKeepr/PartBundle/Action/PartPostAction.php | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/PartKeepr/PartBundle/Action/PostAction.php | 63---------------------------------------------------------------
Asrc/PartKeepr/PartBundle/Exceptions/InternalPartNumberNotUniqueException.php | 13+++++++++++++
Msrc/PartKeepr/PartBundle/Resources/config/actions.xml | 20+++++++++++---------
Msrc/PartKeepr/PartBundle/Resources/config/services.xml | 1+
Msrc/PartKeepr/PartBundle/Services/PartService.php | 48+++++++++++++++++++++++++++++++++++++++++++++++-
Asrc/PartKeepr/PartBundle/Tests/InternalPartNumberTest.php | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 286 insertions(+), 109 deletions(-)

diff --git a/app/SymfonyRequirements.php b/app/SymfonyRequirements.php @@ -33,9 +33,13 @@ class Requirement { private $fulfilled; + private $testMessage; + private $helpText; + private $helpHtml; + private $optional; /** @@ -49,11 +53,11 @@ class Requirement */ public function __construct($fulfilled, $testMessage, $helpHtml, $helpText = null, $optional = false) { - $this->fulfilled = (bool) $fulfilled; - $this->testMessage = (string) $testMessage; - $this->helpHtml = (string) $helpHtml; - $this->helpText = null === $helpText ? strip_tags($this->helpHtml) : (string) $helpText; - $this->optional = (bool) $optional; + $this->fulfilled = (bool)$fulfilled; + $this->testMessage = (string)$testMessage; + $this->helpHtml = (string)$helpHtml; + $this->helpText = null === $helpText ? strip_tags($this->helpHtml) : (string)$helpText; + $this->optional = (bool)$optional; } /** @@ -128,8 +132,15 @@ class PhpIniRequirement extends Requirement * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) * @param bool $optional Whether this is only an optional recommendation not a mandatory requirement */ - public function __construct($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null, $optional = false) - { + public function __construct( + $cfgName, + $evaluation, + $approveCfgAbsence = false, + $testMessage = null, + $helpHtml = null, + $helpText = null, + $optional = false + ) { $cfgValue = ini_get($cfgName); if (is_callable($evaluation)) { @@ -157,7 +168,8 @@ class PhpIniRequirement extends Requirement $fulfilled = $evaluation == $cfgValue; } - parent::__construct($fulfilled || ($approveCfgAbsence && false === $cfgValue), $testMessage, $helpHtml, $helpText, $optional); + parent::__construct($fulfilled || ($approveCfgAbsence && false === $cfgValue), $testMessage, $helpHtml, + $helpText, $optional); } } @@ -168,7 +180,7 @@ class PhpIniRequirement extends Requirement */ class RequirementCollection implements IteratorAggregate { - private $requirements = array(); + private $requirements = []; /** * Gets the current RequirementCollection as an Iterator. @@ -229,9 +241,16 @@ class RequirementCollection implements IteratorAggregate * @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived) * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) */ - public function addPhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null) - { - $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, false)); + public function addPhpIniRequirement( + $cfgName, + $evaluation, + $approveCfgAbsence = false, + $testMessage = null, + $helpHtml = null, + $helpText = null + ) { + $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, + false)); } /** @@ -247,9 +266,16 @@ class RequirementCollection implements IteratorAggregate * @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived) * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) */ - public function addPhpIniRecommendation($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null) - { - $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, true)); + public function addPhpIniRecommendation( + $cfgName, + $evaluation, + $approveCfgAbsence = false, + $testMessage = null, + $helpHtml = null, + $helpText = null + ) { + $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, + true)); } /** @@ -279,7 +305,7 @@ class RequirementCollection implements IteratorAggregate */ public function getRequirements() { - $array = array(); + $array = []; foreach ($this->requirements as $req) { if (!$req->isOptional()) { $array[] = $req; @@ -296,7 +322,7 @@ class RequirementCollection implements IteratorAggregate */ public function getFailedRequirements() { - $array = array(); + $array = []; foreach ($this->requirements as $req) { if (!$req->isFulfilled() && !$req->isOptional()) { $array[] = $req; @@ -313,7 +339,7 @@ class RequirementCollection implements IteratorAggregate */ public function getRecommendations() { - $array = array(); + $array = []; foreach ($this->requirements as $req) { if ($req->isOptional()) { $array[] = $req; @@ -330,7 +356,7 @@ class RequirementCollection implements IteratorAggregate */ public function getFailedRecommendations() { - $array = array(); + $array = []; foreach ($this->requirements as $req) { if (!$req->isFulfilled() && $req->isOptional()) { $array[] = $req; @@ -393,7 +419,8 @@ class SymfonyRequirements extends RequirementCollection sprintf('You are running PHP version "<strong>%s</strong>", but Symfony needs at least PHP "<strong>%s</strong>" to run. Before using Symfony, upgrade your PHP installation, preferably to the latest version.', $installedPhpVersion, self::REQUIRED_PHP_VERSION), - sprintf('Install PHP %s or newer (installed version is %s)', self::REQUIRED_PHP_VERSION, $installedPhpVersion) + sprintf('Install PHP %s or newer (installed version is %s)', self::REQUIRED_PHP_VERSION, + $installedPhpVersion) ); $this->addRequirement( @@ -406,7 +433,7 @@ class SymfonyRequirements extends RequirementCollection is_dir(__DIR__.'/../vendor/composer'), 'Vendor libraries must be installed', 'Vendor libraries are missing. Install composer following instructions from <a href="http://getcomposer.org/">http://getcomposer.org/</a>. '. - 'Then run "<strong>php composer.phar install</strong>" to install them.' + 'Then run "<strong>php composer.phar install</strong>" to install them.' ); $cacheDir = is_dir(__DIR__.'/../var/cache') ? __DIR__.'/../var/cache' : __DIR__.'/cache'; @@ -432,7 +459,7 @@ class SymfonyRequirements extends RequirementCollection ); if (version_compare($installedPhpVersion, self::REQUIRED_PHP_VERSION, '>=')) { - $timezones = array(); + $timezones = []; foreach (DateTimeZone::listAbbreviations() as $abbreviations) { foreach ($abbreviations as $abbreviation) { $timezones[$abbreviation['timezone_id']] = true; @@ -441,7 +468,8 @@ class SymfonyRequirements extends RequirementCollection $this->addRequirement( isset($timezones[@date_default_timezone_get()]), - sprintf('Configured default timezone "%s" must be supported by your installation of PHP', @date_default_timezone_get()), + sprintf('Configured default timezone "%s" must be supported by your installation of PHP', + @date_default_timezone_get()), 'Your default timezone is not supported by PHP. Check for typos in your <strong>php.ini</strong> file and have a look at the list of deprecated timezones at <a href="http://php.net/manual/en/timezones.others.php">http://php.net/manual/en/timezones.others.php</a>.' ); } @@ -528,7 +556,7 @@ class SymfonyRequirements extends RequirementCollection ); } - $pcreVersion = defined('PCRE_VERSION') ? (float) PCRE_VERSION : null; + $pcreVersion = defined('PCRE_VERSION') ? (float)PCRE_VERSION : null; $this->addRequirement( null !== $pcreVersion, @@ -590,7 +618,8 @@ class SymfonyRequirements extends RequirementCollection ); $this->addRecommendation( - (version_compare($installedPhpVersion, '5.3.18', '>=') && version_compare($installedPhpVersion, '5.4.0', '<')) + (version_compare($installedPhpVersion, '5.3.18', '>=') && version_compare($installedPhpVersion, '5.4.0', + '<')) || version_compare($installedPhpVersion, '5.4.8', '>='), 'You should use PHP 5.3.18+ or PHP 5.4.8+ to always get nice error messages for fatal errors in the development environment due to PHP bug #61767/#60909', @@ -697,8 +726,7 @@ class SymfonyRequirements extends RequirementCollection || (extension_loaded('xcache') && ini_get('xcache.cacher')) || - (extension_loaded('wincache') && ini_get('wincache.ocenabled')) - ; + (extension_loaded('wincache') && ini_get('wincache.ocenabled')); $this->addRecommendation( $accelerator, @@ -732,7 +760,8 @@ class SymfonyRequirements extends RequirementCollection $drivers = PDO::getAvailableDrivers(); $this->addRecommendation( count($drivers) > 0, - sprintf('PDO should have some drivers installed (currently available: %s)', count($drivers) ? implode(', ', $drivers) : 'none'), + sprintf('PDO should have some drivers installed (currently available: %s)', + count($drivers) ? implode(', ', $drivers) : 'none'), 'Install <strong>PDO drivers</strong> (mandatory for Doctrine).' ); } @@ -758,7 +787,7 @@ class SymfonyRequirements extends RequirementCollection case 'k': return $size * 1024; default: - return (int) $size; + return (int)$size; } } } diff --git a/app/check.php b/app/check.php @@ -19,7 +19,7 @@ echo PHP_EOL.PHP_EOL; echo '> Checking Symfony requirements:'.PHP_EOL.' '; -$messages = array(); +$messages = []; foreach ($symfonyRequirements->getRequirements() as $req) { /** @var $req Requirement */ if ($helpText = get_error_message($req, $lineSize)) { @@ -99,7 +99,7 @@ function echo_title($title, $style = null) function echo_style($style, $message) { // ANSI color codes - $styles = array( + $styles = [ 'reset' => "\033[0m", 'red' => "\033[31m", 'green' => "\033[32m", @@ -107,10 +107,10 @@ function echo_style($style, $message) 'error' => "\033[37;41m", 'success' => "\033[37;42m", 'title' => "\033[34m", - ); + ]; $supports = has_color_support(); - echo($supports ? $styles[$style] : '').$message.($supports ? $styles['reset'] : ''); + echo ($supports ? $styles[$style] : '').$message.($supports ? $styles['reset'] : ''); } function echo_block($style, $title, $message) diff --git a/app/config/config_test.yml b/app/config/config_test.yml @@ -60,3 +60,6 @@ security: admin: password: x61Ey612Kl2gpFL56FT9weDnpSo4AV8j8+qx2AuTHdRyY036xxzTTrw10Wq3+4qQyB+XURPWx1ONxp3Y3pB37A== roles: 'ROLE_ADMIN' + +parameters: + partkeepr.parts.internalPartNumberUnique: true diff --git a/app/config/parameters.php.dist b/app/config/parameters.php.dist @@ -222,6 +222,14 @@ $container->setParameter('partkeepr.users.limit', false); $container->setParameter('partkeepr.parts.limit', false); /** + * Defines if the internal part number must be unique or not. Note that empty internal part numbers aren't checked - + * if you require to enforce an internal part number, also set the field internalPartNumber to mandatory. + * + * Defaults to false + */ +$container->setParameter('partkeepr.parts.internalPartNumberUnique', false); + +/** * Specifies the PartKeepr data directory */ $container->setParameter('partkeepr.filesystem.data_directory', '%kernel.root_dir%/../data/'); diff --git a/src/PartKeepr/DoctrineReflectionBundle/Filter/Sorter.php b/src/PartKeepr/DoctrineReflectionBundle/Filter/Sorter.php @@ -27,5 +27,4 @@ class Sorter implements AssociationPropertyInterface { $this->direction = $direction; } - -}- \ No newline at end of file +} diff --git a/src/PartKeepr/PartBundle/Action/PartPostAction.php b/src/PartKeepr/PartBundle/Action/PartPostAction.php @@ -0,0 +1,75 @@ +<?php + +namespace PartKeepr\PartBundle\Action; + +use Dunglas\ApiBundle\Action\ActionUtilTrait; +use Dunglas\ApiBundle\Api\ResourceInterface; +use Dunglas\ApiBundle\Exception\RuntimeException; +use PartKeepr\PartBundle\Entity\Part; +use PartKeepr\PartBundle\Exceptions\InternalPartNumberNotUniqueException; +use PartKeepr\PartBundle\Exceptions\PartLimitExceededException; +use PartKeepr\PartBundle\Services\PartService; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Serializer\SerializerInterface; + +class PartPostAction +{ + use ActionUtilTrait; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var PartService + */ + private $partService; + + public function __construct( + SerializerInterface $serializer, + PartService $partService + ) { + $this->serializer = $serializer; + $this->partService = $partService; + } + + /** + * Injects the specific root node ID if "@local-tree-root" was specified. + * + * @param Request $request + * + * @throws RuntimeException + * @throws PartLimitExceededException + * @throws InternalPartNumberNotUniqueException + * + * @return mixed + */ + public function __invoke(Request $request) + { + if ($this->partService->checkPartLimit()) { + throw new PartLimitExceededException(); + } + + /** + * @var $resourceType ResourceInterface + */ + list($resourceType, $format) = $this->extractAttributes($request); + + /** + * @var $part Part + */ + $part = $this->serializer->deserialize( + $request->getContent(), + $resourceType->getEntityClass(), + $format, + $resourceType->getDenormalizationContext() + ); + + if (!$this->partService->isInternalPartNumberUnique($part->getInternalPartNumber())) { + throw new InternalPartNumberNotUniqueException(); + } + + return $part; + } +} diff --git a/src/PartKeepr/PartBundle/Action/PostAction.php b/src/PartKeepr/PartBundle/Action/PostAction.php @@ -1,63 +0,0 @@ -<?php - -namespace PartKeepr\PartBundle\Action; - -use Dunglas\ApiBundle\Action\ActionUtilTrait; -use Dunglas\ApiBundle\Api\ResourceInterface; -use Dunglas\ApiBundle\Exception\RuntimeException; -use PartKeepr\PartBundle\Exceptions\PartLimitExceededException; -use PartKeepr\PartBundle\Services\PartService; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Serializer\SerializerInterface; - -class PostAction -{ - use ActionUtilTrait; - - /** - * @var SerializerInterface - */ - private $serializer; - - /** - * @var PartService - */ - private $partService; - - public function __construct( - SerializerInterface $serializer, - PartService $partService - ) { - $this->serializer = $serializer; - $this->partService = $partService; - } - - /** - * Injects the specific root node ID if "@local-tree-root" was specified. - * - * @param Request $request - * - * @throws RuntimeException - * @throws PartLimitExceededException - * - * @return mixed - */ - public function __invoke(Request $request) - { - /* - * @var $resourceType ResourceInterface - */ - if ($this->partService->checkPartLimit()) { - throw new PartLimitExceededException(); - } - - list($resourceType, $format) = $this->extractAttributes($request); - - return $this->serializer->deserialize( - $request->getContent(), - $resourceType->getEntityClass(), - $format, - $resourceType->getDenormalizationContext() - ); - } -} diff --git a/src/PartKeepr/PartBundle/Exceptions/InternalPartNumberNotUniqueException.php b/src/PartKeepr/PartBundle/Exceptions/InternalPartNumberNotUniqueException.php @@ -0,0 +1,13 @@ +<?php + +namespace PartKeepr\PartBundle\Exceptions; + +use PartKeepr\CoreBundle\Exceptions\TranslatableException; + +class InternalPartNumberNotUniqueException extends TranslatableException +{ + public function getMessageKey() + { + return 'The internal part number is already used'; + } +} diff --git a/src/PartKeepr/PartBundle/Resources/config/actions.xml b/src/PartKeepr/PartBundle/Resources/config/actions.xml @@ -5,30 +5,32 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> - <service id="partkeepr.part.post" class="PartKeepr\PartBundle\Action\PostAction"> - <argument type="service" id="api.serializer" /> - <argument type="service" id="partkeepr.part_service" /> + <service id="partkeepr.part.post" class="PartKeepr\PartBundle\Action\PartPostAction"> + <argument type="service" id="api.serializer"/> + <argument type="service" id="partkeepr.part_service"/> </service> - <service id="partkeepr.part.put" class="PartKeepr\PartBundle\Action\PartPutAction"> + <service id="partkeepr.part.put" class="PartKeepr\PartBundle\Action\PartPutAction"> <argument type="service" id="api.data_provider"/> - <argument type="service" id="api.serializer" /> + <argument type="service" id="api.serializer"/> + <argument type="service" id="partkeepr.part_service"/> </service> <service id="partkeepr.part.add_stock" class="PartKeepr\PartBundle\Action\AddStockAction"> <argument type="service" id="api.data_provider"/> - <argument type="service" id="partkeepr.userservice" /> + <argument type="service" id="partkeepr.userservice"/> <argument type="service" id="doctrine"/> </service> <service id="partkeepr.part.remove_stock" class="PartKeepr\PartBundle\Action\RemoveStockAction"> <argument type="service" id="api.data_provider"/> - <argument type="service" id="partkeepr.userservice" /> + <argument type="service" id="partkeepr.userservice"/> <argument type="service" id="doctrine"/> </service> <service id="partkeepr.part.set_stock" class="PartKeepr\PartBundle\Action\SetStockAction"> <argument type="service" id="api.data_provider"/> - <argument type="service" id="partkeepr.userservice" /> + <argument type="service" id="partkeepr.userservice"/> <argument type="service" id="doctrine"/> </service> - <service id="partkeepr.part_measurement_unit.set_default" class="PartKeepr\PartBundle\Action\SetDefaultUnitAction"> + <service id="partkeepr.part_measurement_unit.set_default" + class="PartKeepr\PartBundle\Action\SetDefaultUnitAction"> <argument type="service" id="api.data_provider"/> <argument type="service" id="partkeepr.part_measurement_unit_service"/> </service> diff --git a/src/PartKeepr/PartBundle/Resources/config/services.xml b/src/PartKeepr/PartBundle/Resources/config/services.xml @@ -16,6 +16,7 @@ <service id="partkeepr.part_service" class="PartKeepr\PartBundle\Services\PartService"> <argument type="service" id="doctrine.orm.default_entity_manager" /> <argument type="string">%partkeepr.parts.limit%</argument> + <argument type="string">%partkeepr.parts.internalPartNumberUnique%</argument> </service> </services> diff --git a/src/PartKeepr/PartBundle/Services/PartService.php b/src/PartKeepr/PartBundle/Services/PartService.php @@ -3,6 +3,7 @@ namespace PartKeepr\PartBundle\Services; use Doctrine\ORM\EntityManager; +use PartKeepr\PartBundle\Entity\Part; class PartService { @@ -14,16 +15,25 @@ class PartService private $partLimit; /** + * Whether to check if the internal part number is unique or not + * + * @var bool + */ + private $checkInternalPartNumberUniqueness; + + /** * @var EntityManager */ private $entityManager; public function __construct( EntityManager $entityManager, - $partLimit = false + $partLimit = false, + $checkInternalPartNumberUniqueness = false ) { $this->entityManager = $entityManager; $this->partLimit = $partLimit; + $this->checkInternalPartNumberUniqueness = $checkInternalPartNumberUniqueness; } /** @@ -40,6 +50,42 @@ class PartService } /** + * Checks if the given internal part number is unique + * + * @param string $internalPartNumber The internal part number to checkl + * @param Part|null $part An optional part to exclude within the check + * + * @return bool + */ + public function isInternalPartNumberUnique ($internalPartNumber, Part $part = null) { + if (!$this->checkInternalPartNumberUniqueness) { + return true; + } + + /** + * Empty internal part numbers aren't checked. If you want to require an internal part number, set the + * field internalPartNumber to mandatory. + */ + if ($internalPartNumber == "") { + return true; + } + + $dql = 'SELECT COUNT(p) FROM PartKeepr\\PartBundle\\Entity\\Part p WHERE p.internalPartNumber = :internalPartNumber'; + + if ($part !== null) { + $dql .= " AND p.id != :partId"; + } + + $query = $this->entityManager->createQuery($dql)->setParameter('internalPartNumber', $internalPartNumber); + + if ($part !== null) { + $query->setParameter('partId', $part->getId()); + } + + return $query->getSingleScalarResult() == 0 ? true : false; + } + + /** * Checks if the amount of parts is exceeded. * * @return bool diff --git a/src/PartKeepr/PartBundle/Tests/InternalPartNumberTest.php b/src/PartKeepr/PartBundle/Tests/InternalPartNumberTest.php @@ -0,0 +1,65 @@ +<?php + +namespace PartKeepr\PartBundle\Tests; + +use Doctrine\Common\DataFixtures\ProxyReferenceRepository; +use Dunglas\ApiBundle\Api\IriConverter; +use PartKeepr\CoreBundle\Tests\WebTestCase; + +class InternalPartNumberTest extends WebTestCase +{ + /** + * @var ProxyReferenceRepository + */ + protected $fixtures; + + public function setUp() + { + $this->fixtures = $this->loadFixtures( + [ + 'PartKeepr\StorageLocationBundle\DataFixtures\CategoryDataLoader', + 'PartKeepr\StorageLocationBundle\DataFixtures\StorageLocationLoader', + 'PartKeepr\PartBundle\DataFixtures\CategoryDataLoader', + 'PartKeepr\PartBundle\DataFixtures\PartDataLoader', + 'PartKeepr\ManufacturerBundle\Tests\DataFixtures\ManufacturerDataLoader', + 'PartKeepr\DistributorBundle\Tests\DataFixtures\DistributorDataLoader', + ] + )->getReferenceRepository(); + } + + public function testInternalPartNumberUniqueness() + { + $client = static::makeClient(true); + + /** + * @var $iriConverter IriConverter + */ + $iriConverter = $this->getContainer()->get('api.iri_converter'); + + $part = [ + "name" => "foobar", + "storageLocation" => $iriConverter->getIriFromItem($this->fixtures->getReference("storagelocation.first")), + "category" => $iriConverter->getIriFromItem($this->fixtures->getReference("partcategory.first")), + "internalPartNumber" => "foo123", + ]; + + $client->request('POST', '/api/parts', [], [], ['CONTENT_TYPE' => 'application/json'], + json_encode($part)); + $client->request('POST', '/api/parts', [], [], ['CONTENT_TYPE' => 'application/json'], + json_encode($part)); + + $this->assertEquals(500, $client->getResponse()->getStatusCode()); + + $response = json_decode($client->getResponse()->getContent()); + + $this->assertObjectHasAttribute("@type", $response); + $this->assertObjectHasAttribute("@context", $response); + $this->assertObjectHasAttribute('hydra:title', $response); + $this->assertObjectHasAttribute('hydra:description', $response); + + $this->assertEquals('/api/contexts/Error', $response->{'@context'}); + $this->assertEquals('Error', $response->{'@type'}); + $this->assertEquals('An error occurred', $response->{'hydra:title'}); + $this->assertEquals('The internal part number is already used', $response->{'hydra:description'}); + } +}