diff --git a/HISTORY.md b/HISTORY.md index 0028c636..8a434fd7 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. ## unreleased +* Added + * Support for ExternalReferences in BOM and Component (via [#17]) + +[#17]: https://github.com/CycloneDX/cyclonedx-php-library/pull/17 + ## 1.0.3 - 2021-11-15 * Fixed diff --git a/src/Core/Enums/ExternalReferenceType.php b/src/Core/Enums/ExternalReferenceType.php new file mode 100644 index 00000000..c8913217 --- /dev/null +++ b/src/Core/Enums/ExternalReferenceType.php @@ -0,0 +1,75 @@ +getConstants(); + + return \in_array($value, $values, true); + } +} diff --git a/src/Core/Models/Bom.php b/src/Core/Models/Bom.php index 50735697..8ce65b43 100644 --- a/src/Core/Models/Bom.php +++ b/src/Core/Models/Bom.php @@ -24,6 +24,7 @@ namespace CycloneDX\Core\Models; use CycloneDX\Core\Repositories\ComponentRepository; +use CycloneDX\Core\Repositories\ExternalReferenceRepository; use DomainException; /** @@ -56,6 +57,14 @@ class Bom */ private $metaData; + /** + * Provides the ability to document external references related to the BOM or + * to the project the BOM describes. + * + * @var ExternalReferenceRepository|null + */ + private $externalReferenceRepository; + public function __construct(?ComponentRepository $componentRepository = null) { $this->setComponentRepository($componentRepository ?? new ComponentRepository()); @@ -124,4 +133,19 @@ public function setMetaData(?MetaData $metaData): self return $this; } + + public function getExternalReferenceRepository(): ?ExternalReferenceRepository + { + return $this->externalReferenceRepository; + } + + /** + * @return $this + */ + public function setExternalReferenceRepository(?ExternalReferenceRepository $externalReferenceRepository): self + { + $this->externalReferenceRepository = $externalReferenceRepository; + + return $this; + } } diff --git a/src/Core/Models/Component.php b/src/Core/Models/Component.php index c56aa5cc..4e8a2d27 100644 --- a/src/Core/Models/Component.php +++ b/src/Core/Models/Component.php @@ -27,6 +27,7 @@ use CycloneDX\Core\Models\License\LicenseExpression; use CycloneDX\Core\Repositories\BomRefRepository; use CycloneDX\Core\Repositories\DisjunctiveLicenseRepository; +use CycloneDX\Core\Repositories\ExternalReferenceRepository; use CycloneDX\Core\Repositories\HashRepository; use DomainException; use PackageUrl\PackageUrl; @@ -137,6 +138,14 @@ class Component */ private $version; + /** + * Provides the ability to document external references related to the + * component or to the project the component describes. + * + * @var ExternalReferenceRepository|null + */ + private $externalReferenceRepository; + public function getBomRef(): BomRef { return $this->bomRef; @@ -331,6 +340,21 @@ public function setDependenciesBomRefRepository(?BomRefRepository $dependenciesB return $this; } + public function getExternalReferenceRepository(): ?ExternalReferenceRepository + { + return $this->externalReferenceRepository; + } + + /** + * @return $this + */ + public function setExternalReferenceRepository(?ExternalReferenceRepository $externalReferenceRepository): self + { + $this->externalReferenceRepository = $externalReferenceRepository; + + return $this; + } + /** * @psalm-assert Classification::* $type * diff --git a/src/Core/Models/ExternalReference.php b/src/Core/Models/ExternalReference.php new file mode 100644 index 00000000..d4c7238a --- /dev/null +++ b/src/Core/Models/ExternalReference.php @@ -0,0 +1,150 @@ +type; + } + + /** + * @param string $type A valid {@see \CycloneDX\Core\Enums\ExternalReferenceType} + * @psalm-assert ExternalReferenceType::* $type + * + * @throws DomainException if value is unknown + * + * @return $this + */ + public function setType(string $type): self + { + if (false === ExternalReferenceType::isValidValue($type)) { + throw new DomainException("Invalid type: $type"); + } + /** @psalm-var ExternalReferenceType::* $type */ + $this->type = $type; + + return $this; + } + + public function getUrl(): string + { + return $this->url; + } + + /** + * @return $this + */ + public function setUrl(string $url): self + { + $this->url = $url; + + return $this; + } + + public function getComment(): ?string + { + return $this->comment; + } + + /** + * @return $this + */ + public function setComment(?string $comment): self + { + $this->comment = $comment; + + return $this; + } + + public function getHashRepository(): ?HashRepository + { + return $this->hashRepository; + } + + /** + * @return $this + */ + public function setHashRepository(?HashRepository $hashRepository): self + { + $this->hashRepository = $hashRepository; + + return $this; + } + + /** + * @psalm-assert ExternalReferenceType::* $type + * + * @throws DomainException if type is unknown + */ + public function __construct(string $type, string $url) + { + $this->setType($type); + $this->setUrl($url); + } +} diff --git a/src/Core/Repositories/ExternalReferenceRepository.php b/src/Core/Repositories/ExternalReferenceRepository.php new file mode 100644 index 00000000..be8d2d77 --- /dev/null +++ b/src/Core/Repositories/ExternalReferenceRepository.php @@ -0,0 +1,72 @@ + + */ + private $externalReferences = []; + + public function __construct(ExternalReference ...$externalReferences) + { + $this->addExternalReference(...$externalReferences); + } + + /** + * @return $this + */ + public function addExternalReference(ExternalReference ...$externalReferences): self + { + foreach ($externalReferences as $externalReference) { + if (\in_array($externalReference, $this->externalReferences, true)) { + continue; + } + $this->externalReferences[] = $externalReference; + } + + return $this; + } + + /** + * @return ExternalReference[] + * @psalm-return list + */ + public function getExternalReferences(): array + { + return $this->externalReferences; + } + + public function count(): int + { + return \count($this->externalReferences); + } +} diff --git a/src/Core/Serialize/DOM/NormalizerFactory.php b/src/Core/Serialize/DOM/NormalizerFactory.php index 43392fc1..445edaa7 100644 --- a/src/Core/Serialize/DOM/NormalizerFactory.php +++ b/src/Core/Serialize/DOM/NormalizerFactory.php @@ -137,4 +137,14 @@ public function makeForDependencies(): Normalizers\DependenciesNormalizer { return new Normalizers\DependenciesNormalizer($this); } + + public function makeForExternalReference(): Normalizers\ExternalReferenceNormalizer + { + return new Normalizers\ExternalReferenceNormalizer($this); + } + + public function makeForExternalReferenceRepository(): Normalizers\ExternalReferenceRepositoryNormalizer + { + return new Normalizers\ExternalReferenceRepositoryNormalizer($this); + } } diff --git a/src/Core/Serialize/DOM/Normalizers/BomNormalizer.php b/src/Core/Serialize/DOM/Normalizers/BomNormalizer.php index dce46ac6..86c43390 100644 --- a/src/Core/Serialize/DOM/Normalizers/BomNormalizer.php +++ b/src/Core/Serialize/DOM/Normalizers/BomNormalizer.php @@ -27,6 +27,7 @@ use CycloneDX\Core\Models\Bom; use CycloneDX\Core\Models\MetaData; use CycloneDX\Core\Repositories\ComponentRepository; +use CycloneDX\Core\Repositories\ExternalReferenceRepository; use CycloneDX\Core\Serialize\DOM\AbstractNormalizer; use DOMElement; @@ -61,8 +62,8 @@ public function normalize(Bom $bom): DOMElement [ $this->normalizeMetaData($bom->getMetaData()), $this->normalizeComponents($bom->getComponentRepository()), + $this->normalizeExternalReferences($bom->getExternalReferenceRepository()), $this->normalizeDependencies($bom), - // externalReferences ] ); @@ -111,4 +112,16 @@ private function normalizeDependencies(Bom $bom): ?DOMElement $deps ); } + + private function normalizeExternalReferences(?ExternalReferenceRepository $externalReferenceRepository): ?DOMElement + { + $factory = $this->getNormalizerFactory(); + + return null === $externalReferenceRepository || 0 === \count($externalReferenceRepository) + ? null + : $this->simpleDomAppendChildren( + $factory->getDocument()->createElement('externalReferences'), + $factory->makeForExternalReferenceRepository()->normalize($externalReferenceRepository) + ); + } } diff --git a/src/Core/Serialize/DOM/Normalizers/ComponentNormalizer.php b/src/Core/Serialize/DOM/Normalizers/ComponentNormalizer.php index 98676ff6..2441dcbc 100644 --- a/src/Core/Serialize/DOM/Normalizers/ComponentNormalizer.php +++ b/src/Core/Serialize/DOM/Normalizers/ComponentNormalizer.php @@ -28,6 +28,7 @@ use CycloneDX\Core\Models\Component; use CycloneDX\Core\Models\License\LicenseExpression; use CycloneDX\Core\Repositories\DisjunctiveLicenseRepository; +use CycloneDX\Core\Repositories\ExternalReferenceRepository; use CycloneDX\Core\Repositories\HashRepository; use CycloneDX\Core\Serialize\DOM\AbstractNormalizer; use DomainException; @@ -87,7 +88,7 @@ public function normalize(Component $component): DOMElement $this->normalizePurl($component->getPackageUrl()), // modified // pedigree - // externalReferences + $this->normalizeExternalReferences($component->getExternalReferenceRepository()), // components ] ); @@ -162,4 +163,16 @@ private function normalizePurl(?PackageUrl $purl): ?DOMElement ? null : $this->simpleDomSafeTextElement($this->getNormalizerFactory()->getDocument(), 'purl', (string) $purl); } + + private function normalizeExternalReferences(?ExternalReferenceRepository $externalReferenceRepository): ?DOMElement + { + $factory = $this->getNormalizerFactory(); + + return null === $externalReferenceRepository || 0 === \count($externalReferenceRepository) + ? null + : $this->simpleDomAppendChildren( + $factory->getDocument()->createElement('externalReferences'), + $factory->makeForExternalReferenceRepository()->normalize($externalReferenceRepository) + ); + } } diff --git a/src/Core/Serialize/DOM/Normalizers/ExternalReferenceNormalizer.php b/src/Core/Serialize/DOM/Normalizers/ExternalReferenceNormalizer.php new file mode 100644 index 00000000..b14ab99c --- /dev/null +++ b/src/Core/Serialize/DOM/Normalizers/ExternalReferenceNormalizer.php @@ -0,0 +1,78 @@ +getNormalizerFactory()->getDocument(); + + return $this->simpleDomAppendChildren( + $this->simpleDomSetAttributes( + $doc->createElement('reference'), + [ + 'type' => $externalReference->getType(), + ] + ), + [ + $this->simpleDomSafeTextElement($doc, 'url', $externalReference->getUrl()), + $this->simpleDomSafeTextElement($doc, 'comment', $externalReference->getComment()), + $this->normalizeHashes($externalReference->getHashRepository()), + ] + ); + } + + private function normalizeHashes(?HashRepository $hashes): ?DOMElement + { + $factory = $this->getNormalizerFactory(); + + if (false === $factory->getSpec()->supportsExternalReferenceHashes()) { + return null; + } + + return null === $hashes || 0 === \count($hashes) + ? null + : $this->simpleDomAppendChildren( + $factory->getDocument()->createElement('hashes'), + $factory->makeForHashRepository()->normalize($hashes) + ); + } +} diff --git a/src/Core/Serialize/DOM/Normalizers/ExternalReferenceRepositoryNormalizer.php b/src/Core/Serialize/DOM/Normalizers/ExternalReferenceRepositoryNormalizer.php new file mode 100644 index 00000000..89efbb7c --- /dev/null +++ b/src/Core/Serialize/DOM/Normalizers/ExternalReferenceRepositoryNormalizer.php @@ -0,0 +1,54 @@ + + */ + public function normalize(ExternalReferenceRepository $repo): array + { + $normalizer = $this->getNormalizerFactory()->makeForExternalReference(); + + $externalReferences = []; + foreach ($repo->getExternalReferences() as $externalReference) { + try { + $externalReferences[] = $normalizer->normalize($externalReference); + } catch (\DomainException $exception) { + continue; + } + } + + return $externalReferences; + } +} diff --git a/src/Core/Serialize/JSON/NormalizerFactory.php b/src/Core/Serialize/JSON/NormalizerFactory.php index 22028bea..6aabfb08 100644 --- a/src/Core/Serialize/JSON/NormalizerFactory.php +++ b/src/Core/Serialize/JSON/NormalizerFactory.php @@ -127,4 +127,14 @@ public function makeForDependencies(): Normalizers\DependenciesNormalizer { return new Normalizers\DependenciesNormalizer($this); } + + public function makeForExternalReference(): Normalizers\ExternalReferenceNormalizer + { + return new Normalizers\ExternalReferenceNormalizer($this); + } + + public function makeForExternalReferenceRepository(): Normalizers\ExternalReferenceRepositoryNormalizer + { + return new Normalizers\ExternalReferenceRepositoryNormalizer($this); + } } diff --git a/src/Core/Serialize/JSON/Normalizers/BomNormalizer.php b/src/Core/Serialize/JSON/Normalizers/BomNormalizer.php index a28f6eb3..178cb4d7 100644 --- a/src/Core/Serialize/JSON/Normalizers/BomNormalizer.php +++ b/src/Core/Serialize/JSON/Normalizers/BomNormalizer.php @@ -26,6 +26,7 @@ use CycloneDX\Core\Helpers\NullAssertionTrait; use CycloneDX\Core\Models\Bom; use CycloneDX\Core\Models\MetaData; +use CycloneDX\Core\Repositories\ExternalReferenceRepository; use CycloneDX\Core\Serialize\JSON\AbstractNormalizer; /** @@ -48,6 +49,7 @@ public function normalize(Bom $bom): array 'version' => $bom->getVersion(), 'metadata' => $this->normalizeMetaData($bom->getMetaData()), 'components' => $factory->makeForComponentRepository()->normalize($bom->getComponentRepository()), + 'externalReferences' => $this->normalizeExternalReferences($bom->getExternalReferenceRepository()), 'dependencies' => $this->normalizeDependencies($bom), ], [$this, 'isNotNull'] @@ -73,6 +75,13 @@ private function normalizeMetaData(?MetaData $metaData): ?array : $data; } + private function normalizeExternalReferences(?ExternalReferenceRepository $externalReferenceRepository): ?array + { + return null === $externalReferenceRepository || 0 === \count($externalReferenceRepository) + ? null + : $this->getNormalizerFactory()->makeForExternalReferenceRepository()->normalize($externalReferenceRepository); + } + private function normalizeDependencies(Bom $bom): ?array { $factory = $this->getNormalizerFactory(); diff --git a/src/Core/Serialize/JSON/Normalizers/ComponentNormalizer.php b/src/Core/Serialize/JSON/Normalizers/ComponentNormalizer.php index 39d9e30b..27b8cb81 100644 --- a/src/Core/Serialize/JSON/Normalizers/ComponentNormalizer.php +++ b/src/Core/Serialize/JSON/Normalizers/ComponentNormalizer.php @@ -28,6 +28,7 @@ use CycloneDX\Core\Models\Component; use CycloneDX\Core\Models\License\LicenseExpression; use CycloneDX\Core\Repositories\DisjunctiveLicenseRepository; +use CycloneDX\Core\Repositories\ExternalReferenceRepository; use CycloneDX\Core\Repositories\HashRepository; use CycloneDX\Core\Serialize\JSON\AbstractNormalizer; use DomainException; @@ -72,6 +73,7 @@ public function normalize(Component $component): array 'licenses' => $this->normalizeLicense($component->getLicense()), 'hashes' => $this->normalizeHashes($component->getHashRepository()), 'purl' => $this->normalizePurl($component->getPackageUrl()), + 'externalReferences' => $this->normalizeExternalReferences($component->getExternalReferenceRepository()), ], [$this, 'isNotNull'] ); @@ -126,4 +128,11 @@ private function normalizePurl(?PackageUrl $purl): ?string ? null : (string) $purl; } + + private function normalizeExternalReferences(?ExternalReferenceRepository $externalReferenceRepository): ?array + { + return null === $externalReferenceRepository || 0 === \count($externalReferenceRepository) + ? null + : $this->getNormalizerFactory()->makeForExternalReferenceRepository()->normalize($externalReferenceRepository); + } } diff --git a/src/Core/Serialize/JSON/Normalizers/ExternalReferenceNormalizer.php b/src/Core/Serialize/JSON/Normalizers/ExternalReferenceNormalizer.php new file mode 100644 index 00000000..6ace1ade --- /dev/null +++ b/src/Core/Serialize/JSON/Normalizers/ExternalReferenceNormalizer.php @@ -0,0 +1,68 @@ + $externalReference->getType(), + 'url' => $externalReference->getUrl(), + 'comment' => $externalReference->getComment(), + 'hashes' => $this->normalizeHashes($externalReference->getHashRepository()), + ], + [$this, 'isNotNull'] + ); + } + + private function normalizeHashes(?HashRepository $hashes): ?array + { + $factory = $this->getNormalizerFactory(); + + if (false === $factory->getSpec()->supportsExternalReferenceHashes()) { + return null; + } + + return null === $hashes || 0 === \count($hashes) + ? null + : $factory->makeForHashRepository()->normalize($hashes); + } +} diff --git a/src/Core/Serialize/JSON/Normalizers/ExternalReferenceRepositoryNormalizer.php b/src/Core/Serialize/JSON/Normalizers/ExternalReferenceRepositoryNormalizer.php new file mode 100644 index 00000000..be05393e --- /dev/null +++ b/src/Core/Serialize/JSON/Normalizers/ExternalReferenceRepositoryNormalizer.php @@ -0,0 +1,56 @@ + + */ + public function normalize(ExternalReferenceRepository $repo): array + { + $normalizer = $this->getNormalizerFactory()->makeForExternalReference(); + + $externalReferences = []; + foreach ($repo->getExternalReferences() as $externalReference) { + try { + $item = $normalizer->normalize($externalReference); + } catch (\DomainException $exception) { + continue; + } + if (false === empty($item)) { + $externalReferences[] = $item; + } + } + + return $externalReferences; + } +} diff --git a/src/Core/Spec/Spec11.php b/src/Core/Spec/Spec11.php index ddc68cbd..fa04452d 100644 --- a/src/Core/Spec/Spec11.php +++ b/src/Core/Spec/Spec11.php @@ -79,4 +79,9 @@ public function supportsDependencies(): bool { return false; } + + public function supportsExternalReferenceHashes(): bool + { + return false; + } } diff --git a/src/Core/Spec/Spec12.php b/src/Core/Spec/Spec12.php index 8a0e49d8..9d08dd66 100644 --- a/src/Core/Spec/Spec12.php +++ b/src/Core/Spec/Spec12.php @@ -87,4 +87,9 @@ public function supportsDependencies(): bool { return true; } + + public function supportsExternalReferenceHashes(): bool + { + return false; + } } diff --git a/src/Core/Spec/Spec13.php b/src/Core/Spec/Spec13.php index 5fd15c70..0617f5f8 100644 --- a/src/Core/Spec/Spec13.php +++ b/src/Core/Spec/Spec13.php @@ -87,4 +87,9 @@ public function supportsDependencies(): bool { return true; } + + public function supportsExternalReferenceHashes(): bool + { + return true; + } } diff --git a/src/Core/Spec/SpecInterface.php b/src/Core/Spec/SpecInterface.php index b73fbcf7..492a688a 100644 --- a/src/Core/Spec/SpecInterface.php +++ b/src/Core/Spec/SpecInterface.php @@ -82,4 +82,9 @@ public function supportsBomRef(): bool; * version < 1.2 does not support BomRef. */ public function supportsDependencies(): bool; + + /** + * version < 1.3 does not support hashes in ExternalReference. + */ + public function supportsExternalReferenceHashes(): bool; } diff --git a/tests/Core/Enums/ExternalReferenceTypeTest.php b/tests/Core/Enums/ExternalReferenceTypeTest.php new file mode 100644 index 00000000..21f4ddd4 --- /dev/null +++ b/tests/Core/Enums/ExternalReferenceTypeTest.php @@ -0,0 +1,76 @@ +getConstants(); + foreach ($allValues as $value) { + yield $value => [$value, true]; + } + } + + public function dpUnknownValue(): \Generator + { + yield 'invalid' => ['UnknownExternalReferenceType', false]; + } + + /** + * @dataProvider dpSchemaValues + */ + public function testIsValidKnowsAllSchemaValues(string $value): void + { + self::assertTrue(ExternalReferenceType::isValidValue($value)); + } + + public function dpSchemaValues(): \Generator + { + $allValues = array_unique(array_merge( + BomSpecData::getExternalReferenceTypeForVersion('1.1'), + BomSpecData::getExternalReferenceTypeForVersion('1.2'), + BomSpecData::getExternalReferenceTypeForVersion('1.3'), + )); + foreach ($allValues as $value) { + yield $value => [$value]; + } + } +} diff --git a/tests/Core/Models/BomTest.php b/tests/Core/Models/BomTest.php index f6205242..dbac1823 100644 --- a/tests/Core/Models/BomTest.php +++ b/tests/Core/Models/BomTest.php @@ -26,6 +26,7 @@ use CycloneDX\Core\Models\Bom; use CycloneDX\Core\Models\MetaData; use CycloneDX\Core\Repositories\ComponentRepository; +use CycloneDX\Core\Repositories\ExternalReferenceRepository; use PHPUnit\Framework\TestCase; /** @@ -40,8 +41,6 @@ class BomTest extends TestCase protected function setUp(): void { - parent::setUp(); - $this->bom = new Bom($this->createStub(ComponentRepository::class)); } @@ -50,7 +49,8 @@ protected function setUp(): void public function testComponentsSetterGetter(): void { $components = $this->createStub(ComponentRepository::class); - $this->bom->setComponentRepository($components); + $bom = $this->bom->setComponentRepository($components); + self::assertSame($this->bom, $bom); self::assertSame($components, $this->bom->getComponentRepository()); } @@ -61,7 +61,8 @@ public function testComponentsSetterGetter(): void public function testVersionSetterGetter(): void { $version = random_int(1, 255); - $this->bom->setVersion($version); + $bom = $this->bom->setVersion($version); + self::assertSame($this->bom, $bom); self::assertSame($version, $this->bom->getVersion()); } @@ -79,9 +80,22 @@ public function testVersionSetterInvalidValue(): void public function testMetaDataSetterGetter(): void { $metaData = $this->createStub(MetaData::class); - $this->bom->setMetaData($metaData); + $bom = $this->bom->setMetaData($metaData); + self::assertSame($this->bom, $bom); self::assertSame($metaData, $this->bom->getMetaData()); } // endregion metaData setter&getter + + // region externalReferenceRepository setter&getter + + public function testExternalReferenceRepositorySetterGetter(): void + { + $extRefRepo = $this->createStub(ExternalReferenceRepository::class); + $bom = $this->bom->setExternalReferenceRepository($extRefRepo); + self::assertSame($this->bom, $bom); + self::assertSame($extRefRepo, $this->bom->getExternalReferenceRepository()); + } + + // endregion externalReferenceRepository setter&getter } diff --git a/tests/Core/Models/ComponentTest.php b/tests/Core/Models/ComponentTest.php index 51e93677..791abba2 100644 --- a/tests/Core/Models/ComponentTest.php +++ b/tests/Core/Models/ComponentTest.php @@ -29,6 +29,7 @@ use CycloneDX\Core\Models\License\LicenseExpression; use CycloneDX\Core\Repositories\BomRefRepository; use CycloneDX\Core\Repositories\DisjunctiveLicenseRepository; +use CycloneDX\Core\Repositories\ExternalReferenceRepository; use CycloneDX\Core\Repositories\HashRepository; use PackageUrl\PackageUrl; use PHPUnit\Framework\TestCase; @@ -244,6 +245,18 @@ public function testDependenciesBomRefRepositorySetterGetter(): void // endregion dependenciesBomRefRepository setter&getter + // region externalReferenceRepository setter&getter + + public function testExternalReferenceRepositorySetterGetter(): void + { + $extRefRepo = $this->createStub(ExternalReferenceRepository::class); + $bom = $this->component->setExternalReferenceRepository($extRefRepo); + self::assertSame($this->component, $bom); + self::assertSame($extRefRepo, $this->component->getExternalReferenceRepository()); + } + + // endregion externalReferenceRepository setter&getter + // region clone public function testCloneHasOwnBom(): void diff --git a/tests/Core/Models/ExternalReferenceTest.php b/tests/Core/Models/ExternalReferenceTest.php new file mode 100644 index 00000000..fdfcd991 --- /dev/null +++ b/tests/Core/Models/ExternalReferenceTest.php @@ -0,0 +1,132 @@ +assertSame(ExternalReferenceType::OTHER, $extRef->getType()); + $this->assertSame('https://localhost/dummy', $extRef->getUrl()); + $this->assertNull($extRef->getComment()); + $this->assertNull($extRef->getHashRepository()); + + return $extRef; + } + + // region test Type + + /** + * @depends testConstructor + */ + public function testTypeSetterAndGetter(ExternalReference $extRef): void + { + $got = $extRef->setType(ExternalReferenceType::CHAT); + $this->assertSame($extRef, $got); + $this->assertSame(ExternalReferenceType::CHAT, $extRef->getType()); + } + + public function testConstructorWithInvalidType(): void + { + $this->expectException(DomainException::class); + new ExternalReference('something else', 'https://localhost/dummy'); + } + + /** + * @depends testConstructor + */ + public function testTypeSetterWithInvalidType(ExternalReference $extRef): void + { + $this->expectException(DomainException::class); + $extRef->setType('something else'); + } + + // endregion test Type + + // region test Url + + /** + * @depends testConstructor + */ + public function testUrlSetterAndGetter(ExternalReference $extRef): void + { + $got = $extRef->setUrl('ftp://localhost/foobar'); + $this->assertSame($extRef, $got); + $this->assertSame('ftp://localhost/foobar', $extRef->getUrl()); + } + + /** + * @depends testConstructor + */ + public function testUrlSetterWithURN(ExternalReference $extRef): void + { + $got = $extRef->setUrl('urn:uuid:bdd819e6-ee8f-42d7-a4d0-166ff44d51e8'); + $this->assertSame($extRef, $got); + $this->assertSame('urn:uuid:bdd819e6-ee8f-42d7-a4d0-166ff44d51e8', $extRef->getUrl()); + } + + // endregion test Url + + // region test Comment + + /** + * @depends testConstructor + */ + public function testCommentSetterAndGetter(ExternalReference $extRef): void + { + $got = $extRef->setComment('foobar'); + $this->assertSame($extRef, $got); + $this->assertSame('foobar', $extRef->getComment()); + } + + // endregion test Comment + + // region test Comment + + /** + * @depends testConstructor + */ + public function testHashesSetterAndGetter(ExternalReference $extRef): void + { + $hashes = $this->createStub(HashRepository::class); + $got = $extRef->setHashRepository($hashes); + $this->assertSame($extRef, $got); + $this->assertSame($hashes, $extRef->getHashRepository()); + } + + // endregion test Comment +} diff --git a/tests/Core/Repositories/ExternalReferenceRepositoryTest.php b/tests/Core/Repositories/ExternalReferenceRepositoryTest.php new file mode 100644 index 00000000..dee87524 --- /dev/null +++ b/tests/Core/Repositories/ExternalReferenceRepositoryTest.php @@ -0,0 +1,77 @@ +getExternalReferences()); + } + + public function testConstructAndGet(): void + { + $externalReference1 = $this->createStub(ExternalReference::class); + $externalReference2 = $this->createStub(ExternalReference::class); + + $repo = new ExternalReferenceRepository( + $externalReference1, + $externalReference2, + $externalReference1, + $externalReference2 + ); + + self::assertCount(2, $repo); + self::assertCount(2, $repo->getExternalReferences()); + self::assertContains($externalReference1, $repo->getExternalReferences()); + self::assertContains($externalReference2, $repo->getExternalReferences()); + } + + public function testAddAndGetExternalReference(): void + { + $externalReference1 = $this->createStub(ExternalReference::class); + $externalReference2 = $this->createStub(ExternalReference::class); + $externalReference3 = $this->createStub(ExternalReference::class); + $repo = new ExternalReferenceRepository($externalReference1, $externalReference2); + + $actual = $repo->addExternalReference($externalReference2, $externalReference3, $externalReference3); + + self::assertSame($repo, $actual); + self::assertCount(3, $repo); + self::assertCount(3, $repo->getExternalReferences()); + self::assertContains($externalReference1, $repo->getExternalReferences()); + self::assertContains($externalReference2, $repo->getExternalReferences()); + self::assertContains($externalReference3, $repo->getExternalReferences()); + } +} diff --git a/tests/Core/Serialize/DOM/NormalizerFactoryTest.php b/tests/Core/Serialize/DOM/NormalizerFactoryTest.php index 063afb63..33f1d357 100644 --- a/tests/Core/Serialize/DOM/NormalizerFactoryTest.php +++ b/tests/Core/Serialize/DOM/NormalizerFactoryTest.php @@ -250,4 +250,28 @@ public function testMakeForDependencies(NormalizerFactory $factory): void self::assertInstanceOf(Normalizers\DependenciesNormalizer::class, $normalizer); self::assertSame($factory, $normalizer->getNormalizerFactory()); } + + /** + * @depends testConstructor + * + * @uses \CycloneDX\Core\Serialize\DOM\Normalizers\ExternalReferenceNormalizer + */ + public function testMakeForExternalReference(NormalizerFactory $factory): void + { + $normalizer = $factory->makeForExternalReference(); + self::assertInstanceOf(Normalizers\ExternalReferenceNormalizer::class, $normalizer); + self::assertSame($factory, $normalizer->getNormalizerFactory()); + } + + /** + * @depends testConstructor + * + * @uses \CycloneDX\Core\Serialize\DOM\Normalizers\ExternalReferenceRepositoryNormalizer + */ + public function testMakeForExternalReferenceRepository(NormalizerFactory $factory): void + { + $normalizer = $factory->makeForExternalReferenceRepository(); + self::assertInstanceOf(Normalizers\ExternalReferenceRepositoryNormalizer::class, $normalizer); + self::assertSame($factory, $normalizer->getNormalizerFactory()); + } } diff --git a/tests/Core/Serialize/DOM/Normalizers/BomNormalizerTest.php b/tests/Core/Serialize/DOM/Normalizers/BomNormalizerTest.php index fb1105a0..815a4c98 100644 --- a/tests/Core/Serialize/DOM/Normalizers/BomNormalizerTest.php +++ b/tests/Core/Serialize/DOM/Normalizers/BomNormalizerTest.php @@ -26,11 +26,9 @@ use CycloneDX\Core\Models\Bom; use CycloneDX\Core\Models\MetaData; use CycloneDX\Core\Repositories\ComponentRepository; +use CycloneDX\Core\Repositories\ExternalReferenceRepository; use CycloneDX\Core\Serialize\DOM\NormalizerFactory; -use CycloneDX\Core\Serialize\DOM\Normalizers\BomNormalizer; -use CycloneDX\Core\Serialize\DOM\Normalizers\ComponentRepositoryNormalizer; -use CycloneDX\Core\Serialize\DOM\Normalizers\DependenciesNormalizer; -use CycloneDX\Core\Serialize\DOM\Normalizers\MetaDataNormalizer; +use CycloneDX\Core\Serialize\DOM\Normalizers; use CycloneDX\Core\Spec\SpecInterface; use CycloneDX\Tests\_traits\DomNodeAssertionTrait; use DOMDocument; @@ -55,7 +53,7 @@ public function testNormalize(): void 'getDocument' => new DOMDocument(), ] ); - $normalizer = new BomNormalizer($factory); + $normalizer = new Normalizers\BomNormalizer($factory); $bom = $this->createConfiguredMock( Bom::class, [ @@ -76,7 +74,7 @@ public function testNormalize(): void public function testNormalizeComponents(): void { $spec = $this->createConfiguredMock(SpecInterface::class, ['getVersion' => '1.2']); - $componentsNormalizer = $this->createMock(ComponentRepositoryNormalizer::class); + $componentsNormalizer = $this->createMock(Normalizers\ComponentRepositoryNormalizer::class); $factory = $this->createConfiguredMock( NormalizerFactory::class, [ @@ -85,7 +83,7 @@ public function testNormalizeComponents(): void 'makeForComponentRepository' => $componentsNormalizer, ] ); - $normalizer = new BomNormalizer($factory); + $normalizer = new Normalizers\BomNormalizer($factory); $bom = $this->createConfiguredMock( Bom::class, [ @@ -118,7 +116,7 @@ public function testNormalizeMetaData(): void 'supportsMetaData' => true, ] ); - $metadataNormalizer = $this->createMock(MetaDataNormalizer::class); + $metadataNormalizer = $this->createMock(Normalizers\MetaDataNormalizer::class); $factory = $this->createConfiguredMock( NormalizerFactory::class, [ @@ -127,7 +125,7 @@ public function testNormalizeMetaData(): void 'makeForMetaData' => $metadataNormalizer, ] ); - $normalizer = new BomNormalizer($factory); + $normalizer = new Normalizers\BomNormalizer($factory); $bom = $this->createConfiguredMock( Bom::class, [ @@ -161,7 +159,7 @@ public function testNormalizeMetaDataNotSupported(): void 'supportsMetaData' => false, ] ); - $metadataNormalizer = $this->createMock(MetaDataNormalizer::class); + $metadataNormalizer = $this->createMock(Normalizers\MetaDataNormalizer::class); $factory = $this->createConfiguredMock( NormalizerFactory::class, [ @@ -170,7 +168,7 @@ public function testNormalizeMetaDataNotSupported(): void 'makeForMetaData' => $metadataNormalizer, ] ); - $normalizer = new BomNormalizer($factory); + $normalizer = new Normalizers\BomNormalizer($factory); $bom = $this->createConfiguredMock( Bom::class, [ @@ -202,7 +200,7 @@ public function testNormalizeDependencies(): void 'supportsDependencies' => true, ] ); - $dependencyNormalizer = $this->createMock(DependenciesNormalizer::class); + $dependencyNormalizer = $this->createMock(Normalizers\DependenciesNormalizer::class); $factory = $this->createConfiguredMock( NormalizerFactory::class, [ @@ -211,7 +209,7 @@ public function testNormalizeDependencies(): void 'makeForDependencies' => $dependencyNormalizer, ] ); - $normalizer = new BomNormalizer($factory); + $normalizer = new Normalizers\BomNormalizer($factory); $bom = $this->createConfiguredMock( Bom::class, [ @@ -245,7 +243,7 @@ public function testNormalizeDependenciesOmitWhenEmpty(): void 'supportsDependencies' => true, ] ); - $dependencyNormalizer = $this->createMock(DependenciesNormalizer::class); + $dependencyNormalizer = $this->createMock(Normalizers\DependenciesNormalizer::class); $factory = $this->createConfiguredMock( NormalizerFactory::class, [ @@ -254,7 +252,7 @@ public function testNormalizeDependenciesOmitWhenEmpty(): void 'makeForDependencies' => $dependencyNormalizer, ] ); - $normalizer = new BomNormalizer($factory); + $normalizer = new Normalizers\BomNormalizer($factory); $bom = $this->createConfiguredMock( Bom::class, [ @@ -278,4 +276,92 @@ public function testNormalizeDependenciesOmitWhenEmpty(): void $actual ); } + + // region normalize ExternalReferences + + public function testNormalizeExternalReferences(): void + { + $spec = $this->createConfiguredMock( + SpecInterface::class, + [ + 'getVersion' => '1.2', + ] + ); + $externalReferenceRepositoryNormalizer = $this->createMock(Normalizers\ExternalReferenceRepositoryNormalizer::class); + $factory = $this->createConfiguredMock( + NormalizerFactory::class, + [ + 'getSpec' => $spec, + 'getDocument' => new DOMDocument(), + 'makeForExternalReferenceRepository' => $externalReferenceRepositoryNormalizer, + ] + ); + $normalizer = new Normalizers\BomNormalizer($factory); + $bom = $this->createConfiguredMock( + Bom::class, + [ + 'getVersion' => 23, + 'getExternalReferenceRepository' => $this->createConfiguredMock(ExternalReferenceRepository::class, ['count' => 1]), + ] + ); + + $externalReferenceRepositoryNormalizer->expects(self::once()) + ->method('normalize') + ->with($bom->getExternalReferenceRepository()) + ->willReturn([$factory->getDocument()->createElement('FakeReferenceRepositoryNormalized', 'faked')]); + + $actual = $normalizer->normalize($bom); + + self::assertStringEqualsDomNode( + ''. + ''. + 'faked'. + '', + $actual + ); + } + + public function testNormalizeExternalReferencesOmitIfEmpty(): void + { + $spec = $this->createConfiguredMock( + SpecInterface::class, + [ + 'getVersion' => '1.2', + ] + ); + $externalReferenceRepositoryNormalizer = $this->createMock(Normalizers\ExternalReferenceRepositoryNormalizer::class); + $factory = $this->createConfiguredMock( + NormalizerFactory::class, + [ + 'getSpec' => $spec, + 'getDocument' => new DOMDocument(), + 'makeForExternalReferenceRepository' => $externalReferenceRepositoryNormalizer, + ] + ); + $normalizer = new Normalizers\BomNormalizer($factory); + $bom = $this->createConfiguredMock( + Bom::class, + [ + 'getVersion' => 23, + 'getExternalReferenceRepository' => $this->createConfiguredMock(ExternalReferenceRepository::class, ['count' => 0]), + ] + ); + + $externalReferenceRepositoryNormalizer->expects(self::never()) + ->method('normalize') + ->with($bom->getExternalReferenceRepository()) + ->willReturn(['FakeReferenceRepositoryNormalized']); + + $actual = $normalizer->normalize($bom); + + self::assertStringEqualsDomNode( + ''. + ''. + // externalReferences are omitted + '', + $actual + ); + } + + // endregion normalize ExternalReferences } diff --git a/tests/Core/Serialize/DOM/Normalizers/ComponentNormalizerTest.php b/tests/Core/Serialize/DOM/Normalizers/ComponentNormalizerTest.php index 19f11acd..f2d10434 100644 --- a/tests/Core/Serialize/DOM/Normalizers/ComponentNormalizerTest.php +++ b/tests/Core/Serialize/DOM/Normalizers/ComponentNormalizerTest.php @@ -28,12 +28,10 @@ use CycloneDX\Core\Models\License\DisjunctiveLicenseWithName; use CycloneDX\Core\Models\License\LicenseExpression; use CycloneDX\Core\Repositories\DisjunctiveLicenseRepository; +use CycloneDX\Core\Repositories\ExternalReferenceRepository; use CycloneDX\Core\Repositories\HashRepository; use CycloneDX\Core\Serialize\DOM\NormalizerFactory; -use CycloneDX\Core\Serialize\DOM\Normalizers\ComponentNormalizer; -use CycloneDX\Core\Serialize\DOM\Normalizers\DisjunctiveLicenseRepositoryNormalizer; -use CycloneDX\Core\Serialize\DOM\Normalizers\HashRepositoryNormalizer; -use CycloneDX\Core\Serialize\DOM\Normalizers\LicenseExpressionNormalizer; +use CycloneDX\Core\Serialize\DOM\Normalizers; use CycloneDX\Core\Spec\SpecInterface; use CycloneDX\Tests\_traits\DomNodeAssertionTrait; use DomainException; @@ -62,7 +60,7 @@ public function testNormalizeThrowsOnUnsupportedType(): void ); $spec = $this->createMock(SpecInterface::class); $factory = $this->createConfiguredMock(NormalizerFactory::class, ['getSpec' => $spec]); - $normalizer = new ComponentNormalizer($factory); + $normalizer = new Normalizers\ComponentNormalizer($factory); $spec->expects(self::once())->method('isSupportedComponentType') ->with('FakeType') @@ -94,7 +92,7 @@ public function testNormalizeMinimal(): void NormalizerFactory::class, ['getSpec' => $spec, 'getDocument' => new DOMDocument()] ); - $normalizer = new ComponentNormalizer($factory); + $normalizer = new Normalizers\ComponentNormalizer($factory); $spec->expects(self::once())->method('isSupportedComponentType') ->with('FakeType') @@ -137,8 +135,8 @@ public function testNormalizeFull(): void 'supportsBomRef' => true, ] ); - $licenseExpressionNormalizer = $this->createMock(LicenseExpressionNormalizer::class); - $hashRepositoryNormalizer = $this->createMock(HashRepositoryNormalizer::class); + $licenseExpressionNormalizer = $this->createMock(Normalizers\LicenseExpressionNormalizer::class); + $hashRepositoryNormalizer = $this->createMock(Normalizers\HashRepositoryNormalizer::class); $factory = $this->createConfiguredMock( NormalizerFactory::class, [ @@ -148,7 +146,7 @@ public function testNormalizeFull(): void 'makeForHashRepository' => $hashRepositoryNormalizer, ] ); - $normalizer = new ComponentNormalizer($factory); + $normalizer = new Normalizers\ComponentNormalizer($factory); $spec->expects(self::once())->method('isSupportedComponentType') ->with('FakeType') @@ -198,7 +196,7 @@ public function testNormalizeUnsupportedLicenseExpression(): void 'supportsLicenseExpression' => false, ] ); - $licenseNormalizer = $this->createMock(DisjunctiveLicenseRepositoryNormalizer::class); + $licenseNormalizer = $this->createMock(Normalizers\DisjunctiveLicenseRepositoryNormalizer::class); $factory = $this->createConfiguredMock( NormalizerFactory::class, [ @@ -207,7 +205,7 @@ public function testNormalizeUnsupportedLicenseExpression(): void 'makeForDisjunctiveLicenseRepository' => $licenseNormalizer, ] ); - $normalizer = new ComponentNormalizer($factory); + $normalizer = new Normalizers\ComponentNormalizer($factory); $transformedLicenseTest = static function (DisjunctiveLicenseRepository $licenses): bool { $licenses = $licenses->getLicenses(); @@ -250,7 +248,7 @@ public function testNormalizeDisjunctiveLicenses(): void ] ); $spec = $this->createMock(SpecInterface::class); - $licenseNormalizer = $this->createMock(DisjunctiveLicenseRepositoryNormalizer::class); + $licenseNormalizer = $this->createMock(Normalizers\DisjunctiveLicenseRepositoryNormalizer::class); $factory = $this->createConfiguredMock( NormalizerFactory::class, [ @@ -259,7 +257,7 @@ public function testNormalizeDisjunctiveLicenses(): void 'makeForDisjunctiveLicenseRepository' => $licenseNormalizer, ] ); - $normalizer = new ComponentNormalizer($factory); + $normalizer = new Normalizers\ComponentNormalizer($factory); $spec->expects(self::once())->method('isSupportedComponentType') ->with('FakeType') @@ -292,7 +290,7 @@ public function testNormalizeDisjunctiveLicensesEmpty(): void ] ); $spec = $this->createMock(SpecInterface::class); - $licenseNormalizer = $this->createMock(DisjunctiveLicenseRepositoryNormalizer::class); + $licenseNormalizer = $this->createMock(Normalizers\DisjunctiveLicenseRepositoryNormalizer::class); $factory = $this->createConfiguredMock( NormalizerFactory::class, [ @@ -301,7 +299,7 @@ public function testNormalizeDisjunctiveLicensesEmpty(): void 'makeForDisjunctiveLicenseRepository' => $licenseNormalizer, ] ); - $normalizer = new ComponentNormalizer($factory); + $normalizer = new Normalizers\ComponentNormalizer($factory); $spec->expects(self::once())->method('isSupportedComponentType') ->with('FakeType') @@ -318,4 +316,90 @@ public function testNormalizeDisjunctiveLicensesEmpty(): void $got ); } + + // region normalize ExternalReferences + + public function testNormalizeExternalReferences(): void + { + $component = $this->createConfiguredMock( + Component::class, + [ + 'getName' => 'myName', + 'getVersion' => 'some-version', + 'getType' => 'FakeType', + 'getExternalReferenceRepository' => $this->createConfiguredMock(ExternalReferenceRepository::class, ['count' => 1]), + ] + ); + $spec = $this->createMock(SpecInterface::class); + $externalReferenceRepositoryNormalizer = $this->createMock(Normalizers\ExternalReferenceRepositoryNormalizer::class); + $factory = $this->createConfiguredMock( + NormalizerFactory::class, + [ + 'getSpec' => $spec, + 'getDocument' => new DOMDocument(), + 'makeForExternalReferenceRepository' => $externalReferenceRepositoryNormalizer, + ] + ); + $normalizer = new Normalizers\ComponentNormalizer($factory); + + $spec->expects(self::once())->method('isSupportedComponentType') + ->with('FakeType') + ->willReturn(true); + $externalReferenceRepositoryNormalizer->expects(self::once()) + ->method('normalize') + ->with($component->getExternalReferenceRepository()) + ->willReturn([$factory->getDocument()->createElement('FakeExternalReference', 'dummy')]); + + $actual = $normalizer->normalize($component); + + self::assertStringEqualsDomNode( + ''. + 'myName'. + 'some-version'. + 'dummy'. + '', + $actual + ); + } + + public function testNormalizeExternalReferencesOmitIfEmpty(): void + { + $component = $this->createConfiguredMock( + Component::class, + [ + 'getName' => 'myName', + 'getVersion' => 'some-version', + 'getType' => 'FakeType', + 'getExternalReferenceRepository' => $this->createConfiguredMock(ExternalReferenceRepository::class, ['count' => 0]), + ] + ); + $spec = $this->createMock(SpecInterface::class); + $externalReferenceRepositoryNormalizer = $this->createMock(Normalizers\ExternalReferenceRepositoryNormalizer::class); + $factory = $this->createConfiguredMock( + NormalizerFactory::class, + [ + 'getSpec' => $spec, + 'getDocument' => new DOMDocument(), + 'makeForExternalReferenceRepository' => $externalReferenceRepositoryNormalizer, + ] + ); + $normalizer = new Normalizers\ComponentNormalizer($factory); + + $spec->expects(self::once())->method('isSupportedComponentType') + ->with('FakeType') + ->willReturn(true); + $externalReferenceRepositoryNormalizer->expects(self::never())->method('normalize'); + + $actual = $normalizer->normalize($component); + + self::assertStringEqualsDomNode( + ''. + 'myName'. + 'some-version'. + '', + $actual + ); + } + + // endregion normalize ExternalReferences } diff --git a/tests/Core/Serialize/DOM/Normalizers/ExternalReferenceNormalizerTest.php b/tests/Core/Serialize/DOM/Normalizers/ExternalReferenceNormalizerTest.php new file mode 100644 index 00000000..0dab4245 --- /dev/null +++ b/tests/Core/Serialize/DOM/Normalizers/ExternalReferenceNormalizerTest.php @@ -0,0 +1,195 @@ +createConfiguredMock(NormalizerFactory::class, [ + 'getDocument' => new \DOMDocument(), + ]); + $normalizer = new Normalizers\ExternalReferenceNormalizer($normalizerFactory); + $extRef = $this->createConfiguredMock(ExternalReference::class, [ + 'getUrl' => 'someUrl', + 'getType' => 'someType', + 'getComment' => null, + 'getHashRepository' => null, + ]); + + $actual = $normalizer->normalize($extRef); + + self::assertStringEqualsDomNode( + 'someUrl', + $actual + ); + } + + /** + * @throws \Exception on assertion error + */ + public function testNormalizeComment(): void + { + $normalizerFactory = $this->createConfiguredMock(NormalizerFactory::class, [ + 'getDocument' => new \DOMDocument(), + ]); + $normalizer = new Normalizers\ExternalReferenceNormalizer($normalizerFactory); + $extRef = $this->createConfiguredMock(ExternalReference::class, [ + 'getUrl' => 'someUrl', + 'getType' => 'someType', + 'getComment' => 'someComment', + 'getHashRepository' => null, + ]); + + $actual = $normalizer->normalize($extRef); + + self::assertStringEqualsDomNode( + 'someUrlsomeComment', + $actual + ); + } + + // region normalize hashes + + /** + * @throws \Exception on assertion error + */ + public function testNormalizeHashes(): void + { + $hashRepositoryNormalizer = $this->createMock(Normalizers\HashRepositoryNormalizer::class); + $normalizerFactory = $this->createConfiguredMock(NormalizerFactory::class, [ + 'getSpec' => $this->createConfiguredMock(SpecInterface::class, [ + 'supportsExternalReferenceHashes' => true, + ]), + 'getDocument' => new \DOMDocument(), + 'makeForHashRepository' => $hashRepositoryNormalizer, + ]); + $normalizer = new Normalizers\ExternalReferenceNormalizer($normalizerFactory); + $extRef = $this->createConfiguredMock(ExternalReference::class, [ + 'getUrl' => 'someUrl', + 'getType' => 'someType', + 'getComment' => null, + 'getHashRepository' => $this->createConfiguredMock(HashRepository::class, ['count' => 1]), + ]); + + $hashRepositoryNormalizer->expects(self::once()) + ->method('normalize') + ->with($extRef->getHashRepository()) + ->willReturn([$normalizerFactory->getDocument()->createElement('FakeHash', 'dummy')]); + + $actual = $normalizer->normalize($extRef); + + self::assertStringEqualsDomNode( + ''. + 'someUrl'. + 'dummy'. + '', + $actual + ); + } + + /** + * @throws \Exception on assertion error + */ + public function testNormalizeHashesOmitIfEmpty(): void + { + $hashRepositoryNormalizer = $this->createMock(Normalizers\HashRepositoryNormalizer::class); + $normalizerFactory = $this->createConfiguredMock(NormalizerFactory::class, [ + 'getSpec' => $this->createConfiguredMock(SpecInterface::class, [ + 'supportsExternalReferenceHashes' => true, + ]), + 'getDocument' => new \DOMDocument(), + 'makeForHashRepository' => $hashRepositoryNormalizer, + ]); + $normalizer = new Normalizers\ExternalReferenceNormalizer($normalizerFactory); + $extRef = $this->createConfiguredMock(ExternalReference::class, [ + 'getUrl' => 'someUrl', + 'getType' => 'someType', + 'getComment' => null, + 'getHashRepository' => $this->createConfiguredMock(HashRepository::class, ['count' => 0]), + ]); + + $hashRepositoryNormalizer->expects(self::never()) + ->method('normalize') + ->with($extRef->getHashRepository()); + + $actual = $normalizer->normalize($extRef); + + self::assertStringEqualsDomNode( + 'someUrl', + $actual + ); + } + + /** + * @throws \Exception on assertion error + */ + public function testNormalizeHashesOmitIfNotSupported(): void + { + $hashRepositoryNormalizer = $this->createMock(Normalizers\HashRepositoryNormalizer::class); + $normalizerFactory = $this->createConfiguredMock(NormalizerFactory::class, [ + 'getSpec' => $this->createConfiguredMock(SpecInterface::class, [ + 'supportsExternalReferenceHashes' => false, + ]), + 'getDocument' => new \DOMDocument(), + 'makeForHashRepository' => $hashRepositoryNormalizer, + ]); + $normalizer = new Normalizers\ExternalReferenceNormalizer($normalizerFactory); + $extRef = $this->createConfiguredMock(ExternalReference::class, [ + 'getUrl' => 'someUrl', + 'getType' => 'someType', + 'getComment' => null, + 'getHashRepository' => $this->createConfiguredMock(HashRepository::class, ['count' => 1]), + ]); + + $hashRepositoryNormalizer->expects(self::never()) + ->method('normalize') + ->with($extRef->getHashRepository()); + + $actual = $normalizer->normalize($extRef); + + self::assertStringEqualsDomNode( + 'someUrl', + $actual + ); + } + + // endregion normalize hashes +} diff --git a/tests/Core/Serialize/DOM/Normalizers/ExternalReferenceRepositoryNormalizerTest.php b/tests/Core/Serialize/DOM/Normalizers/ExternalReferenceRepositoryNormalizerTest.php new file mode 100644 index 00000000..a75ab026 --- /dev/null +++ b/tests/Core/Serialize/DOM/Normalizers/ExternalReferenceRepositoryNormalizerTest.php @@ -0,0 +1,111 @@ +createStub(SpecInterface::class); + $externalReferenceNormalizer = $this->createMock(Normalizers\ExternalReferenceNormalizer::class); + $factory = $this->createConfiguredMock(NormalizerFactory::class, [ + 'getSpec' => $spec, + 'makeForExternalReference' => $externalReferenceNormalizer, + ]); + + $normalizer = new Normalizers\ExternalReferenceRepositoryNormalizer($factory); + $repo = $this->createConfiguredMock(ExternalReferenceRepository::class, ['count' => 0]); + + $actual = $normalizer->normalize($repo); + + self::assertSame([], $actual); + } + + public function testNormalize(): void + { + $spec = $this->createStub(SpecInterface::class); + $externalReferenceNormalizer = $this->createMock(Normalizers\ExternalReferenceNormalizer::class); + $factory = $this->createConfiguredMock(NormalizerFactory::class, [ + 'getSpec' => $spec, + 'makeForExternalReference' => $externalReferenceNormalizer, + ]); + $normalizer = new Normalizers\ExternalReferenceRepositoryNormalizer($factory); + $externalReference = $this->createStub(ExternalReference::class); + $repo = $this->createConfiguredMock(ExternalReferenceRepository::class, [ + 'count' => 1, + 'getExternalReferences' => [$externalReference], + ]); + $FakeExtRef = $this->createStub(DOMElement::class); + + $externalReferenceNormalizer->expects(self::once()) + ->method('normalize') + ->with($externalReference) + ->willReturn($FakeExtRef); + + $actual = $normalizer->normalize($repo); + + self::assertSame([$FakeExtRef], $actual); + } + + public function testNormalizeSkipsOnThrow(): void + { + $spec = $this->createStub(SpecInterface::class); + $externalReferenceNormalizer = $this->createMock(Normalizers\ExternalReferenceNormalizer::class); + $factory = $this->createConfiguredMock(NormalizerFactory::class, [ + 'getSpec' => $spec, + 'makeForExternalReference' => $externalReferenceNormalizer, + ]); + $normalizer = new Normalizers\ExternalReferenceRepositoryNormalizer($factory); + $extRef1 = $this->createStub(ExternalReference::class); + $extRef2 = $this->createStub(ExternalReference::class); + $tools = $this->createConfiguredMock(ExternalReferenceRepository::class, [ + 'count' => 1, + 'getExternalReferences' => [$extRef1, $extRef2], + ]); + + $externalReferenceNormalizer->expects(self::exactly(2))->method('normalize') + ->withConsecutive([$extRef1], [$extRef2]) + ->willThrowException(new \DomainException()); + + $actual = $normalizer->normalize($tools); + + self::assertSame([], $actual); + } +} diff --git a/tests/Core/Serialize/JSON/NormalizerFactoryTest.php b/tests/Core/Serialize/JSON/NormalizerFactoryTest.php index 1830e897..25dc117c 100644 --- a/tests/Core/Serialize/JSON/NormalizerFactoryTest.php +++ b/tests/Core/Serialize/JSON/NormalizerFactoryTest.php @@ -241,7 +241,7 @@ public function testMakeForTool(NormalizerFactory $factory): void /** * @depends testConstructor * - * @uses \CycloneDX\Core\Serialize\DOM\Normalizers\ToolNormalizer + * @uses \CycloneDX\Core\Serialize\JSON\Normalizers\ToolNormalizer * @uses \CycloneDX\Core\Serialize\JSON\Normalizers\DependenciesNormalizer */ public function testMakeForDependencies(NormalizerFactory $factory): void @@ -250,4 +250,28 @@ public function testMakeForDependencies(NormalizerFactory $factory): void self::assertInstanceOf(Normalizers\DependenciesNormalizer::class, $normalizer); self::assertSame($factory, $normalizer->getNormalizerFactory()); } + + /** + * @depends testConstructor + * + * @uses \CycloneDX\Core\Serialize\JSON\Normalizers\ExternalReferenceNormalizer + */ + public function testMakeForExternalReference(NormalizerFactory $factory): void + { + $normalizer = $factory->makeForExternalReference(); + self::assertInstanceOf(Normalizers\ExternalReferenceNormalizer::class, $normalizer); + self::assertSame($factory, $normalizer->getNormalizerFactory()); + } + + /** + * @depends testConstructor + * + * @uses \CycloneDX\Core\Serialize\JSON\Normalizers\ExternalReferenceRepositoryNormalizer + */ + public function testMakeForExternalReferenceRepository(NormalizerFactory $factory): void + { + $normalizer = $factory->makeForExternalReferenceRepository(); + self::assertInstanceOf(Normalizers\ExternalReferenceRepositoryNormalizer::class, $normalizer); + self::assertSame($factory, $normalizer->getNormalizerFactory()); + } } diff --git a/tests/Core/Serialize/JSON/Normalizers/BomNormalizerTest.php b/tests/Core/Serialize/JSON/Normalizers/BomNormalizerTest.php index 80b028ed..10a52f7b 100644 --- a/tests/Core/Serialize/JSON/Normalizers/BomNormalizerTest.php +++ b/tests/Core/Serialize/JSON/Normalizers/BomNormalizerTest.php @@ -26,11 +26,9 @@ use CycloneDX\Core\Models\Bom; use CycloneDX\Core\Models\MetaData; use CycloneDX\Core\Repositories\ComponentRepository; +use CycloneDX\Core\Repositories\ExternalReferenceRepository; use CycloneDX\Core\Serialize\JSON\NormalizerFactory; -use CycloneDX\Core\Serialize\JSON\Normalizers\BomNormalizer; -use CycloneDX\Core\Serialize\JSON\Normalizers\ComponentRepositoryNormalizer; -use CycloneDX\Core\Serialize\JSON\Normalizers\DependenciesNormalizer; -use CycloneDX\Core\Serialize\JSON\Normalizers\MetaDataNormalizer; +use CycloneDX\Core\Serialize\JSON\Normalizers; use CycloneDX\Core\Spec\SpecInterface; use PHPUnit\Framework\TestCase; @@ -52,7 +50,7 @@ public function testNormalize(): void 'getSpec' => $spec, ] ); - $normalizer = new BomNormalizer($factory); + $normalizer = new Normalizers\BomNormalizer($factory); $bom = $this->createConfiguredMock( Bom::class, [ @@ -60,7 +58,7 @@ public function testNormalize(): void ] ); - $got = $normalizer->normalize($bom); + $actual = $normalizer->normalize($bom); self::assertSame( [ @@ -69,14 +67,14 @@ public function testNormalize(): void 'version' => 23, 'components' => [], ], - $got + $actual ); } public function testNormalizeComponents(): void { $spec = $this->createConfiguredMock(SpecInterface::class, ['getVersion' => '1.2']); - $componentsNormalizer = $this->createMock(ComponentRepositoryNormalizer::class); + $componentsNormalizer = $this->createMock(Normalizers\ComponentRepositoryNormalizer::class); $factory = $this->createConfiguredMock( NormalizerFactory::class, [ @@ -84,7 +82,7 @@ public function testNormalizeComponents(): void 'makeForComponentRepository' => $componentsNormalizer, ] ); - $normalizer = new BomNormalizer($factory); + $normalizer = new Normalizers\BomNormalizer($factory); $bom = $this->createConfiguredMock( Bom::class, [ @@ -98,7 +96,7 @@ public function testNormalizeComponents(): void ->with($bom->getComponentRepository()) ->willReturn(['FakeComponents']); - $got = $normalizer->normalize($bom); + $actual = $normalizer->normalize($bom); self::assertSame( [ @@ -107,7 +105,7 @@ public function testNormalizeComponents(): void 'version' => 23, 'components' => ['FakeComponents'], ], - $got + $actual ); } @@ -123,7 +121,7 @@ public function testNormalizeMetaData(): void 'supportsMetaData' => true, ] ); - $metadataNormalizer = $this->createMock(MetaDataNormalizer::class); + $metadataNormalizer = $this->createMock(Normalizers\MetaDataNormalizer::class); $factory = $this->createConfiguredMock( NormalizerFactory::class, [ @@ -131,7 +129,7 @@ public function testNormalizeMetaData(): void 'makeForMetaData' => $metadataNormalizer, ] ); - $normalizer = new BomNormalizer($factory); + $normalizer = new Normalizers\BomNormalizer($factory); $bom = $this->createConfiguredMock( Bom::class, [ @@ -145,7 +143,7 @@ public function testNormalizeMetaData(): void ->with($bom->getMetaData()) ->willReturn(['FakeMetaData']); - $got = $normalizer->normalize($bom); + $actual = $normalizer->normalize($bom); self::assertSame( [ @@ -155,7 +153,7 @@ public function testNormalizeMetaData(): void 'metadata' => ['FakeMetaData'], 'components' => [], ], - $got + $actual ); } @@ -168,7 +166,7 @@ public function testNormalizeMetaDataEmpty(): void 'supportsMetaData' => true, ] ); - $metadataNormalizer = $this->createMock(MetaDataNormalizer::class); + $metadataNormalizer = $this->createMock(Normalizers\MetaDataNormalizer::class); $factory = $this->createConfiguredMock( NormalizerFactory::class, [ @@ -176,7 +174,7 @@ public function testNormalizeMetaDataEmpty(): void 'makeForMetaData' => $metadataNormalizer, ] ); - $normalizer = new BomNormalizer($factory); + $normalizer = new Normalizers\BomNormalizer($factory); $bom = $this->createConfiguredMock( Bom::class, [ @@ -189,7 +187,7 @@ public function testNormalizeMetaDataEmpty(): void ->with($bom->getMetaData()) ->willReturn([/* empty */]); - $got = $normalizer->normalize($bom); + $actual = $normalizer->normalize($bom); self::assertSame( [ @@ -198,7 +196,7 @@ public function testNormalizeMetaDataEmpty(): void 'version' => 23, 'components' => [], ], - $got + $actual ); } @@ -211,7 +209,7 @@ public function testNormalizeMetaDataSkipped(): void 'supportsMetaData' => false, ] ); - $metadataNormalizer = $this->createMock(MetaDataNormalizer::class); + $metadataNormalizer = $this->createMock(Normalizers\MetaDataNormalizer::class); $factory = $this->createConfiguredMock( NormalizerFactory::class, [ @@ -219,7 +217,7 @@ public function testNormalizeMetaDataSkipped(): void 'makeForMetaData' => $metadataNormalizer, ] ); - $normalizer = new BomNormalizer($factory); + $normalizer = new Normalizers\BomNormalizer($factory); $bom = $this->createConfiguredMock( Bom::class, [ @@ -232,7 +230,7 @@ public function testNormalizeMetaDataSkipped(): void ->with($bom->getMetaData()) ->willReturn(['FakeMetaData']); - $got = $normalizer->normalize($bom); + $actual = $normalizer->normalize($bom); self::assertSame( [ @@ -241,7 +239,7 @@ public function testNormalizeMetaDataSkipped(): void 'version' => 23, 'components' => [], ], - $got + $actual ); } @@ -254,7 +252,7 @@ public function testNormalizeDependencies(): void 'supportsDependencies' => true, ] ); - $dependencyNormalizer = $this->createMock(DependenciesNormalizer::class); + $dependencyNormalizer = $this->createMock(Normalizers\DependenciesNormalizer::class); $factory = $this->createConfiguredMock( NormalizerFactory::class, [ @@ -262,7 +260,7 @@ public function testNormalizeDependencies(): void 'makeForDependencies' => $dependencyNormalizer, ] ); - $normalizer = new BomNormalizer($factory); + $normalizer = new Normalizers\BomNormalizer($factory); $bom = $this->createConfiguredMock( Bom::class, [ @@ -299,7 +297,7 @@ public function testNormalizeDependenciesOmitWhenEmpty(): void 'supportsDependencies' => true, ] ); - $dependencyNormalizer = $this->createMock(DependenciesNormalizer::class); + $dependencyNormalizer = $this->createMock(Normalizers\DependenciesNormalizer::class); $factory = $this->createConfiguredMock( NormalizerFactory::class, [ @@ -307,7 +305,7 @@ public function testNormalizeDependenciesOmitWhenEmpty(): void 'makeForDependencies' => $dependencyNormalizer, ] ); - $normalizer = new BomNormalizer($factory); + $normalizer = new Normalizers\BomNormalizer($factory); $bom = $this->createConfiguredMock( Bom::class, [ @@ -334,4 +332,95 @@ public function testNormalizeDependenciesOmitWhenEmpty(): void $actual ); } + + // region normalize ExternalReferences + + public function testNormalizeExternalReferences(): void + { + $spec = $this->createConfiguredMock( + SpecInterface::class, + [ + 'getVersion' => '1.2', + ] + ); + $externalReferenceRepositoryNormalizer = $this->createMock(Normalizers\ExternalReferenceRepositoryNormalizer::class); + $factory = $this->createConfiguredMock( + NormalizerFactory::class, + [ + 'getSpec' => $spec, + 'makeForExternalReferenceRepository' => $externalReferenceRepositoryNormalizer, + ] + ); + $normalizer = new Normalizers\BomNormalizer($factory); + $bom = $this->createConfiguredMock( + Bom::class, + [ + 'getVersion' => 23, + 'getExternalReferenceRepository' => $this->createConfiguredMock(ExternalReferenceRepository::class, ['count' => 1]), + ] + ); + + $externalReferenceRepositoryNormalizer->expects(self::once()) + ->method('normalize') + ->with($bom->getExternalReferenceRepository()) + ->willReturn(['FakeReferenceRepositoryNormalized']); + + $actual = $normalizer->normalize($bom); + + self::assertSame( + [ + 'bomFormat' => 'CycloneDX', + 'specVersion' => '1.2', + 'version' => 23, + 'components' => [], + 'externalReferences' => ['FakeReferenceRepositoryNormalized'], + ], + $actual + ); + } + + public function testNormalizeExternalReferencesOmitIfEmpty(): void + { + $spec = $this->createConfiguredMock( + SpecInterface::class, + [ + 'getVersion' => '1.2', + ] + ); + $externalReferenceRepositoryNormalizer = $this->createMock(Normalizers\ExternalReferenceRepositoryNormalizer::class); + $factory = $this->createConfiguredMock( + NormalizerFactory::class, + [ + 'getSpec' => $spec, + 'makeForExternalReferenceRepository' => $externalReferenceRepositoryNormalizer, + ] + ); + $normalizer = new Normalizers\BomNormalizer($factory); + $bom = $this->createConfiguredMock( + Bom::class, + [ + 'getVersion' => 23, + 'getExternalReferenceRepository' => $this->createConfiguredMock(ExternalReferenceRepository::class, ['count' => 0]), + ] + ); + + $externalReferenceRepositoryNormalizer->expects(self::never()) + ->method('normalize') + ->with($bom->getExternalReferenceRepository()) + ->willReturn(['FakeReferenceRepositoryNormalized']); + + $actual = $normalizer->normalize($bom); + + self::assertSame( + [ + 'bomFormat' => 'CycloneDX', + 'specVersion' => '1.2', + 'version' => 23, + 'components' => [], + ], + $actual + ); + } + + // endregion normalize ExternalReferences } diff --git a/tests/Core/Serialize/JSON/Normalizers/ComponentNormalizerTest.php b/tests/Core/Serialize/JSON/Normalizers/ComponentNormalizerTest.php index e6759b92..4eccbbae 100644 --- a/tests/Core/Serialize/JSON/Normalizers/ComponentNormalizerTest.php +++ b/tests/Core/Serialize/JSON/Normalizers/ComponentNormalizerTest.php @@ -28,12 +28,10 @@ use CycloneDX\Core\Models\License\DisjunctiveLicenseWithName; use CycloneDX\Core\Models\License\LicenseExpression; use CycloneDX\Core\Repositories\DisjunctiveLicenseRepository; +use CycloneDX\Core\Repositories\ExternalReferenceRepository; use CycloneDX\Core\Repositories\HashRepository; use CycloneDX\Core\Serialize\JSON\NormalizerFactory; -use CycloneDX\Core\Serialize\JSON\Normalizers\ComponentNormalizer; -use CycloneDX\Core\Serialize\JSON\Normalizers\DisjunctiveLicenseRepositoryNormalizer; -use CycloneDX\Core\Serialize\JSON\Normalizers\HashRepositoryNormalizer; -use CycloneDX\Core\Serialize\JSON\Normalizers\LicenseExpressionNormalizer; +use CycloneDX\Core\Serialize\JSON\Normalizers; use CycloneDX\Core\Spec\SpecInterface; use DomainException; use PackageUrl\PackageUrl; @@ -57,7 +55,7 @@ public function testNormalizeThrowsOnUnsupportedType(): void ); $spec = $this->createMock(SpecInterface::class); $factory = $this->createConfiguredMock(NormalizerFactory::class, ['getSpec' => $spec]); - $normalizer = new ComponentNormalizer($factory); + $normalizer = new Normalizers\ComponentNormalizer($factory); $spec->expects(self::once())->method('isSupportedComponentType') ->with('FakeType') @@ -86,7 +84,7 @@ public function testNormalizeMinimal(): void ); $spec = $this->createMock(SpecInterface::class); $factory = $this->createConfiguredMock(NormalizerFactory::class, ['getSpec' => $spec]); - $normalizer = new ComponentNormalizer($factory); + $normalizer = new Normalizers\ComponentNormalizer($factory); $spec->expects(self::once())->method('isSupportedComponentType') ->with('FakeType') @@ -127,14 +125,14 @@ public function testNormalizeFull(): void 'supportsLicenseExpression' => true, 'supportsBomRef' => true, ]); - $licenseExpressionNormalizer = $this->createMock(LicenseExpressionNormalizer::class); - $hashRepositoryNormalizer = $this->createMock(HashRepositoryNormalizer::class); + $licenseExpressionNormalizer = $this->createMock(Normalizers\LicenseExpressionNormalizer::class); + $hashRepositoryNormalizer = $this->createMock(Normalizers\HashRepositoryNormalizer::class); $factory = $this->createConfiguredMock(NormalizerFactory::class, [ 'getSpec' => $spec, 'makeForLicenseExpression' => $licenseExpressionNormalizer, 'makeForHashRepository' => $hashRepositoryNormalizer, ]); - $normalizer = new ComponentNormalizer($factory); + $normalizer = new Normalizers\ComponentNormalizer($factory); $spec->expects(self::once())->method('isSupportedComponentType') ->with('FakeType') @@ -186,12 +184,12 @@ public function testNormalizeUnsupportedLicenseExpression(): void 'supportsLicenseExpression' => false, ] ); - $licenseNormalizer = $this->createMock(DisjunctiveLicenseRepositoryNormalizer::class); + $licenseNormalizer = $this->createMock(Normalizers\DisjunctiveLicenseRepositoryNormalizer::class); $factory = $this->createConfiguredMock(NormalizerFactory::class, [ 'getSpec' => $spec, 'makeForDisjunctiveLicenseRepository' => $licenseNormalizer, ]); - $normalizer = new ComponentNormalizer($factory); + $normalizer = new Normalizers\ComponentNormalizer($factory); $transformedLicenseTest = static function (DisjunctiveLicenseRepository $licenses): bool { $licenses = $licenses->getLicenses(); @@ -232,12 +230,12 @@ public function testNormalizeDisjunctiveLicenses(): void ] ); $spec = $this->createMock(SpecInterface::class); - $licenseNormalizer = $this->createMock(DisjunctiveLicenseRepositoryNormalizer::class); + $licenseNormalizer = $this->createMock(Normalizers\DisjunctiveLicenseRepositoryNormalizer::class); $factory = $this->createConfiguredMock(NormalizerFactory::class, [ 'getSpec' => $spec, 'makeForDisjunctiveLicenseRepository' => $licenseNormalizer, ]); - $normalizer = new ComponentNormalizer($factory); + $normalizer = new Normalizers\ComponentNormalizer($factory); $spec->expects(self::once())->method('isSupportedComponentType') ->with('FakeType') @@ -268,12 +266,12 @@ public function testNormalizeDisjunctiveLicensesEmpty(): void ] ); $spec = $this->createMock(SpecInterface::class); - $licenseNormalizer = $this->createMock(DisjunctiveLicenseRepositoryNormalizer::class); + $licenseNormalizer = $this->createMock(Normalizers\DisjunctiveLicenseRepositoryNormalizer::class); $factory = $this->createConfiguredMock(NormalizerFactory::class, [ 'getSpec' => $spec, 'makeForDisjunctiveLicenseRepository' => $licenseNormalizer, ]); - $normalizer = new ComponentNormalizer($factory); + $normalizer = new Normalizers\ComponentNormalizer($factory); $spec->expects(self::once())->method('isSupportedComponentType') ->with('FakeType') @@ -288,4 +286,86 @@ public function testNormalizeDisjunctiveLicensesEmpty(): void 'version' => 'some-version', ], $got); } + + // region normalize ExternalReferences + + public function testNormalizeExternalReferences(): void + { + $component = $this->createConfiguredMock( + Component::class, + [ + 'getName' => 'myName', + 'getVersion' => 'some-version', + 'getType' => 'FakeType', + 'getExternalReferenceRepository' => $this->createConfiguredMock(ExternalReferenceRepository::class, ['count' => 1]), + ] + ); + $spec = $this->createMock(SpecInterface::class); + $externalReferenceRepositoryNormalizer = $this->createMock(Normalizers\ExternalReferenceRepositoryNormalizer::class); + $factory = $this->createConfiguredMock( + NormalizerFactory::class, + [ + 'getSpec' => $spec, + 'makeForExternalReferenceRepository' => $externalReferenceRepositoryNormalizer, + ] + ); + $normalizer = new Normalizers\ComponentNormalizer($factory); + + $spec->expects(self::once())->method('isSupportedComponentType') + ->with('FakeType') + ->willReturn(true); + $externalReferenceRepositoryNormalizer->expects(self::once()) + ->method('normalize') + ->with($component->getExternalReferenceRepository()) + ->willReturn(['FakeExternalReference']); + + $actual = $normalizer->normalize($component); + + self::assertSame([ + 'type' => 'FakeType', + 'name' => 'myName', + 'version' => 'some-version', + 'externalReferences' => ['FakeExternalReference'], + ], + $actual + ); + } + + public function testNormalizeExternalReferencesOmitIfEmpty(): void + { + $component = $this->createConfiguredMock( + Component::class, + [ + 'getName' => 'myName', + 'getVersion' => 'some-version', + 'getType' => 'FakeType', + 'getExternalReferenceRepository' => $this->createConfiguredMock(ExternalReferenceRepository::class, ['count' => 0]), + ] + ); + $spec = $this->createMock(SpecInterface::class); + $externalReferenceRepositoryNormalizer = $this->createMock(Normalizers\ExternalReferenceRepositoryNormalizer::class); + $factory = $this->createConfiguredMock( + NormalizerFactory::class, + [ + 'getSpec' => $spec, + 'makeForExternalReferenceRepository' => $externalReferenceRepositoryNormalizer, + ] + ); + $normalizer = new Normalizers\ComponentNormalizer($factory); + + $spec->expects(self::once())->method('isSupportedComponentType') + ->with('FakeType') + ->willReturn(true); + $externalReferenceRepositoryNormalizer->expects(self::never())->method('normalize'); + + $actual = $normalizer->normalize($component); + + self::assertEquals([ + 'type' => 'FakeType', + 'name' => 'myName', + 'version' => 'some-version', + ], $actual); + } + + // endregion normalize ExternalReferences } diff --git a/tests/Core/Serialize/JSON/Normalizers/ExternalReferenceNormalizerTest.php b/tests/Core/Serialize/JSON/Normalizers/ExternalReferenceNormalizerTest.php new file mode 100644 index 00000000..1e071ec1 --- /dev/null +++ b/tests/Core/Serialize/JSON/Normalizers/ExternalReferenceNormalizerTest.php @@ -0,0 +1,173 @@ +createStub(NormalizerFactory::class); + $normalizer = new Normalizers\ExternalReferenceNormalizer($normalizerFactory); + $extRef = $this->createConfiguredMock(ExternalReference::class, [ + 'getUrl' => 'someUrl', + 'getType' => 'someType', + 'getComment' => null, + 'getHashRepository' => null, + ]); + + $actual = $normalizer->normalize($extRef); + + self::assertSame([ + 'type' => 'someType', + 'url' => 'someUrl', + // comment omitted + // hashes omitted + ], $actual); + } + + public function testNormalizeComment(): void + { + $normalizerFactory = $this->createStub(NormalizerFactory::class); + $normalizer = new Normalizers\ExternalReferenceNormalizer($normalizerFactory); + $extRef = $this->createConfiguredMock(ExternalReference::class, [ + 'getUrl' => 'someUrl', + 'getType' => 'someType', + 'getComment' => 'someComment', + 'getHashRepository' => null, + ]); + + $actual = $normalizer->normalize($extRef); + + self::assertSame([ + 'type' => 'someType', + 'url' => 'someUrl', + 'comment' => 'someComment', + ], $actual); + } + + // region normalize hashes + + public function testNormalizeHashes(): void + { + $hashRepositoryNormalizer = $this->createMock(Normalizers\HashRepositoryNormalizer::class); + $normalizerFactory = $this->createConfiguredMock(NormalizerFactory::class, [ + 'getSpec' => $this->createConfiguredMock(SpecInterface::class, [ + 'supportsExternalReferenceHashes' => true, + ]), + 'makeForHashRepository' => $hashRepositoryNormalizer, + ]); + $normalizer = new Normalizers\ExternalReferenceNormalizer($normalizerFactory); + $extRef = $this->createConfiguredMock(ExternalReference::class, [ + 'getUrl' => 'someUrl', + 'getType' => 'someType', + 'getComment' => null, + 'getHashRepository' => $this->createConfiguredMock(HashRepository::class, ['count' => 1]), + ]); + + $hashRepositoryNormalizer->expects(self::once()) + ->method('normalize') + ->with($extRef->getHashRepository()) + ->willReturn(['NormalizedHashRepoFake']); + + $actual = $normalizer->normalize($extRef); + + self::assertSame([ + 'type' => 'someType', + 'url' => 'someUrl', + 'hashes' => ['NormalizedHashRepoFake'], + ], $actual); + } + + public function testNormalizeHashesOmitIfEmpty(): void + { + $hashRepositoryNormalizer = $this->createMock(Normalizers\HashRepositoryNormalizer::class); + $normalizerFactory = $this->createConfiguredMock(NormalizerFactory::class, [ + 'getSpec' => $this->createConfiguredMock(SpecInterface::class, [ + 'supportsExternalReferenceHashes' => true, + ]), + 'makeForHashRepository' => $hashRepositoryNormalizer, + ]); + $normalizer = new Normalizers\ExternalReferenceNormalizer($normalizerFactory); + $extRef = $this->createConfiguredMock(ExternalReference::class, [ + 'getUrl' => 'someUrl', + 'getType' => 'someType', + 'getComment' => null, + 'getHashRepository' => $this->createConfiguredMock(HashRepository::class, ['count' => 0]), + ]); + + $hashRepositoryNormalizer->expects(self::never()) + ->method('normalize') + ->with($extRef->getHashRepository()); + + $actual = $normalizer->normalize($extRef); + + self::assertSame([ + 'type' => 'someType', + 'url' => 'someUrl', + // hashes is omitted + ], $actual); + } + + public function testNormalizeHashesOmitIfNotSupported(): void + { + $hashRepositoryNormalizer = $this->createMock(Normalizers\HashRepositoryNormalizer::class); + $normalizerFactory = $this->createConfiguredMock(NormalizerFactory::class, [ + 'getSpec' => $this->createConfiguredMock(SpecInterface::class, [ + 'supportsExternalReferenceHashes' => false, + ]), + 'makeForHashRepository' => $hashRepositoryNormalizer, + ]); + $normalizer = new Normalizers\ExternalReferenceNormalizer($normalizerFactory); + $extRef = $this->createConfiguredMock(ExternalReference::class, [ + 'getUrl' => 'someUrl', + 'getType' => 'someType', + 'getComment' => null, + 'getHashRepository' => $this->createConfiguredMock(HashRepository::class, ['count' => 1]), + ]); + + $hashRepositoryNormalizer->expects(self::never()) + ->method('normalize') + ->with($extRef->getHashRepository()); + + $actual = $normalizer->normalize($extRef); + + self::assertSame([ + 'type' => 'someType', + 'url' => 'someUrl', + // hashes is omitted + ], $actual); + } + + // endregion normalize hashes +} diff --git a/tests/Core/Serialize/JSON/Normalizers/ExternalReferenceRepositoryNormalizerTest.php b/tests/Core/Serialize/JSON/Normalizers/ExternalReferenceRepositoryNormalizerTest.php new file mode 100644 index 00000000..2a372115 --- /dev/null +++ b/tests/Core/Serialize/JSON/Normalizers/ExternalReferenceRepositoryNormalizerTest.php @@ -0,0 +1,109 @@ +createStub(SpecInterface::class); + $externalReferenceNormalizer = $this->createMock(Normalizers\ExternalReferenceNormalizer::class); + $factory = $this->createConfiguredMock(NormalizerFactory::class, [ + 'getSpec' => $spec, + 'makeForExternalReference' => $externalReferenceNormalizer, + ]); + + $normalizer = new Normalizers\ExternalReferenceRepositoryNormalizer($factory); + $repo = $this->createConfiguredMock(ExternalReferenceRepository::class, ['count' => 0]); + + $actual = $normalizer->normalize($repo); + + self::assertSame([], $actual); + } + + public function testNormalize(): void + { + $spec = $this->createStub(SpecInterface::class); + $externalReferenceNormalizer = $this->createMock(Normalizers\ExternalReferenceNormalizer::class); + $factory = $this->createConfiguredMock(NormalizerFactory::class, [ + 'getSpec' => $spec, + 'makeForExternalReference' => $externalReferenceNormalizer, + ]); + $normalizer = new Normalizers\ExternalReferenceRepositoryNormalizer($factory); + $externalReference = $this->createStub(ExternalReference::class); + $repo = $this->createConfiguredMock(ExternalReferenceRepository::class, [ + 'count' => 1, + 'getExternalReferences' => [$externalReference], + ]); + + $externalReferenceNormalizer->expects(self::once()) + ->method('normalize') + ->with($externalReference) + ->willReturn(['FakeExtRef' => 'dummy']); + + $actual = $normalizer->normalize($repo); + + self::assertSame([['FakeExtRef' => 'dummy']], $actual); + } + + public function testNormalizeSkipsOnThrow(): void + { + $spec = $this->createStub(SpecInterface::class); + $externalReferenceNormalizer = $this->createMock(Normalizers\ExternalReferenceNormalizer::class); + $factory = $this->createConfiguredMock(NormalizerFactory::class, [ + 'getSpec' => $spec, + 'makeForExternalReference' => $externalReferenceNormalizer, + ]); + $normalizer = new Normalizers\ExternalReferenceRepositoryNormalizer($factory); + $extRef1 = $this->createStub(ExternalReference::class); + $extRef2 = $this->createStub(ExternalReference::class); + $tools = $this->createConfiguredMock(ExternalReferenceRepository::class, [ + 'count' => 1, + 'getExternalReferences' => [$extRef1, $extRef2], + ]); + + $externalReferenceNormalizer->expects(self::exactly(2))->method('normalize') + ->withConsecutive([$extRef1], [$extRef2]) + ->willThrowException(new \DomainException()); + + $actual = $normalizer->normalize($tools); + + self::assertSame([], $actual); + } +} diff --git a/tests/Core/Serialize/JSON/Normalizers/ToolRepositoryNormalizerTest.php b/tests/Core/Serialize/JSON/Normalizers/ToolRepositoryNormalizerTest.php index 02ffca25..5baf4e22 100644 --- a/tests/Core/Serialize/JSON/Normalizers/ToolRepositoryNormalizerTest.php +++ b/tests/Core/Serialize/JSON/Normalizers/ToolRepositoryNormalizerTest.php @@ -37,6 +37,9 @@ */ class ToolRepositoryNormalizerTest extends TestCase { + /** + * @uses \CycloneDX\Core\Serialize\JSON\Normalizers\ToolNormalizer + */ public function testNormalizeEmpty(): void { $spec = $this->createStub(SpecInterface::class); diff --git a/tests/Core/SerializeToJsonTest.php b/tests/Core/SerializeToJsonTest.php index d8cfbf9b..7b396a04 100644 --- a/tests/Core/SerializeToJsonTest.php +++ b/tests/Core/SerializeToJsonTest.php @@ -27,7 +27,6 @@ use CycloneDX\Core\Serialize\JsonSerializer; use CycloneDX\Core\Spec\Spec11; use CycloneDX\Core\Spec\Spec12; -use CycloneDX\Core\Spec\Spec13; use CycloneDX\Core\Validation\Validators\JsonStrictValidator; use DomainException; use PHPUnit\Framework\TestCase; @@ -66,6 +65,8 @@ public function testSerialization11(): void * This test might require online-connectivity. * * @dataProvider \CycloneDX\Tests\_data\BomModelProvider::allBomTestData + * + * @throws \Exception on validation failure */ public function testSchema12(Bom $bom): void { @@ -88,10 +89,12 @@ public function testSchema12(Bom $bom): void * This test might require online-connectivity. * * @dataProvider \CycloneDX\Tests\_data\BomModelProvider::allBomTestData + * + * @throws \Exception on validation failure */ public function testSchema13(Bom $bom): void { - $spec = new Spec13(); + $spec = new Spec12(); $serializer = new JsonSerializer($spec); $validator = new JsonStrictValidator($spec); diff --git a/tests/Core/SerializeToXmlTest.php b/tests/Core/SerializeToXmlTest.php index 15aef084..7ac7101c 100644 --- a/tests/Core/SerializeToXmlTest.php +++ b/tests/Core/SerializeToXmlTest.php @@ -47,6 +47,8 @@ class SerializeToXmlTest extends TestCase * This test might require online-connectivity. * * @dataProvider \CycloneDX\Tests\_data\BomModelProvider::allBomTestData + * + * @throws \Exception on validation failure */ public function testSchema11(Bom $bom): void { @@ -69,6 +71,8 @@ public function testSchema11(Bom $bom): void * This test might require online-connectivity. * * @dataProvider \CycloneDX\Tests\_data\BomModelProvider::allBomTestData + * + * @throws \Exception on validation failure */ public function testSchema12(Bom $bom): void { @@ -91,6 +95,8 @@ public function testSchema12(Bom $bom): void * This test might require online-connectivity. * * @dataProvider \CycloneDX\Tests\_data\BomModelProvider::allBomTestData + * + * @throws \Exception on validation failure */ public function testSchema13(Bom $bom): void { diff --git a/tests/Core/Spec/AbstractSpecTestCase.php b/tests/Core/Spec/AbstractSpecTestCase.php index 861ef856..4d882eed 100644 --- a/tests/Core/Spec/AbstractSpecTestCase.php +++ b/tests/Core/Spec/AbstractSpecTestCase.php @@ -26,7 +26,6 @@ use CycloneDX\Core\Enums\Classification; use CycloneDX\Core\Enums\HashAlgorithm; use CycloneDX\Core\Spec\SpecInterface; -use CycloneDX\Core\Spec\Version; use CycloneDX\Tests\_data\BomSpecData; use Generator; use PHPUnit\Framework\TestCase; @@ -36,7 +35,7 @@ abstract class AbstractSpecTestCase extends TestCase abstract protected function getSpec(): SpecInterface; /** - * @psalm-return Version::V_* + * @psalm-return \CycloneDX\Core\Spec\Version::V_* */ abstract protected function getSpecVersion(): string; @@ -61,7 +60,7 @@ final public function testKnownFormats(): array /** * @depends testKnownFormats */ - public function testGetSupportedFormats(array $knownFormats): void + final public function testGetSupportedFormats(array $knownFormats): void { $formats = $this->getSpec()->getSupportedFormats(); self::assertEquals($knownFormats, $formats); @@ -190,4 +189,12 @@ final public function testSupportsDependencies(): void } abstract public function shouldSupportDependencies(): bool; + + final public function testSupportsExternalReferenceHashes(): void + { + $isSupported = $this->getSpec()->supportsExternalReferenceHashes(); + self::assertSame($this->shouldSupportExternalReferenceHashes(), $isSupported); + } + + abstract public function shouldSupportExternalReferenceHashes(): bool; } diff --git a/tests/Core/Spec/Spec11Test.php b/tests/Core/Spec/Spec11Test.php index 3d1c5ab2..315ceadb 100644 --- a/tests/Core/Spec/Spec11Test.php +++ b/tests/Core/Spec/Spec11Test.php @@ -66,4 +66,9 @@ public function shouldSupportDependencies(): bool { return false; } + + public function shouldSupportExternalReferenceHashes(): bool + { + return false; + } } diff --git a/tests/Core/Spec/Spec12Test.php b/tests/Core/Spec/Spec12Test.php index 1852ad45..c45b4612 100644 --- a/tests/Core/Spec/Spec12Test.php +++ b/tests/Core/Spec/Spec12Test.php @@ -66,4 +66,9 @@ public function shouldSupportDependencies(): bool { return true; } + + public function shouldSupportExternalReferenceHashes(): bool + { + return false; + } } diff --git a/tests/Core/Spec/Spec13Test.php b/tests/Core/Spec/Spec13Test.php index 46cb5345..02e5f2a7 100644 --- a/tests/Core/Spec/Spec13Test.php +++ b/tests/Core/Spec/Spec13Test.php @@ -66,4 +66,9 @@ public function shouldSupportDependencies(): bool { return true; } + + public function shouldSupportExternalReferenceHashes(): bool + { + return true; + } } diff --git a/tests/_data/BomModelProvider.php b/tests/_data/BomModelProvider.php index 19763d5f..a8d85241 100644 --- a/tests/_data/BomModelProvider.php +++ b/tests/_data/BomModelProvider.php @@ -24,9 +24,11 @@ namespace CycloneDX\Tests\_data; use CycloneDX\Core\Enums\Classification; +use CycloneDX\Core\Enums\ExternalReferenceType; use CycloneDX\Core\Enums\HashAlgorithm; use CycloneDX\Core\Models\Bom; use CycloneDX\Core\Models\Component; +use CycloneDX\Core\Models\ExternalReference; use CycloneDX\Core\Models\License\DisjunctiveLicenseWithId; use CycloneDX\Core\Models\License\DisjunctiveLicenseWithName; use CycloneDX\Core\Models\License\LicenseExpression; @@ -34,6 +36,7 @@ use CycloneDX\Core\Models\Tool; use CycloneDX\Core\Repositories\ComponentRepository; use CycloneDX\Core\Repositories\DisjunctiveLicenseRepository; +use CycloneDX\Core\Repositories\ExternalReferenceRepository; use CycloneDX\Core\Repositories\HashRepository; use CycloneDX\Core\Repositories\ToolRepository; use Generator; @@ -55,17 +58,49 @@ public static function allBomTestData(): Generator yield from self::bomWithAllComponents(); yield from self::bomWithAllMetadata(); + + yield from self::bomWithExternalReferences(); } /** - * Just an plain BOM. + * Just a plain BOM. * * @psalm-return Generator */ public static function bomPlain(): Generator { - yield 'plain' => [new Bom()]; - yield 'plain v23' => [(new Bom())->setVersion(23)]; + yield 'bom plain' => [new Bom()]; + yield 'bom plain v23' => [(new Bom())->setVersion(23)]; + } + + /** + * BOM with externalReferences. + * + * @psalm-return Generator + */ + public static function bomWithExternalReferences(): Generator + { + yield 'bom with empty ExternalReferences' => [ + (new Bom())->setExternalReferenceRepository( + new ExternalReferenceRepository() + ), + ]; + + foreach (self::externalReferencesForAllTypes() as $label => $extRef) { + yield "bom with $label" => [ + (new Bom())->setExternalReferenceRepository( + new ExternalReferenceRepository($extRef) + ), + ]; + } + + foreach (self::externalReferencesForHashAlgorithmsAllKnown() as $label => $extRef) { + yield "bom with $label" => [ + (new Bom())->setExternalReferenceRepository( + new ExternalReferenceRepository($extRef) + ), + ]; + } } /** @@ -85,7 +120,7 @@ public static function bomWithAllComponents(): Generator yield from self::bomWithComponentLicenseUrl(); yield from self::bomWithComponentHashAlgorithmsAllKnown(); - + yield from self::bomWithComponentWithExternalReferences(); yield from self::bomWithComponentTypeAllKnown(); } @@ -135,6 +170,34 @@ public static function bomWithComponentTypeAllKnown(): Generator ); } + /** + * BOM with externalReferences. + * + * @psalm-return Generator + */ + public static function bomWithComponentWithExternalReferences(): Generator + { + yield 'component with empty ExternalReferences' => [ + (new Bom())->setComponentRepository( + new ComponentRepository( + (new Component(Classification::LIBRARY, 'dummy', 'foo-beta')) + ->setExternalReferenceRepository(new ExternalReferenceRepository()) + ) + ), + ]; + + foreach (self::externalReferencesForAllTypes() as $label => $extRef) { + yield "component with $label" => [ + (new Bom())->setComponentRepository( + new ComponentRepository( + (new Component(Classification::LIBRARY, 'dummy', 'foo-beta')) + ->setExternalReferenceRepository(new ExternalReferenceRepository($extRef)) + ) + ), + ]; + } + } + /** * @psalm-return Generator */ @@ -286,6 +349,21 @@ public static function bomWithComponentVersion(): Generator } } + /** @psalm-return list */ + private static function allHashAlgorithms(): array + { + /** @psalm-var list $known */ + $known = array_values((new \ReflectionClass(HashAlgorithm::class))->getConstants()); + + return array_values(array_unique(array_merge( + $known, + BomSpecData::getHashAlgEnumForVersion('1.0'), + BomSpecData::getHashAlgEnumForVersion('1.1'), + BomSpecData::getHashAlgEnumForVersion('1.2'), + BomSpecData::getHashAlgEnumForVersion('1.3') + ), \SORT_STRING)); + } + /** * BOMs with all hash algorithms known. * @@ -293,14 +371,8 @@ public static function bomWithComponentVersion(): Generator */ public static function bomWithComponentHashAlgorithmsAllKnown(): Generator { - /** @psalm-var list $known */ - $known = array_values((new \ReflectionClass(HashAlgorithm::class))->getConstants()); yield from self::bomWithComponentHashAlgorithms( - ...$known, - ...BomSpecData::getHashAlgEnumForVersion('1.0'), - ...BomSpecData::getHashAlgEnumForVersion('1.1'), - ...BomSpecData::getHashAlgEnumForVersion('1.2'), - ...BomSpecData::getHashAlgEnumForVersion('1.3'), + ...self::allHashAlgorithms() ); } @@ -483,4 +555,38 @@ private static function bomWithMetaDataComponent(): Generator ), ]; } + + /** + * @return Generator + */ + public static function externalReferencesForAllTypes(): Generator + { + /** @psalm-var list $known */ + $known = array_values((new \ReflectionClass(ExternalReferenceType::class))->getConstants()); + $all = array_unique( + array_merge( + $known, + BomSpecData::getExternalReferenceTypeForVersion('1.1'), + BomSpecData::getExternalReferenceTypeForVersion('1.2'), + BomSpecData::getExternalReferenceTypeForVersion('1.3'), + ) + ); + + foreach ($all as $type) { + yield "externalReferenceType: $type" => new ExternalReference($type, ".../types/{$type}.txt"); + } + } + + /** + * BOMs with all hash algorithms known. + * + * @psalm-return Generator + */ + public static function externalReferencesForHashAlgorithmsAllKnown(): Generator + { + $type = ExternalReferenceType::OTHER; + foreach (self::allHashAlgorithms() as $algorithm) { + yield "externalReferenceHash: $algorithm" => (new ExternalReference($type, ".../algorithm/{$algorithm}.txt"))->setHashRepository(new HashRepository([$algorithm => '12345678901234567890123456789012'])); + } + } } diff --git a/tests/_data/BomSpecData.php b/tests/_data/BomSpecData.php index 5ff943b2..856c0e29 100644 --- a/tests/_data/BomSpecData.php +++ b/tests/_data/BomSpecData.php @@ -46,6 +46,14 @@ public static function getClassificationEnumForVersion(string $version): array return self::getEnumValuesForName($version, 'classification'); } + /** + * @psalm-return list sorted list + */ + public static function getExternalReferenceTypeForVersion(string $version): array + { + return self::getEnumValuesForName($version, 'externalReferenceType'); + } + /** * @psalm-return list sorted list */