diff --git a/composer.json b/composer.json index be8725d..a2e5990 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ "symfony/browser-kit": "^6.2", "symfony/framework-bundle": "^6.2", "symfony/yaml": "^6.2", - "symfony/messenger": "^6.2" + "symfony/messenger": "^6.2", + "thecodingmachine/safe": "^2.5" }, "autoload": { "psr-4": { diff --git a/src/Assert/ZddMessageAsserter.php b/src/Assert/ZddMessageAsserter.php index 1faccea..c856f98 100644 --- a/src/Assert/ZddMessageAsserter.php +++ b/src/Assert/ZddMessageAsserter.php @@ -1,10 +1,12 @@ $messageFqcn - * * @throws UnableToDeserializeException */ public function assert( - string $messageFqcn, - string $serializedMessage, - PropertyList $propertyList + object $messageInstance, + ZddMessage $message, ): void { // ✅ Assert message is unserializable - $objectBefore = $this->serializer->deserialize($serializedMessage); + /** @var object $objectBefore */ + $objectBefore = $this->messageSerializer->deserialize($message->serializedMessage); - if (!$objectBefore instanceof $messageFqcn) { - throw new \LogicException(\sprintf('Class mismatch between $messageFqcn: "%s" and $serializedMessage: "%s". Please verify your integration.', $messageFqcn, $serializedMessage)); + if ($objectBefore::class !== $messageInstance::class) { + throw new \LogicException(sprintf('Class mismatch between $messageFqcn: "%s" and $serializedMessage: "%s". Please verify your integration.', $messageInstance::class, $objectBefore::class)); } - $reflection = new \ReflectionClass($messageFqcn); - $reflectionProperties = $reflection->getProperties(); + $properties = $message->properties; - // ✅ Assert property type hint has not changed and new property have a default value - foreach ($reflectionProperties as $reflectionProperty) { - // ✅ Assert error "Typed property Message::$theProperty must not be accessed before initialization". - $reflectionProperty->getValue($objectBefore); // @phpstan-ignore-line ::: Call to method ReflectionProperty::getValue() on a separate line has no effect. + $this->assertProperties($objectBefore, $properties); - // ✅ Assert property - if ($propertyList->has($reflectionProperty->getName())) { - self::assertProperty($reflectionProperty, $propertyList->get($reflectionProperty->getName()), $messageFqcn); - $propertyList->remove($reflectionProperty->getName()); - } - } - - if (0 !== $propertyList->count()) { - throw new \LogicException(\sprintf('⚠️ The properties "%s" in class "%s" seems to have been removed', implode(', ', $propertyList->getPropertiesName()), $messageFqcn)); + if ([] !== $properties) { + throw new \LogicException(sprintf('⚠️ The properties "%s" in class "%s" seems to have been removed', implode(', ', Property::getPropertyNames($properties)), $objectBefore::class)); } } - private static function assertProperty(\ReflectionProperty $reflectionProperty, Property $property, string $messageFqcn): void + /** + * @param Property[] $properties + */ + private function assertProperties(object $object, array &$properties): void { - if (null === $reflectionProperty->getType()) { - throw new \LogicException(\sprintf('$reflectionProperty::getType cannot be null')); - } - if (!$reflectionProperty->getType() instanceof \ReflectionNamedType) { - throw new \LogicException(\sprintf('$reflectionProperty::getType must be an instance of ReflectionNamedType')); - } - if ($reflectionProperty->getType()->getName() !== $property->type) { - throw new \LogicException(\sprintf('Error for property "%s" in class "%s", the type mismatch between the old and the new version of class. Please verify your integration.', $reflectionProperty->getName(), $messageFqcn)); + $reflectionClass = new \ReflectionClass($object); + + foreach ($reflectionClass->getProperties() as $reflectionProperty) { + $value = $reflectionProperty->getValue($object); + + if (null === $reflectionProperty->getType()) { // Same check as in ZddPropertyExtractor + continue; + } + + // ✅ Assert property + foreach ($properties as $key => $p) { + if ($p->name === $reflectionProperty->getName()) { + $propertyIndex = $key; + $property = $p; + } + } + + if (!isset($property, $propertyIndex)) { + throw new \LogicException(sprintf('Unable to find %s property in ZddMessage properties', $reflectionProperty->getName())); + } + + if ($reflectionProperty->getType() instanceof \ReflectionIntersectionType) { + throw new \LogicException('$reflectionProperty::getType must not be an instance of ReflectionIntersectionType'); + } + + $type = is_object($value) ? $value::class : gettype($value); + if ('NULL' !== $type && $type !== $property->type) { + throw new \LogicException(sprintf('Error for property "%s" in class "%s", the type mismatch between the old and the new version of class. Please verify your integration.', $reflectionProperty->getName(), $object::class)); + } + + $childrenProperties = $property->children; + if (is_object($value)) { + $this->assertProperties($value, $childrenProperties); + } + + if ([] === $childrenProperties) { + unset($properties[$propertyIndex]); + } } } } diff --git a/src/Command/ListZddMessageCommand.php b/src/Command/DebugZddMessageCommand.php similarity index 68% rename from src/Command/ListZddMessageCommand.php rename to src/Command/DebugZddMessageCommand.php index ce685fc..ffc4ae2 100644 --- a/src/Command/ListZddMessageCommand.php +++ b/src/Command/DebugZddMessageCommand.php @@ -1,5 +1,7 @@ setHeaderTitle('List of tracked messages for the zdd'); $table->setHeaders(['#', 'Message']); - foreach ($this->zddMessageConfig->getMessageToAssert() as $key => $message) { - $table->addRow([$key + 1, $message]); + $row = 1; + foreach ($this->config->getMessageToAssert() as $key => $_) { + $table->addRow([$row++, $key]); } $table->render(); diff --git a/src/Command/GenerateZddMessageCommand.php b/src/Command/GenerateZddMessageCommand.php index 32abc04..8811915 100644 --- a/src/Command/GenerateZddMessageCommand.php +++ b/src/Command/GenerateZddMessageCommand.php @@ -10,20 +10,19 @@ use Yousign\ZddMessageBundle\Config\ZddMessageConfigInterface; use Yousign\ZddMessageBundle\Factory\ZddMessageFactory; use Yousign\ZddMessageBundle\Filesystem\ZddMessageFilesystem; -use Yousign\ZddMessageBundle\Serializer\SerializerInterface; -#[AsCommand(name: 'yousign:zdd-message:generate', description: 'Generate serialized version of managed messages to validate them afterwards.')] +#[AsCommand( + name: 'yousign:zdd-message:generate', + description: 'Generate serialized version of managed messages to validate them afterwards.', +)] final class GenerateZddMessageCommand extends Command { - private ZddMessageFactory $zddMessageFactory; - private ZddMessageFilesystem $zddMessageFilesystem; - - public function __construct(private readonly string $zddMessagePath, private readonly ZddMessageConfigInterface $zddMessageConfig, SerializerInterface $serializer) - { + public function __construct( + private readonly ZddMessageConfigInterface $config, + private readonly ZddMessageFactory $messageFactory, + private readonly ZddMessageFilesystem $filesystem, + ) { parent::__construct(); - - $this->zddMessageFactory = new ZddMessageFactory($zddMessageConfig, $serializer); - $this->zddMessageFilesystem = new ZddMessageFilesystem($this->zddMessagePath); } public function execute(InputInterface $input, OutputInterface $output): int @@ -33,12 +32,12 @@ public function execute(InputInterface $input, OutputInterface $output): int $table = $io->createTable(); $table->setHeaders(['#', 'Message']); - foreach ($this->zddMessageConfig->getMessageToAssert() as $key => $messageFqcn) { - $zddMessage = $this->zddMessageFactory->create($messageFqcn); - - $this->zddMessageFilesystem->write($zddMessage); + $row = 1; + foreach ($this->config->getMessageToAssert() as $key => $instance) { + $message = $this->messageFactory->create($key, $instance); + $this->filesystem->write($message); - $table->addRow([$key + 1, $messageFqcn]); + $table->addRow([$row++, $key]); } $table->render(); diff --git a/src/Command/ValidateZddMessageCommand.php b/src/Command/ValidateZddMessageCommand.php index 6d78c4b..c1fa0b0 100644 --- a/src/Command/ValidateZddMessageCommand.php +++ b/src/Command/ValidateZddMessageCommand.php @@ -1,5 +1,7 @@ zddMessageFactory = new ZddMessageFactory($zddMessageConfig, $serializer); - $this->zddMessageFilesystem = new ZddMessageFilesystem($this->zddMessagePath); - $this->zddMessageAsserter = new ZddMessageAsserter($serializer); } public function execute(InputInterface $input, OutputInterface $output): int @@ -37,21 +34,24 @@ public function execute(InputInterface $input, OutputInterface $output): int $table->setHeaders(['#', 'Message', 'ZDD Compliant?']); $errorCount = 0; - foreach ($this->zddMessageConfig->getMessageToAssert() as $key => $messageFqcn) { - if (false === $this->zddMessageFilesystem->exists($messageFqcn)) { - // It happens on newly added message, the trade-off here is to validate itself on current version - $zddMessage = $this->zddMessageFactory->create($messageFqcn); - $this->zddMessageFilesystem->write($zddMessage); + + $row = 1; + foreach ($this->config->getMessageToAssert() as $name => $instance) { + if (false === $this->filesystem->exists($name)) { + $message = $this->messageFactory->create( + $name, + $instance, + ); + $this->filesystem->write($message); } - $messageToAssert = $this->zddMessageFilesystem->read($messageFqcn); + $messageToAssert = $this->filesystem->read($name); try { - $this->zddMessageAsserter->assert($messageFqcn, $messageToAssert->serializedMessage(), $messageToAssert->propertyList()); - - $table->addRow([$key + 1, $messageFqcn, 'Yes ✅']); + $this->messageAsserter->assert($instance, $messageToAssert); + $table->addRow([$row++, $name, 'Yes ✅']); } catch (\Throwable $e) { - $table->addRow([$key + 1, $messageFqcn, 'No ❌']); + $table->addRow([$row++, $name, 'No ❌']); ++$errorCount; } } diff --git a/src/Config/ZddMessageConfigInterface.php b/src/Config/ZddMessageConfigInterface.php index 4e70ae1..4d01b51 100644 --- a/src/Config/ZddMessageConfigInterface.php +++ b/src/Config/ZddMessageConfigInterface.php @@ -5,50 +5,15 @@ interface ZddMessageConfigInterface { /** - * The list of FQCN message to assert. + * The method should generate the list of message instances to assert. * - * @example getMessageToAssert(): array - * { - * return [ - * 'App\Message\MyMessage', - * 'App\Message\MyOtherMessage' - * ]; - * } - * - * @return array - */ - public function getMessageToAssert(): array; - - /** - * Provide a fake value for each custom property type used in your messages. - * You can also override the fake value used for scalar types. - * - * @example - * Suppose you have message which contains an object as property type: - * - * class MyMessage - * { - * private MyObject $object; - * // ... - * } - * - * class MyObject - * { - * private string $content; - * // ... - * } - * - * The implementation of generateValueForCustomPropertyType should be like this: - * - * public function generateValueForCustomPropertyType(string $type): array; + * @example getMessageToAssert(): \Generator * { - * return match($type) { - * 'Namespace\MyObject' => new MyObject("Hi!"), - * default => null, - * }; + * yield App\Message\MyMessage::class => new App\Message\MyMessage(), + * yield App\Message\MyOtherMessage::class => new App\Message\MyOtherMessage(), * } * - * @see MessageConfig in ZddMessageFakerTest.php for a concret examples + * @return \Generator */ - public function generateValueForCustomPropertyType(string $type): mixed; + public function getMessageToAssert(): \Generator; } diff --git a/src/Factory/Property.php b/src/Factory/Property.php index 0ecd324..fe671e9 100644 --- a/src/Factory/Property.php +++ b/src/Factory/Property.php @@ -1,10 +1,72 @@ name.':'.$this->type; + + if ([] !== $this->children) { + $fingerprint .= '('; + + $childrenCount = count($this->children); + + foreach ($this->children as $index => $property) { + $fingerprint .= $property->getFingerprint(); + + if ($index < $childrenCount - 1) { + $fingerprint .= ','; + } + } + + $fingerprint .= ')'; + } + + return $fingerprint; + } + + /** + * @param self[] $properties + * + * @return string[] + */ + public static function getPropertyNames(array $properties): array + { + return array_map( + static fn (self $property) => $property->name, + $properties, + ); + } + + public function jsonSerialize(): array // @phpstan-ignore-line + { + return [ + 'name' => $this->name, + 'type' => $this->type, + 'children' => $this->children, + ]; + } + + public static function fromArray(array $data): self // @phpstan-ignore-line { + return new self( + $data['name'], + $data['type'], + array_map(static fn (array $p) => Property::fromArray($p), $data['children']), + ); } } diff --git a/src/Factory/PropertyList.php b/src/Factory/PropertyList.php deleted file mode 100644 index 10d7e23..0000000 --- a/src/Factory/PropertyList.php +++ /dev/null @@ -1,102 +0,0 @@ -properties[$property->name] = $property; - } - } - - public function addProperty(Property $property): void - { - $this->properties[$property->name] = $property; - } - - public static function fromJson(string $data): self - { - /** @var array> $decodedProperties */ - $decodedProperties = \json_decode($data, true); - $properties = []; - foreach ($decodedProperties as $decodedProperty) { - $name = $decodedProperty['name'] ?? null; - $type = $decodedProperty['type'] ?? null; - - if (null === $name || null === $type) { - throw new \LogicException(sprintf('Missing keys name and/or type in decoded properties from data: "%s"', $data)); - } - $properties[] = new Property($decodedProperty['name'], $decodedProperty['type'], null); - } - - return new self($properties); - } - - /** - * @return array - */ - public function getPropertiesName(): array - { - return array_keys($this->properties); - } - - /** - * @return Property[] - */ - public function getProperties(): array - { - return $this->properties; - } - - public function has(string $name): bool - { - return array_key_exists($name, $this->properties); - } - - public function get(string $name): Property - { - $property = $this->properties[$name] ?? null; - - if (null === $property) { - throw new \LogicException(sprintf('No property "%s" found in the properties list', $name)); - } - - return $property; - } - - public function remove(string $name): void - { - unset($this->properties[$name]); - } - - public function count(): int - { - return count($this->properties); - } - - public function toJson(): string - { - $data = []; - foreach ($this->properties as $property) { - $data[] = [ - 'name' => $property->name, - 'type' => $property->type, - ]; - } - - return json_encode($data, JSON_THROW_ON_ERROR); - } -} diff --git a/src/Factory/ZddMessage.php b/src/Factory/ZddMessage.php index 591f2cf..f36ee32 100644 --- a/src/Factory/ZddMessage.php +++ b/src/Factory/ZddMessage.php @@ -5,33 +5,58 @@ /** * @internal */ -final class ZddMessage +final class ZddMessage implements \JsonSerializable { + /** + * @param Property[] $properties + */ public function __construct( - private readonly string $messageFqcn, - private readonly string $serializedMessage, - private readonly PropertyList $propertyList, - private readonly ?object $message = null, + public readonly string $name, + public readonly string $type, + public readonly string $serializedMessage, + public readonly array $properties, ) { } - public function message(): ?object + public function getFingerprint(): string { - return $this->message; - } + $fingerprint = $this->type.'('; - public function serializedMessage(): string - { - return $this->serializedMessage; + $childrenCount = count($this->properties); + + foreach ($this->properties as $index => $property) { + $fingerprint .= $property->getFingerprint(); + + if ($index < $childrenCount - 1) { + $fingerprint .= ','; + } + } + + $fingerprint .= ')'; + + return $fingerprint; } - public function propertyList(): PropertyList + public function jsonSerialize(): array // @phpstan-ignore-line { - return $this->propertyList; + return [ + 'name' => $this->name, + 'type' => $this->type, + 'serialized_message' => $this->serializedMessage, + 'properties' => $this->properties, + ]; } - public function messageFqcn(): string + public static function fromArray(array $data): self // @phpstan-ignore-line { - return $this->messageFqcn; + return new self( + $data['name'], + $data['type'], + $data['serialized_message'], + array_map( + static fn (array $p) => Property::fromArray($p), + $data['properties'], + ), + ); } } diff --git a/src/Factory/ZddMessageCollection.php b/src/Factory/ZddMessageCollection.php new file mode 100644 index 0000000..bab3692 --- /dev/null +++ b/src/Factory/ZddMessageCollection.php @@ -0,0 +1,40 @@ +getMessageToAssert() as $name => $message) { + $messages[] = $messageFactory->create($name, $message); + } + + $this->messages = $messages; + } + + public function fingerprintExists(string $fingerprint): bool + { + return in_array( + $fingerprint, + array_map(static fn (ZddMessage $message) => $message->getFingerprint(), $this->messages), + true, + ); + } +} diff --git a/src/Factory/ZddMessageFactory.php b/src/Factory/ZddMessageFactory.php index 8d7cf87..653b680 100644 --- a/src/Factory/ZddMessageFactory.php +++ b/src/Factory/ZddMessageFactory.php @@ -4,44 +4,26 @@ namespace Yousign\ZddMessageBundle\Factory; -use Yousign\ZddMessageBundle\Config\ZddMessageConfigInterface; -use Yousign\ZddMessageBundle\Serializer\SerializerInterface; +use Yousign\ZddMessageBundle\Serializer\MessageSerializerInterface; /** * @internal */ final class ZddMessageFactory { - private ZddPropertyExtractor $propertyExtractor; - - public function __construct(ZddMessageConfigInterface $config, private readonly SerializerInterface $serializer) - { - $this->propertyExtractor = new ZddPropertyExtractor($config); + public function __construct( + private readonly MessageSerializerInterface $serializer, + private readonly ZddPropertyExtractor $propertyExtractor, + ) { } - /** - * @param class-string $className - */ - public function create(string $className): ZddMessage + public function create(string $messageName, object $message): ZddMessage { - $propertyList = $this->propertyExtractor->extractPropertiesFromClass($className); - - $message = (new \ReflectionClass($className))->newInstanceWithoutConstructor(); - foreach ($propertyList->getProperties() as $property) { - $this->forcePropertyValue($message, $property->name, $property->value); - } - - $serializedMessage = $this->serializer->serialize($message); - - return new ZddMessage($className, $serializedMessage, $propertyList, $message); - } - - private function forcePropertyValue(object $object, string $property, mixed $value): void - { - $reflectionClass = new \ReflectionClass($object); - $reflectionProperty = $reflectionClass->getProperty($property); - - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($object, $value); + return new ZddMessage( + $messageName, + $message::class, + $this->serializer->serialize($message), + $this->propertyExtractor->extractProperties($message), + ); } } diff --git a/src/Factory/ZddPropertyExtractor.php b/src/Factory/ZddPropertyExtractor.php index 38d1f5b..54fe22a 100644 --- a/src/Factory/ZddPropertyExtractor.php +++ b/src/Factory/ZddPropertyExtractor.php @@ -1,70 +1,42 @@ getProperties() as $property) { - $propertyName = $property->getName(); - $propertyType = $property->getType(); + $properties = []; - if (null === $propertyType) { - throw InvalidTypeException::typeMissing($propertyName, $className); + foreach ($reflectionClass->getProperties() as $reflectionProperty) { + if (null === $reflectionProperty->getType()) { + continue; } - if (!$propertyType instanceof \ReflectionNamedType) { + if ($reflectionProperty->getType() instanceof \ReflectionIntersectionType) { throw InvalidTypeException::typeNotSupported(); } - $typeHint = $propertyType->getName(); - $value = $propertyType->allowsNull() ? null : $this->generateFakeValueFromType($typeHint); - $propertyList->addProperty(new Property($propertyName, $typeHint, $value)); - } - - return $propertyList; - } - - /** - * @throws MissingValueForTypeException - */ - private function generateFakeValueFromType(string $typeHint): mixed - { - $value = $this->config->generateValueForCustomPropertyType($typeHint); - if (null !== $value) { - return $value; + $value = $reflectionProperty->getValue($object); + $properties[] = new Property( + $reflectionProperty->getName(), + is_object($value) ? $value::class : gettype($value), + is_object($value) ? $this->extractProperties($value) : [], + ); } - return match ($typeHint) { - 'string' => 'Hello World!', - 'int' => 42, - 'float' => 42.42, - 'bool' => true, - 'array' => ['PHP', 'For The Win'], - default => throw MissingValueForTypeException::missingValue($typeHint, $this->config), - }; + return $properties; } } diff --git a/src/Filesystem/ZddMessageFilesystem.php b/src/Filesystem/ZddMessageFilesystem.php index f0af3fa..cd4c455 100644 --- a/src/Filesystem/ZddMessageFilesystem.php +++ b/src/Filesystem/ZddMessageFilesystem.php @@ -4,7 +4,7 @@ namespace Yousign\ZddMessageBundle\Filesystem; -use Yousign\ZddMessageBundle\Factory\PropertyList; +use Yousign\ZddMessageBundle\Factory\Property; use Yousign\ZddMessageBundle\Factory\ZddMessage; /** @@ -12,87 +12,95 @@ */ final class ZddMessageFilesystem { - public function __construct(private readonly string $zddPath) - { + public function __construct( + private readonly string $path, + ) { } - public function write(ZddMessage $zddMessage): void + public function write(ZddMessage $message): void { - $basePath = $this->getBasePath($zddMessage->messageFqcn()); + $basePath = $this->getBasePath($message->name); if (false === file_exists($basePath)) { if (!mkdir($basePath, recursive: true) && !is_dir($basePath)) { throw new \RuntimeException(\sprintf('Unable to create directory "%s"', $basePath)); } } - $serializedMessagePath = $this->getPathToSerializedMessage($zddMessage->messageFqcn()); - $byteWrittenInTxt = \file_put_contents($serializedMessagePath, $zddMessage->serializedMessage()); + $serializedMessagePath = $this->getPathToSerializedMessage($message->name); + $byteWrittenInTxt = \file_put_contents($serializedMessagePath, $message->serializedMessage); if (false === $byteWrittenInTxt || 0 === $byteWrittenInTxt) { throw new \RuntimeException(\sprintf('Unable to write file "%s"', $serializedMessagePath)); } - $propertiesPath = $this->getPathToProperties($zddMessage->messageFqcn()); - $byteWrittenInJson = \file_put_contents($propertiesPath, $zddMessage->propertyList()->toJson()); + $propertiesPath = $this->getPathToProperties($message->name); + $byteWrittenInJson = \file_put_contents( + $propertiesPath, + json_encode($message->properties, JSON_THROW_ON_ERROR), + ); if (false === $byteWrittenInJson || 0 === $byteWrittenInJson) { throw new \RuntimeException(\sprintf('Unable to write file "%s"', $propertiesPath)); } } - public function read(string $messageFqcn): ZddMessage + public function read(string $messageName): ZddMessage { - $serializedMessagePath = $this->getPathToSerializedMessage($messageFqcn); + $serializedMessagePath = $this->getPathToSerializedMessage($messageName); if (false === $serializedMessage = \file_get_contents($serializedMessagePath)) { throw new \RuntimeException(\sprintf('Unable to read file "%s"', $serializedMessagePath)); } - $propertiesPath = $this->getPathToProperties($messageFqcn); - if (false === $properties = \file_get_contents($propertiesPath)) { + $propertiesPath = $this->getPathToProperties($messageName); + if (false === $propertiesJson = \file_get_contents($propertiesPath)) { throw new \RuntimeException(\sprintf('Unable to read file "%s"', $propertiesPath)); } - $propertyList = PropertyList::fromJson($properties); + /** @var Property[] $properties */ + $properties = array_map( + static fn (array $p) => Property::fromArray($p), // @phpstan-ignore-line + json_decode($propertiesJson, true) // @phpstan-ignore-line + ); - return new ZddMessage($messageFqcn, $serializedMessage, $propertyList); + return new ZddMessage($messageName, $messageName, $serializedMessage, $properties); // TODO: Check 2nd parameter value } - public function exists(string $messageFqcn): bool + public function exists(string $messageName): bool { - $serializedMessagePath = $this->getPathToSerializedMessage($messageFqcn); + $serializedMessagePath = $this->getPathToSerializedMessage($messageName); return file_exists($serializedMessagePath); } /** - * @return array + * @return array */ - private function getDirectoryAndShortname(string $classFqcn): array + private function getDirectoryAndShortname(string $messageName): array { - $path = explode('\\', $classFqcn); + $path = explode('\\', $messageName); $shortName = end($path); array_pop($path); - $directory = implode('/', $path); + $directory = implode(DIRECTORY_SEPARATOR, $path); return [$directory, $shortName]; } - private function getPathToSerializedMessage(string $messageFqcn): string + private function getPathToSerializedMessage(string $messageName): string { - [$directory, $shortName] = $this->getDirectoryAndShortname($messageFqcn); + [$directory, $shortName] = $this->getDirectoryAndShortname($messageName); - return $this->zddPath.'/'.$directory.'/'.$shortName.'.txt'; + return $this->path.DIRECTORY_SEPARATOR.$directory.DIRECTORY_SEPARATOR.$shortName.'.txt'; } - private function getPathToProperties(string $messageFqcn): string + private function getPathToProperties(string $messageName): string { - [$directory, $shortName] = $this->getDirectoryAndShortname($messageFqcn); + [$directory, $shortName] = $this->getDirectoryAndShortname($messageName); - return $this->zddPath.'/'.$directory.'/'.$shortName.'.properties.json'; + return $this->path.'/'.$directory.'/'.$shortName.'.properties.json'; } - private function getBasePath(string $messageFqcn): string + private function getBasePath(string $messageName): string { - [$directory] = $this->getDirectoryAndShortname($messageFqcn); + [$directory] = $this->getDirectoryAndShortname($messageName); - return $this->zddPath.'/'.$directory; + return $this->path.DIRECTORY_SEPARATOR.$directory; } } diff --git a/src/Listener/Symfony/MessengerListener.php b/src/Listener/Symfony/MessengerListener.php index 6eed485..1dc3def 100644 --- a/src/Listener/Symfony/MessengerListener.php +++ b/src/Listener/Symfony/MessengerListener.php @@ -5,14 +5,16 @@ use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent; -use Yousign\ZddMessageBundle\Config\ZddMessageConfigInterface; +use Yousign\ZddMessageBundle\Factory\ZddMessageCollection; +use Yousign\ZddMessageBundle\Factory\ZddMessageFactory; final class MessengerListener implements EventSubscriberInterface { public function __construct( private readonly LoggerInterface $logger, - private readonly ZddMessageConfigInterface $config, - private readonly string $logLevel = 'warning' + private readonly ZddMessageFactory $messageFactory, + private readonly ZddMessageCollection $zddMessageCollection, + private readonly string $logLevel = 'warning', ) { } @@ -20,17 +22,27 @@ public function onMessageReceived(WorkerMessageReceivedEvent $event): void { try { $message = $event->getEnvelope()->getMessage(); + // In case of $message act like an envelope. if (method_exists($message, 'getMessage')) { $message = $message->getMessage(); } - if (is_object($message) && !in_array($class = get_class($message), $this->config->getMessageToAssert(), true)) { + if (!is_object($message)) { + return; + } + + $zddMessage = $this->messageFactory->create( + $message::class, + $message, + ); + + if (!$this->zddMessageCollection->fingerprintExists($zddMessage->getFingerprint())) { $this->logger->log( $this->logLevel, 'Untracked {class} has been detected, add it in your configuration to ensure ZDD compliance.', [ - 'class' => $class, + 'class' => $message::class, ], ); } diff --git a/src/Serializer/SerializerInterface.php b/src/Serializer/MessageSerializerInterface.php similarity index 87% rename from src/Serializer/SerializerInterface.php rename to src/Serializer/MessageSerializerInterface.php index 3211c82..7288b52 100644 --- a/src/Serializer/SerializerInterface.php +++ b/src/Serializer/MessageSerializerInterface.php @@ -4,7 +4,7 @@ namespace Yousign\ZddMessageBundle\Serializer; -interface SerializerInterface +interface MessageSerializerInterface { public function serialize(object $data): string; diff --git a/src/Serializer/ZddMessageMessengerSerializer.php b/src/Serializer/ZddMessageMessengerSerializer.php index 36e9ae4..7ca7be2 100644 --- a/src/Serializer/ZddMessageMessengerSerializer.php +++ b/src/Serializer/ZddMessageMessengerSerializer.php @@ -6,7 +6,7 @@ use Symfony\Component\Messenger\Exception\MessageDecodingFailedException; use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface as MessengerSerializerInterface; -class ZddMessageMessengerSerializer implements SerializerInterface +class ZddMessageMessengerSerializer implements MessageSerializerInterface { public function __construct(private readonly MessengerSerializerInterface $serializer) { diff --git a/src/ZddMessageBundle.php b/src/ZddMessageBundle.php index d63148b..cd73b3f 100644 --- a/src/ZddMessageBundle.php +++ b/src/ZddMessageBundle.php @@ -1,5 +1,7 @@ defaults() ->autowire() ->autoconfigure() + ->load('Yousign\\ZddMessageBundle\\', __DIR__) ; $messageConfigServiceId = $config['message_config_service']; @@ -58,8 +59,7 @@ public function loadExtension(array $config, ContainerConfigurator $container, C throw new \LogicException(sprintf('You should configure zdd_message.message_config_service with a service that implements %s', ZddMessageConfigInterface::class)); } - $serviceConfigurator->bind('$zddMessageConfig', service($messageConfigServiceId)); - $serviceConfigurator->bind('$zddMessagePath', $config['serialized_messages_dir'] ?? $this->getDefaultPath($builder)); + $serviceConfigurator->bind('$config', service($messageConfigServiceId)); $messengerEnable = $config['log_untracked_messages']['messenger']['enable'] ?? false; if ($messengerEnable) { @@ -77,10 +77,9 @@ public function loadExtension(array $config, ContainerConfigurator $container, C } $serviceConfigurator - ->set(SerializerInterface::class, $config['serializer']) - ->set(GenerateZddMessageCommand::class) - ->set(ValidateZddMessageCommand::class) - ->set(ListZddMessageCommand::class) + ->set(ZddMessageFilesystem::class) + ->arg('$path', $config['serialized_messages_dir'] ?? $this->getDefaultPath($builder)) + ->set(MessageSerializerInterface::class, $config['serializer']) ; } diff --git a/tests/Fixtures/App/Messages/Command.php b/tests/Fixtures/App/Messages/Command.php new file mode 100644 index 0000000..39dbbdd --- /dev/null +++ b/tests/Fixtures/App/Messages/Command.php @@ -0,0 +1,14 @@ + new Locale('fr'), - 'Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\Input\Status' => Status::DRAFT, - DummyMessageWithNullableNumberProperty::class => new DummyMessageWithNullableNumberProperty('content'), - default => null, - }; + return; + } + + yield DummyMessage::class => new DummyMessage( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac volutpat nisl.', + ); + + yield DummyMessageWithNullableNumberProperty::class => new DummyMessageWithNullableNumberProperty( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac volutpat nisl.', + ); + + yield DummyMessageWithPrivateConstructor::class => DummyMessageWithPrivateConstructor::create( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac volutpat nisl.', + ); + + yield DummyMessageWithSafeDateTimeImmutable::class => new DummyMessageWithSafeDateTimeImmutable( + new DateTimeImmutable('2021-01-01T00:00:00+00:00'), + ); + + yield DummyMessageWithAllManagedTypes::class => new DummyMessageWithAllManagedTypes( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac volutpat nisl.', + 1, + true, + ['key' => 'value'], + new Locale('fr'), + Status::DRAFT, + ); + + yield Other\DummyMessage::class => new Other\DummyMessage([ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac volutpat nisl.', + ]); } public static function reset(): void diff --git a/tests/Fixtures/App/Messages/DummyMessageWithSafeDateTimeImmutable.php b/tests/Fixtures/App/Messages/DummyMessageWithSafeDateTimeImmutable.php new file mode 100644 index 0000000..97e9de5 --- /dev/null +++ b/tests/Fixtures/App/Messages/DummyMessageWithSafeDateTimeImmutable.php @@ -0,0 +1,20 @@ +occuredAt = $occuredAt; + } + + public function getOccuredAt(): DateTimeImmutable + { + return $this->occuredAt; + } +} diff --git a/tests/Fixtures/App/Messages/EnumInt.php b/tests/Fixtures/App/Messages/EnumInt.php new file mode 100644 index 0000000..b8e7c4e --- /dev/null +++ b/tests/Fixtures/App/Messages/EnumInt.php @@ -0,0 +1,9 @@ +command = new CommandTester((new Application($kernel))->find('yousign:zdd-message:generate')); - $customBasePathFile = $kernel->getContainer()->getParameter('custom_path_file'); - $this->serializedMessagesDir = $customBasePathFile.'/Yousign/ZddMessageBundle/Tests/Fixtures/App/Messages'; + $this->serializedMessagesDir = __DIR__.'/../Fixtures/App/tmp/serialized_messages_directory'; } protected function tearDown(): void @@ -34,12 +33,14 @@ protected function tearDown(): void public function testThatCommandIsSuccessful(): void { - $this->assertDirectoryDoesNotExist($this->serializedMessagesDir); + $baseDirectory = $this->serializedMessagesDir.'/Yousign/ZddMessageBundle/Tests/Fixtures/App/Messages'; + + $this->assertDirectoryDoesNotExist($baseDirectory); $this->command->execute([]); - $this->assertDirectoryExists($this->serializedMessagesDir); - $this->assertSerializedFilesExist($this->serializedMessagesDir); + $this->assertDirectoryExists($baseDirectory); + $this->assertSerializedFilesExist($baseDirectory); $expectedResult = <<command = new CommandTester((new Application(self::$kernel))->find('yousign:zdd-message:validate')); - $customBasePathFile = $kernel->getContainer()->getParameter('custom_path_file'); - $this->serializedMessagesDir = $customBasePathFile.'/Yousign/ZddMessageBundle/Tests/Fixtures/App/Messages'; - MessageConfig::$messagesToAssert = [ - DummyMessage::class, - ]; + $kernel = self::bootKernel(); + $this->generateCommand = new CommandTester((new Application($kernel))->find('yousign:zdd-message:generate')); + $this->validateCommand = new CommandTester((new Application($kernel))->find('yousign:zdd-message:validate')); + $this->serializedMessagesDir = __DIR__.'/../Fixtures/App/tmp/serialized_messages_directory'; } protected function tearDown(): void @@ -36,7 +32,6 @@ protected function tearDown(): void parent::tearDown(); (new Filesystem())->remove($this->serializedMessagesDir); - MessageConfig::reset(); } public function getSerializer(): ZddMessageMessengerSerializer @@ -46,76 +41,81 @@ public function getSerializer(): ZddMessageMessengerSerializer public function testThatCommandIsSuccessful(): void { - mkdir($this->serializedMessagesDir); - file_put_contents($this->serializedMessagesDir.'/DummyMessage.txt', $this->getSerializer()->serialize(new DummyMessage('Hi'))); - file_put_contents($this->serializedMessagesDir.'/DummyMessage.properties.json', '[{"name":"content","type":"string"}]'); - $this->assertSerializedFilesExist($this->serializedMessagesDir); + $this->generateCommand->execute([]); - $this->command->execute([]); - $this->command->assertCommandIsSuccessful(); + $this->validateCommand->execute([]); + $this->validateCommand->assertCommandIsSuccessful(); $expectedResult = <<assertSame(trim($expectedResult), trim($this->command->getDisplay())); + $this->assertSame(trim($expectedResult), trim($this->validateCommand->getDisplay())); } public function testThatCommandIsSuccessfulEvenIfTheSerializedMessageDoesNotExists(): void { - $this->assertFileDoesNotExist($this->serializedMessagesDir.'/DummyMessage.txt'); - - $this->command->execute([]); - - $this->command->assertCommandIsSuccessful(); + $this->validateCommand->execute([]); + $this->validateCommand->assertCommandIsSuccessful(); $expectedResult = <<assertSame(trim($expectedResult), trim($this->command->getDisplay())); + $this->assertSame(trim($expectedResult), trim($this->validateCommand->getDisplay())); } public function testThatCommandFailsWhenMessageIsNotZddCompliant(): void { + $baseDirectory = $this->serializedMessagesDir.'/Yousign/ZddMessageBundle/Tests/Fixtures/App/Messages'; $serializedMessage = $this->getSerializedMessageForPreviousVersionOfDummyMessageWithNumberProperty(); - $data = [ - [ - 'name' => 'content', - 'type' => 'string', - ], - [ - 'name' => 'number', - 'type' => 'int', - ], - ]; - mkdir($this->serializedMessagesDir); - file_put_contents($this->serializedMessagesDir.'/DummyMessage.txt', $serializedMessage); - file_put_contents($this->serializedMessagesDir.'/DummyMessage.properties.json', json_encode($data)); - $this->assertSerializedFilesExist($this->serializedMessagesDir); - - $this->command->execute([]); - - self::assertEquals(Command::FAILURE, $this->command->getStatusCode()); + + $this->generateCommand->execute([]); + + file_put_contents($baseDirectory.'/DummyMessage.txt', $serializedMessage); + file_put_contents($baseDirectory.'/DummyMessage.properties.json', json_encode([ + new Property('content', 'string', []), + new Property('number', 'int', []), + ])); + + $this->validateCommand->execute([]); + $this->validateCommand->execute([]); + + self::assertEquals(Command::FAILURE, $this->validateCommand->getStatusCode()); $expectedResult = <<assertSame(trim($expectedResult), trim($this->command->getDisplay())); + $this->assertSame(trim($expectedResult), trim($this->validateCommand->getDisplay())); } private function getSerializedMessageForPreviousVersionOfDummyMessageWithNumberProperty(): string @@ -125,15 +125,4 @@ private function getSerializedMessageForPreviousVersionOfDummyMessageWithNumberP O:65:"Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessage":1:{s:74:" Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessage content";s:11:"Hello world";} TXT; } - - private function assertSerializedFilesExist(string $baseDirectory): void - { - /** @var ZddMessageConfigInterface $messageConfig */ - $messageConfig = self::$kernel->getContainer()->get(MessageConfig::class); - foreach ($messageConfig->getMessageToAssert() as $message) { - $shortName = (new \ReflectionClass($message))->getShortName(); - $this->assertFileExists($baseDirectory.'/'.$shortName.'.txt'); - $this->assertFileExists($baseDirectory.'/'.$shortName.'.properties.json'); - } - } } diff --git a/tests/Unit/Assert/ZddMessageAsserterTest.php b/tests/Unit/Assert/ZddMessageAsserterTest.php new file mode 100644 index 0000000..e53d6c7 --- /dev/null +++ b/tests/Unit/Assert/ZddMessageAsserterTest.php @@ -0,0 +1,165 @@ +getSut(); + $sut->assert($instance, $zddMessage); + self::assertTrue(true); // if we reached this statement, no exception has been thrown => OK test + } + + public function provideValidAssertion(): iterable + { + yield 'Unchanged message' => [ + $instance = new DummyMessage('Hello world'), + new ZddMessage( + DummyMessage::class, + DummyMessage::class, + $this->getSerializer()->serialize($instance), + [ + new Property('content', 'string', []), + ], + ), + ]; + + yield 'Number property has been switched to nullable' => [ + $instance = new DummyMessageWithNullableNumberProperty('Hello world'), + new ZddMessage( + DummyMessageWithNullableNumberProperty::class, + DummyMessageWithNullableNumberProperty::class, + $this->getSerializer()->serialize($instance), + [ + new Property('content', 'string', []), + new Property('number', 'integer', []), + ], + ), + ]; + } + + public function testItThrowsExceptionWhenAPropertyHasBeenRemoved(): void + { + $instance = new DummyMessage('Hello world'); + $zddMessage = new ZddMessage( + DummyMessage::class, + DummyMessage::class, + $this->getSerializer()->serialize($instance), + [ + new Property('content', 'string', []), + new Property('number', 'integer', []), + ], + ); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('⚠️ The properties "number" in class "Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessage" seems to have been removed'); + + $sut = $this->getSut(); + $sut->assert($instance, $zddMessage); + } + + public function testItThrowsExceptionForInvalidIntegrationDueToClassMismatch(): void + { + $instance = new StdClass(); + $zddMessage = new ZddMessage( + DummyMessage::class, + DummyMessage::class, + $this->getSerializer()->serialize(new DummyMessage('Hello world')), + [ + new Property('content', 'string', []), + ], + ); + + $sut = $this->getSut(); + + try { + $sut->assert($instance, $zddMessage); + $this->fail('This test should raised expected exception'); + } catch (Throwable $t) { + $this->assertInstanceOf(LogicException::class, $t); + $this->assertStringContainsString('Class mismatch', $t->getMessage()); + $this->assertStringContainsString(DummyMessage::class, $t->getMessage()); + } + } + + public function testItThrowsExceptionForInvalidIntegrationDueToPropertyTypeMismatch(): void + { + $instance = new DummyMessage('Hello world'); + $zddMessage = new ZddMessage( + DummyMessage::class, + DummyMessage::class, + $this->getSerializer()->serialize($instance), + [ + new Property('content', 'integer', []), // Simulate error using 'int' typeHint instead of 'string' + ], + ); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Error for property "content" in class "Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessage", the type mismatch between the old and the new version of class. Please verify your integration.'); + + $sut = $this->getSut(); + $sut->assert($instance, $zddMessage); + } + + public function testItThrowsExceptionForInvalidPropertyType(): void + { + $instance = new DummyMessageWithWrongPropertyType(22); + + $serializedMessage = $this->getSerializer()->serialize($instance); + $serializedMessage = str_replace('DummyMessageWithWrongPropertyType', 'DummyMessage', $serializedMessage); + + $zddMessage = new ZddMessage( + DummyMessage::class, + DummyMessage::class, + $serializedMessage, + [ + new Property('content', 'integer', []), // Simulate error using 'int' typeHint instead of 'string' + ], + ); + + $sut = $this->getSut(); + + try { + $sut->assert($instance, $zddMessage); + } catch (UnableToDeserializeException $e) { + $this->assertInstanceOf(MessageDecodingFailedException::class, $e->getPrevious()); + + return; + } + + $this->fail('This test should raised expected exception'); + } + + public function getSut(): ZddMessageAsserter + { + return new ZddMessageAsserter($this->getSerializer()); + } +} diff --git a/tests/Unit/Factory/PropertyTest.php b/tests/Unit/Factory/PropertyTest.php new file mode 100644 index 0000000..2269412 --- /dev/null +++ b/tests/Unit/Factory/PropertyTest.php @@ -0,0 +1,107 @@ +assertEquals($property, $decodedMessage); + } + + /** + * @dataProvider provideProperties + */ + public function testGetFingerprint(Property $property, string $expectedFingerprint): void + { + // When + $fingerprint = $property->getFingerprint(); + + // Then + $this->assertEquals($expectedFingerprint, $fingerprint); + } + + public function provideProperties(): iterable + { + yield [ + new Property('myString', 'string', []), + 'myString:string', + ]; + + yield [ + new Property('myString', EnumString::class, [ + new Property('name', 'string', []), + new Property('value', 'string', []), + ]), + 'myString:Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\EnumString(name:string,value:string)', + ]; + + yield [ + new Property('myInt', EnumInt::class, [ + new Property('name', 'string', []), + new Property('value', 'int', []), + ]), + 'myInt:Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\EnumInt(name:string,value:int)', + ]; + + yield [ + new Property('command', Command::class, [ + new Property('name', 'string', []), + new Property('myString', EnumString::class, [ + new Property('name', 'string', []), + new Property('value', 'string', []), + ]), + new Property('myInt', EnumInt::class, [ + new Property('name', 'string', []), + new Property('value', 'int', []), + ]), + ]), + 'command:Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\Command(name:string,myString:Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\EnumString(name:string,value:string),myInt:Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\EnumInt(name:string,value:int))', + ]; + + yield [ + new Property('wrapper', Wrapper::class, [ + new Property('command', Command::class, [ + new Property('name', 'string', []), + new Property('myString', EnumString::class, [ + new Property('name', 'string', []), + new Property('value', 'string', []), + ]), + new Property('myInt', EnumInt::class, [ + new Property('name', 'string', []), + new Property('value', 'int', []), + ]), + ]), + ]), + 'wrapper:Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\Wrapper(command:Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\Command(name:string,myString:Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\EnumString(name:string,value:string),myInt:Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\EnumInt(name:string,value:int)))', + ]; + } +} diff --git a/tests/Unit/Factory/ZddMessageCollectionTest.php b/tests/Unit/Factory/ZddMessageCollectionTest.php new file mode 100644 index 0000000..ffb1d41 --- /dev/null +++ b/tests/Unit/Factory/ZddMessageCollectionTest.php @@ -0,0 +1,54 @@ + $this->message1; + } + }, + $factory = new ZddMessageFactory( + $this->getSerializer(), + new ZddPropertyExtractor(), + ), + ); + + $zddMessage1 = $factory->create($message1::class, $message1); + $zddMessage2 = $factory->create($message2::class, $message2); + + // When / Then + $this->assertTrue($collection->fingerprintExists($zddMessage1->getFingerprint())); + $this->assertFalse($collection->fingerprintExists($zddMessage2->getFingerprint())); + } +} diff --git a/tests/Unit/Factory/ZddMessageFactoryTest.php b/tests/Unit/Factory/ZddMessageFactoryTest.php new file mode 100644 index 0000000..8b9df82 --- /dev/null +++ b/tests/Unit/Factory/ZddMessageFactoryTest.php @@ -0,0 +1,133 @@ +zddMessageFactory = new ZddMessageFactory($this->getSerializer(), new ZddPropertyExtractor()); + } + + public function testItGeneratesSerializedMessageWithNullAndNotNullableProperties(): void + { + $zddMessage = $this->zddMessageFactory->create(DummyMessageWithNullableNumberProperty::class, new DummyMessageWithNullableNumberProperty('Hello World!')); + self::assertSame( + $this->getSerializer()->serialize(new DummyMessageWithNullableNumberProperty('Hello World!')), + $zddMessage->serializedMessage, + ); + + self::assertCount(2, $zddMessage->properties); + + $propertyContent = $zddMessage->properties[0]; + $this->assertSame('content', $propertyContent->name); + $this->assertSame('string', $propertyContent->type); + $this->assertCount(0, $propertyContent->children); + + $propertyNumber = $zddMessage->properties[1]; + $this->assertSame('number', $propertyNumber->name); + $this->assertSame('NULL', $propertyNumber->type); + $this->assertCount(0, $propertyNumber->children); + } + + public function testItGeneratesSerializedMessageForDummyMessageWithPrivateConstructor(): void + { + $zddMessage = $this->zddMessageFactory->create(DummyMessageWithPrivateConstructor::class, DummyMessageWithPrivateConstructor::create('Hello World!')); + self::assertSame( + $this->getSerializer()->serialize(DummyMessageWithPrivateConstructor::create('Hello World!')), + $zddMessage->serializedMessage, + ); + + self::assertCount(1, $zddMessage->properties); + + $propertyContent = $zddMessage->properties[0]; + $this->assertSame('content', $propertyContent->name); + $this->assertSame('string', $propertyContent->type); + $this->assertCount(0, $propertyContent->children); + } + + public function testItGeneratesSerializedMessageForDummyMessageContainingAllManagedTypesWithoutError(): void + { + $zddMessage = $this->zddMessageFactory->create(DummyMessageWithAllManagedTypes::class, new DummyMessageWithAllManagedTypes( + 'Hello World!', + 42, + true, + ['PHP', 'For The Win'], + new Locale('fr'), + Status::DRAFT, + )); + + self::assertSame( + $this->getSerializer()->serialize(new DummyMessageWithAllManagedTypes( + 'Hello World!', + 42, + true, + ['PHP', 'For The Win'], + new Locale('fr'), + Status::DRAFT + )), + $zddMessage->serializedMessage, + ); + + self::assertCount(6, $zddMessage->properties); + + $propertyContent = $zddMessage->properties[0]; + $this->assertSame('content', $propertyContent->name); + $this->assertSame('string', $propertyContent->type); + $this->assertCount(0, $propertyContent->children); + + $propertyCount = $zddMessage->properties[1]; + $this->assertSame('count', $propertyCount->name); + $this->assertSame('integer', $propertyCount->type); + $this->assertCount(0, $propertyCount->children); + + $propertyEnable = $zddMessage->properties[2]; + $this->assertSame('enable', $propertyEnable->name); + $this->assertSame('boolean', $propertyEnable->type); + $this->assertCount(0, $propertyEnable->children); + + $propertyData = $zddMessage->properties[3]; + $this->assertSame('data', $propertyData->name); + $this->assertSame('array', $propertyData->type); + $this->assertCount(0, $propertyData->children); + + $propertyLocale = $zddMessage->properties[4]; + $this->assertSame('locale', $propertyLocale->name); + $this->assertSame(Locale::class, $propertyLocale->type); + $this->assertCount(1, $propertyLocale->children); + + $propertyLocaleValue = $propertyLocale->children[0]; + $this->assertSame('locale', $propertyLocaleValue->name); + $this->assertSame('string', $propertyLocaleValue->type); + $this->assertCount(0, $propertyLocaleValue->children); + + $propertyStatus = $zddMessage->properties[5]; + $this->assertSame('status', $propertyStatus->name); + $this->assertSame(Status::class, $propertyStatus->type); + $this->assertCount(2, $propertyStatus->children); + + $propertyStatusName = $propertyStatus->children[0]; + $this->assertSame('name', $propertyStatusName->name); + $this->assertSame('string', $propertyStatusName->type); + $this->assertCount(0, $propertyStatusName->children); + + $propertyStatusValue = $propertyStatus->children[1]; + $this->assertSame('value', $propertyStatusValue->name); + $this->assertSame('string', $propertyStatusValue->type); + $this->assertCount(0, $propertyStatusValue->children); + } +} diff --git a/tests/Unit/Factory/ZddMessageTest.php b/tests/Unit/Factory/ZddMessageTest.php new file mode 100644 index 0000000..32571bb --- /dev/null +++ b/tests/Unit/Factory/ZddMessageTest.php @@ -0,0 +1,49 @@ +assertEquals($message, $decodedMessage); + } +} diff --git a/tests/Unit/Factory/ZddPropertyExtractorTest.php b/tests/Unit/Factory/ZddPropertyExtractorTest.php new file mode 100644 index 0000000..3102888 --- /dev/null +++ b/tests/Unit/Factory/ZddPropertyExtractorTest.php @@ -0,0 +1,82 @@ +extractProperties($message); + + // Then + $this->assertCount(1, $properties); + + // wapper.command + $propertyCommand = $properties[0]; + $this->assertSame('command', $propertyCommand->name); + $this->assertSame(Command::class, $propertyCommand->type); + $this->assertCount(3, $propertyCommand->children); + + // wapper.command.name + $childName = $propertyCommand->children[0]; + $this->assertSame('name', $childName->name); + $this->assertSame('string', $childName->type); + $this->assertCount(0, $childName->children); + + // wapper.command.stringType + $childStringType = $propertyCommand->children[1]; + $this->assertSame('myString', $childStringType->name); + $this->assertSame(EnumString::class, $childStringType->type); + $this->assertCount(2, $childStringType->children); + + // wapper.command.stringType.name + $propertyName = $childStringType->children[0]; + $this->assertSame('name', $propertyName->name); + $this->assertSame('string', $propertyName->type); + $this->assertCount(0, $propertyName->children); + + // wapper.command.stringType.value + $propertyValue = $childStringType->children[1]; + $this->assertSame('value', $propertyValue->name); + $this->assertSame('string', $propertyValue->type); + $this->assertCount(0, $propertyValue->children); + + // wapper.command.intType + $childIntType = $propertyCommand->children[2]; + $this->assertSame('myInt', $childIntType->name); + $this->assertSame(EnumInt::class, $childIntType->type); + $this->assertCount(2, $childIntType->children); + + // wapper.command.intType.value + $propertyName = $childIntType->children[0]; + $this->assertSame('name', $propertyName->name); + $this->assertSame('string', $propertyName->type); + $this->assertCount(0, $propertyName->children); + + // wapper.command.intType.value + $propertyValue = $childIntType->children[1]; + $this->assertSame('value', $propertyValue->name); + $this->assertSame('integer', $propertyValue->type); + $this->assertCount(0, $propertyValue->children); + } +} diff --git a/tests/Unit/Listener/Symfony/MessengerListenerTest.php b/tests/Unit/Listener/Symfony/MessengerListenerTest.php index 762528f..956df40 100644 --- a/tests/Unit/Listener/Symfony/MessengerListenerTest.php +++ b/tests/Unit/Listener/Symfony/MessengerListenerTest.php @@ -5,7 +5,12 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent; +use Symfony\Component\Messenger\Transport\Serialization\Serializer; +use Yousign\ZddMessageBundle\Factory\ZddMessageCollection; +use Yousign\ZddMessageBundle\Factory\ZddMessageFactory; +use Yousign\ZddMessageBundle\Factory\ZddPropertyExtractor; use Yousign\ZddMessageBundle\Listener\Symfony\MessengerListener; +use Yousign\ZddMessageBundle\Serializer\ZddMessageMessengerSerializer; use Yousign\ZddMessageBundle\Tests\Fixtures\App\Logger\SpyLogger; use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\Config\MessageConfig; use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessage; @@ -13,9 +18,12 @@ use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessageWithNullableNumberProperty; use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\Input\Locale; use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\Input\Status; +use Yousign\ZddMessageBundle\Tests\Unit\SerializerTrait; class MessengerListenerTest extends TestCase { + use SerializerTrait; + public function provideTrackedMessages(): iterable { yield DummyMessage::class => [ @@ -43,9 +51,22 @@ public function provideTrackedMessages(): iterable */ public function testOnMessageReceivedLogNothingWhenMessageIsTracked(object $message): void { + $messageFactory = new ZddMessageFactory( + $this->getSerializer(), + new ZddPropertyExtractor(), + ); + + MessageConfig::$messagesToAssert = [ + $message::class => $message, + ]; + $messageListener = new MessengerListener( $spyLogger = new SpyLogger(), - new MessageConfig(), + $messageFactory, + new ZddMessageCollection( + new MessageConfig(), + $messageFactory + ), 'warning', ); @@ -76,9 +97,20 @@ public function provideUntrackedMessages(): iterable */ public function testOnMessageReceivedLogMessageWhenMessageIsNotTracked(object $message, string $class, string $logLevel): void { + $messageFactory = new ZddMessageFactory( + $this->getSerializer(), + new ZddPropertyExtractor(), + ); + + MessageConfig::reset(); + $messageListener = new MessengerListener( $spyLogger = new SpyLogger(), - new MessageConfig(), + $messageFactory, + new ZddMessageCollection( + new MessageConfig(), + $messageFactory + ), $logLevel, ); @@ -97,9 +129,20 @@ public function testOnMessageReceivedLogMessageWhenMessageIsNotTracked(object $m public function testOnMessageReceivedLogNothingWhenGetMessageIsNotAnObject(): void { + $messageFactory = new ZddMessageFactory( + $this->getSerializer(), + new ZddPropertyExtractor(), + ); + + MessageConfig::reset(); + $messageListener = new MessengerListener( $spyLogger = new SpyLogger(), - new MessageConfig(), + $messageFactory, + new ZddMessageCollection( + new MessageConfig(), + $messageFactory + ), 'warning', ); @@ -118,9 +161,21 @@ public function getMessage(): string public function testOnMessageReceivedLogAWarningWhenAnErrorOccurs(): void { + $messageFactory = new ZddMessageFactory( + $this->getSerializer(), + new ZddPropertyExtractor(), + ); + + MessageConfig::reset(); + $messageListener = new MessengerListener( $spyLogger = new SpyLogger(), - new MessageConfig(), + $messageFactory, + new ZddMessageCollection( + new MessageConfig(), + $messageFactory + ), + 'warning', ); $message = new class() { diff --git a/tests/Unit/ZddMessageAsserterTest.php b/tests/Unit/ZddMessageAsserterTest.php deleted file mode 100644 index 82cf238..0000000 --- a/tests/Unit/ZddMessageAsserterTest.php +++ /dev/null @@ -1,160 +0,0 @@ -getSut(); - $sut->assert($messageFqcn, $serializedMessage, $propertyList); - self::assertTrue(true); // if we reached this statement, no exception has been thrown => OK test - } - - public function provideValidAssertion(): iterable - { - yield 'Unchanged message' => [ - DummyMessage::class, - $this->getSerializer()->serialize(new DummyMessage('Hello world')), - << [ - DummyMessageWithNullableNumberProperty::class, - $this->getSerializer()->serialize(new DummyMessageWithNullableNumberProperty('Hello world')), - <<getSerializedMessageForPreviousVersionOfDummyMessageWithNumberProperty(); - - $propertyList = PropertyList::fromJson($jsonProperties); - self::assertTrue($propertyList->has('content')); - self::assertTrue($propertyList->has('number')); - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('⚠️ The properties "number" in class "Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessage" seems to have been removed'); - - $sut = $this->getSut(); - $sut->assert(DummyMessage::class, $serializedMessage, $propertyList); - } - - private function getSerializedMessageForPreviousVersionOfDummyMessageWithNumberProperty(): array - { - $jsonProperties = <<getSerializer()->serialize(new DummyMessage('Hello world')), - $jsonProperties, - ]; - } - - public function testItThrowsExceptionForInvalidIntegrationDueToClassMismatch(): void - { - $sut = $this->getSut(); - - try { - $sut->assert(DummyMessage::class, $this->getSerializer()->serialize(new \stdClass()), new PropertyList()); - $this->fail('This test should raised expected exception'); - } catch (\Throwable $t) { - $this->assertInstanceOf(\LogicException::class, $t); - $this->assertStringContainsString('Class mismatch', $t->getMessage()); - $this->assertStringContainsString(DummyMessage::class, $t->getMessage()); - } - } - - public function testItThrowsExceptionForInvalidIntegrationDueToPropertyTypeMismatch(): void - { - // Simulate error using 'int' typeHint instead of 'string' - $serializedMessage = $this->getSerializer()->serialize(new DummyMessage('Hello world')); - $propertyList = PropertyList::fromJson(<<expectException(\LogicException::class); - $this->expectExceptionMessage('Error for property "content" in class "Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessage", the type mismatch between the old and the new version of class. Please verify your integration.'); - - $sut = $this->getSut(); - $sut->assert(DummyMessage::class, $serializedMessage, $propertyList); - } - - public function testItThrowsExceptionForInvalidPropertyType(): void - { - $serializedMessage = $this->getSerializer()->serialize(new DummyMessageWithWrongPropertyType(22)); - $serializedMessage = \str_replace('DummyMessageWithWrongPropertyType', 'DummyMessage', $serializedMessage); - - $sut = $this->getSut(); - try { - $sut->assert(DummyMessage::class, $serializedMessage, new PropertyList()); - } catch (UnableToDeserializeException $e) { - $this->assertInstanceOf(MessageDecodingFailedException::class, $e->getPrevious()); - - return; - } - - $this->fail('This test should raised expected exception'); - } - - public function getSut(): ZddMessageAsserter - { - return new ZddMessageAsserter($this->getSerializer()); - } -} diff --git a/tests/Unit/ZddMessageFactoryTest.php b/tests/Unit/ZddMessageFactoryTest.php deleted file mode 100644 index 89d6699..0000000 --- a/tests/Unit/ZddMessageFactoryTest.php +++ /dev/null @@ -1,121 +0,0 @@ -zddMessageFactory = new ZddMessageFactory(new MessageConfig(), $this->getSerializer()); - } - - public function testItGeneratesSerializedMessageWithNullAndNotNullableProperties(): void - { - $zddMessage = $this->zddMessageFactory->create(DummyMessageWithNullableNumberProperty::class); - self::assertSame( - $this->getSerializer()->serialize(new DummyMessageWithNullableNumberProperty('Hello World!')), - $zddMessage->serializedMessage() - ); - - self::assertEquals(2, $zddMessage->propertyList()->count()); - self::assertTrue($zddMessage->propertyList()->has('content')); - $property = $zddMessage->propertyList()->get('content'); - self::assertSame('string', $property->type); - self::assertSame('Hello World!', $property->value); - - $propertyNumber = $zddMessage->propertyList()->get('number'); - self::assertSame('int', $propertyNumber->type); - self::assertNull($propertyNumber->value); - } - - public function testItGeneratesSerializedMessageForDummyMessageWithPrivateConstructor(): void - { - $zddMessage = $this->zddMessageFactory->create(DummyMessageWithPrivateConstructor::class); - self::assertSame( - $this->getSerializer()->serialize(DummyMessageWithPrivateConstructor::create('Hello World!')), - $zddMessage->serializedMessage() - ); - - self::assertEquals(1, $zddMessage->propertyList()->count()); - self::assertTrue($zddMessage->propertyList()->has('content')); - $property = $zddMessage->propertyList()->get('content'); - self::assertSame('string', $property->type); - self::assertSame('Hello World!', $property->value); - } - - public function testItGeneratesSerializedMessageForDummyMessageContainingAllManagedTypesWithoutError(): void - { - $zddMessage = $this->zddMessageFactory->create(DummyMessageWithAllManagedTypes::class); - - self::assertSame( - $this->getSerializer()->serialize(new DummyMessageWithAllManagedTypes( - 'Hello World!', - 42, - true, - ['PHP', 'For The Win'], - new Locale('fr'), - Status::DRAFT - )), - $zddMessage->serializedMessage()); - - self::assertEquals(6, $zddMessage->propertyList()->count()); - self::assertSame('string', $zddMessage->propertyList()->get('content')->type); - self::assertSame('int', $zddMessage->propertyList()->get('count')->type); - self::assertSame('bool', $zddMessage->propertyList()->get('enable')->type); - self::assertSame('array', $zddMessage->propertyList()->get('data')->type); - self::assertSame(Locale::class, $zddMessage->propertyList()->get('locale')->type); - self::assertSame(Status::class, $zddMessage->propertyList()->get('status')->type); - } - - public function testItThrownAMissingValueForTypeException(): void - { - $factory = new ZddMessageFactory(new WithoutValueConfig(), $this->getSerializer()); - - $this->expectException(MissingValueForTypeException::class); - $this->expectExceptionMessage('Missing value for property type "Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessage" maybe you forgot to add it in "App\WithoutValue\WithoutValueConfig"'); - $factory->create(WithoutValue::class); - } -} - -namespace App\WithoutValue; - -use Yousign\ZddMessageBundle\Config\ZddMessageConfigInterface; -use Yousign\ZddMessageBundle\Tests\Fixtures\App\Messages\DummyMessage; - -final class WithoutValue -{ - public function __construct(private DummyMessage $dummyMessage) - { - } -} - -final class WithoutValueConfig implements ZddMessageConfigInterface -{ - public function getMessageToAssert(): array - { - return [ - WithoutValue::class, - ]; - } - - public function generateValueForCustomPropertyType(string $type): mixed - { - return null; - } -}