diff --git a/.gitignore b/.gitignore index b98b4400..fe1abf22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ vendor/ composer.lock /.php-cs-fixer.cache +/.phpunit.cache +.phpunit.result.cache diff --git a/.jane-openapi b/.jane-openapi index 85e86f26..eba467cc 100644 --- a/.jane-openapi +++ b/.jane-openapi @@ -7,5 +7,5 @@ return [ 'openapi-file' => __DIR__ . '/spec/v1.45.json', 'reference' => true, 'strict' => false, - 'date-input-format' => 'Y-m-d\TH:i:s.uuP', + 'date-input-format' => '', ]; diff --git a/composer.json b/composer.json index cd5f9f6a..f027ef4e 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,7 @@ "ergebnis/composer-normalize": "^2.42", "friendsofphp/php-cs-fixer": "^3.8", "jane-php/open-api-3": "^7.4", + "phpunit/phpunit": "^11.0", "roave/security-advisories": "dev-latest" }, "conflict": { @@ -44,6 +45,11 @@ "Docker\\API\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Docker\\API\\Tests\\": "tests/" + } + }, "config": { "allow-plugins": { "ergebnis/composer-normalize": true, @@ -63,6 +69,7 @@ } }, "scripts": { + "test": "vendor/bin/phpunit", "generate": [ "vendor/bin/jane-openapi generate", "cat patches/*.patch | patch -s -p1" diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..6e1a5d82 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,17 @@ + + + + + tests + + + + + src + + + diff --git a/src/Normalizer/HealthcheckResultNormalizer.php b/src/Normalizer/HealthcheckResultNormalizer.php index b3962a2b..a4437632 100644 --- a/src/Normalizer/HealthcheckResultNormalizer.php +++ b/src/Normalizer/HealthcheckResultNormalizer.php @@ -33,18 +33,18 @@ public function supportsNormalization(mixed $data, ?string $format = null, array public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed { - if (isset($data['$ref'])) { + $object = new \Docker\API\Model\HealthcheckResult(); + if (null === $data || false === \is_array($data)) { + return $object; + } + if (isset($data['$ref']) && !isset($data['type']) && !isset($data['properties']) && !isset($data['allOf'])) { return new Reference($data['$ref'], $context['document-origin']); } if (isset($data['$recursiveRef'])) { return new Reference($data['$recursiveRef'], $context['document-origin']); } - $object = new \Docker\API\Model\HealthcheckResult(); - if (null === $data || false === \is_array($data)) { - return $object; - } if (\array_key_exists('Start', $data) && null !== $data['Start']) { - $object->setStart(\DateTime::createFromFormat('Y-m-d\TH:i:s.uuP', $data['Start'])); + $object->setStart('Z' === (new \DateTime($data['Start']))->getTimezone()->getName() ? (new \DateTime($data['Start']))->setTimezone(new \DateTimeZone('GMT')) : new \DateTime($data['Start'])); unset($data['Start']); } elseif (\array_key_exists('Start', $data) && null === $data['Start']) { $object->setStart(null); @@ -80,7 +80,7 @@ public function normalize(mixed $data, ?string $format = null, array $context = { $dataArray = []; if ($data->isInitialized('start') && null !== $data->getStart()) { - $dataArray['Start'] = $data->getStart()?->format('Y-m-d\TH:i:sP'); + $dataArray['Start'] = $data->getStart()->format('Y-m-d\TH:i:sP'); } if ($data->isInitialized('end') && null !== $data->getEnd()) { $dataArray['End'] = $data->getEnd(); diff --git a/tests/Normalizer/HealthcheckResultNormalizerTest.php b/tests/Normalizer/HealthcheckResultNormalizerTest.php new file mode 100644 index 00000000..c0ba83d0 --- /dev/null +++ b/tests/Normalizer/HealthcheckResultNormalizerTest.php @@ -0,0 +1,66 @@ +normalizer = new HealthcheckResultNormalizer(); + } + + public static function parsedStartProvider(): iterable + { + yield 'nanosecond precision with Z timezone (localstack format)' => [ + '2019-12-22T10:59:05.6385933Z', + '2019-12-22', + '10:59:05', + ]; + yield 'microsecond precision with numeric offset' => [ + '2019-12-22T10:59:05.638593+00:00', + '2019-12-22', + '10:59:05', + ]; + } + + #[DataProvider('parsedStartProvider')] + public function testDenormalizeStartParsesDateTime(string $input, string $expectedDate, string $expectedTime): void + { + $result = $this->normalizer->denormalize(['Start' => $input], HealthcheckResult::class); + + self::assertInstanceOf(\DateTimeInterface::class, $result->getStart()); + self::assertSame($expectedDate, $result->getStart()->format('Y-m-d')); + self::assertSame($expectedTime, $result->getStart()->format('H:i:s')); + } + + public static function nullStartProvider(): iterable + { + yield 'explicit null' => [['Start' => null]]; + yield 'unparsable string' => [['Start' => 'not-a-date']]; + yield 'non-string value' => [['Start' => 12345]]; + } + + #[DataProvider('nullStartProvider')] + public function testDenormalizeStartReturnsNull(array $data): void + { + $result = $this->normalizer->denormalize($data, HealthcheckResult::class); + + self::assertNull($result->getStart()); + } + + public function testDenormalizeStartNotInitializedWhenKeyMissing(): void + { + $result = $this->normalizer->denormalize([], HealthcheckResult::class); + + self::assertFalse($result->isInitialized('start')); + } +}