From 63271a3088e1ec0b57a010afc8bd1a36cd411b2f Mon Sep 17 00:00:00 2001 From: Alex Alzate Date: Mon, 25 Aug 2025 22:16:24 -0500 Subject: [PATCH 1/2] Initial commit --- .../java/org/cyclonedx/model/Component.java | 2 + .../ComponentListDeserializer.java | 101 +++++ .../deserializer/MetadataDeserializer.java | 14 +- .../org/cyclonedx/Issue663RegressionTest.java | 369 ++++++++++++++++++ .../issue-663-metadata-nested-components.xml | 36 ++ 5 files changed, 521 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/cyclonedx/util/deserializer/ComponentListDeserializer.java create mode 100644 src/test/java/org/cyclonedx/Issue663RegressionTest.java create mode 100644 src/test/resources/issue-663-metadata-nested-components.xml diff --git a/src/main/java/org/cyclonedx/model/Component.java b/src/main/java/org/cyclonedx/model/Component.java index 37c1f184a..0ba8fabe1 100644 --- a/src/main/java/org/cyclonedx/model/Component.java +++ b/src/main/java/org/cyclonedx/model/Component.java @@ -29,6 +29,7 @@ import org.cyclonedx.model.component.crypto.CryptoProperties; import org.cyclonedx.model.component.Tags; import org.cyclonedx.model.component.data.ComponentData; +import org.cyclonedx.util.deserializer.ComponentListDeserializer; import org.cyclonedx.util.deserializer.ExternalReferencesDeserializer; import org.cyclonedx.util.deserializer.HashesDeserializer; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -452,6 +453,7 @@ public void addProperty(Property property) { @JacksonXmlElementWrapper(localName = "components") @JacksonXmlProperty(localName = "component") + @JsonDeserialize(using = ComponentListDeserializer.class) public List getComponents() { return components; } diff --git a/src/main/java/org/cyclonedx/util/deserializer/ComponentListDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/ComponentListDeserializer.java new file mode 100644 index 000000000..a981412fd --- /dev/null +++ b/src/main/java/org/cyclonedx/util/deserializer/ComponentListDeserializer.java @@ -0,0 +1,101 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.util.deserializer; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.cyclonedx.model.Component; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Custom deserializer for List<Component> that handles XML parsing issues where + * single nested components might be represented as objects instead of arrays. + * + * This addresses GitHub issue #663 where nested components in metadata fail to parse + * due to XML-to-JSON mapping inconsistencies. + */ +public class ComponentListDeserializer extends JsonDeserializer> { + + @Override + public List deserialize(JsonParser parser, DeserializationContext context) throws IOException { + JsonToken currentToken = parser.getCurrentToken(); + + if (currentToken == JsonToken.START_ARRAY) { + // Handle normal array case + return Arrays.asList(parser.readValueAs(Component[].class)); + } else if (currentToken == JsonToken.START_OBJECT) { + // Handle single object case (common in XML parsing) + ObjectMapper mapper = getMapper(parser); + ObjectNode node = parser.readValueAs(ObjectNode.class); + + if (node.has("component")) { + JsonNode componentNode = node.get("component"); + return deserializeComponentNode(componentNode, parser, mapper); + } else { + // If the object doesn't have a "component" field, treat the whole object as a single component + Component component = mapper.convertValue(node, Component.class); + return Collections.singletonList(component); + } + } else if (currentToken == JsonToken.VALUE_NULL) { + return null; + } else { + // Try to deserialize as a single component + ObjectMapper mapper = getMapper(parser); + Component component = parser.readValueAs(Component.class); + return Collections.singletonList(component); + } + } + + /** + * Deserializes a component node that might be either a single component or an array of components + */ + private List deserializeComponentNode(JsonNode componentNode, JsonParser originalParser, ObjectMapper mapper) throws IOException { + try (JsonParser componentParser = componentNode.traverse(originalParser.getCodec())) { + componentParser.nextToken(); // Advance to the first token + + if (componentNode.isArray()) { + return Arrays.asList(componentParser.readValueAs(Component[].class)); + } else { + Component component = componentParser.readValueAs(Component.class); + return Collections.singletonList(component); + } + } + } + + /** + * Gets the ObjectMapper from the JsonParser codec or creates a new one + */ + private ObjectMapper getMapper(JsonParser parser) { + if (parser.getCodec() instanceof ObjectMapper) { + return (ObjectMapper) parser.getCodec(); + } else { + return new ObjectMapper(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java index f0c5a756f..542ff9f01 100644 --- a/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java +++ b/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java @@ -42,7 +42,7 @@ public Metadata deserialize(JsonParser jsonParser, DeserializationContext ctxt) } if(node.has("component")) { - Component component = mapper.convertValue(node.get("component"), Component.class); + Component component = deserializeComponent(node.get("component"), jsonParser, mapper); metadata.setComponent(component); } @@ -136,4 +136,16 @@ private void setTimestamp(JsonNode node, Metadata metadata) { metadata.setTimestamp(TimestampUtils.parseTimestamp(timestampNode.textValue())); } } + + /** + * Deserializes a Component from a JsonNode, handling both simple and complex nested structures. + * This method properly handles XML parsing where nested components might be represented + * as single objects or arrays depending on the XML structure. + */ + private Component deserializeComponent(JsonNode componentNode, JsonParser originalParser, ObjectMapper mapper) throws IOException { + try (JsonParser componentParser = componentNode.traverse(originalParser.getCodec())) { + componentParser.nextToken(); // Advance to the first token + return componentParser.readValueAs(Component.class); + } + } } diff --git a/src/test/java/org/cyclonedx/Issue663RegressionTest.java b/src/test/java/org/cyclonedx/Issue663RegressionTest.java new file mode 100644 index 000000000..e27fda9ba --- /dev/null +++ b/src/test/java/org/cyclonedx/Issue663RegressionTest.java @@ -0,0 +1,369 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx; + +import org.cyclonedx.Version; +import org.cyclonedx.generators.BomGeneratorFactory; +import org.cyclonedx.generators.json.BomJsonGenerator; +import org.cyclonedx.generators.xml.BomXmlGenerator; +import org.cyclonedx.model.Bom; +import org.cyclonedx.model.Component; +import org.cyclonedx.model.Metadata; +import org.cyclonedx.parsers.BomParserFactory; +import org.cyclonedx.parsers.JsonParser; +import org.cyclonedx.parsers.Parser; +import org.cyclonedx.parsers.XmlParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; + +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; + +/** + * Regression test for GitHub issue #663: XMLParser fails when parsing metadata component + * + * This test verifies that the XML parser can correctly handle BOMs that contain + * metadata components with nested components structures. + */ +public class Issue663RegressionTest { + + @Test + @DisplayName("Should successfully parse BOM with nested components in metadata") + public void testParsingMetadataWithNestedComponents() throws Exception { + // Arrange + File inputFile = new File("src/test/resources/issue-663-metadata-nested-components.xml"); + Parser parser = BomParserFactory.createParser(inputFile); + + // Act & Assert - This should not throw an exception + assertThatNoException().isThrownBy(() -> { + Bom bom = parser.parse(inputFile); + + // Verify the BOM structure was parsed correctly + assertThat(bom).isNotNull(); + assertThat(bom.getSerialNumber()).isEqualTo("urn:uuid:12345678-1234-1234-1234-123456789abc"); + + // Verify metadata component exists and has nested components + Metadata metadata = bom.getMetadata(); + assertThat(metadata).isNotNull(); + + Component metadataComponent = metadata.getComponent(); + assertThat(metadataComponent).isNotNull(); + assertThat(metadataComponent.getName()).isEqualTo("Main Application"); + assertThat(metadataComponent.getVersion()).isEqualTo("1.0.0"); + assertThat(metadataComponent.getType()).isEqualTo(Component.Type.APPLICATION); + assertThat(metadataComponent.getBomRef()).isEqualTo("main-app"); + + // Verify nested components in metadata component + assertThat(metadataComponent.getComponents()).isNotNull(); + assertThat(metadataComponent.getComponents()).hasSize(2); + + // First nested component + Component nestedLib1 = metadataComponent.getComponents().get(0); + assertThat(nestedLib1.getName()).isEqualTo("Nested Library 1"); + assertThat(nestedLib1.getVersion()).isEqualTo("2.1.0"); + assertThat(nestedLib1.getBomRef()).isEqualTo("nested-lib-1"); + assertThat(nestedLib1.getType()).isEqualTo(Component.Type.LIBRARY); + + // Second nested component (which has its own nested component) + Component nestedLib2 = metadataComponent.getComponents().get(1); + assertThat(nestedLib2.getName()).isEqualTo("Nested Library 2"); + assertThat(nestedLib2.getVersion()).isEqualTo("3.2.1"); + assertThat(nestedLib2.getBomRef()).isEqualTo("nested-lib-2"); + assertThat(nestedLib2.getType()).isEqualTo(Component.Type.LIBRARY); + + // Verify deeply nested component + assertThat(nestedLib2.getComponents()).isNotNull(); + assertThat(nestedLib2.getComponents()).hasSize(1); + + Component deeplyNested = nestedLib2.getComponents().get(0); + assertThat(deeplyNested.getName()).isEqualTo("Deeply Nested Library"); + assertThat(deeplyNested.getVersion()).isEqualTo("1.5.0"); + assertThat(deeplyNested.getBomRef()).isEqualTo("deeply-nested-lib"); + assertThat(deeplyNested.getType()).isEqualTo(Component.Type.LIBRARY); + + // Verify root-level components still work + assertThat(bom.getComponents()).isNotNull(); + assertThat(bom.getComponents()).hasSize(1); + + Component rootLib = bom.getComponents().get(0); + assertThat(rootLib.getName()).isEqualTo("Root Level Library"); + assertThat(rootLib.getVersion()).isEqualTo("4.0.0"); + assertThat(rootLib.getBomRef()).isEqualTo("root-lib"); + assertThat(rootLib.getType()).isEqualTo(Component.Type.LIBRARY); + }); + } + + @Test + @DisplayName("Should handle metadata component without nested components") + public void testParsingMetadataWithSimpleComponent() throws Exception { + // Create a simpler XML for baseline comparison + String simpleXml = "\n" + + "\n" + + " \n" + + " \n" + + " Simple App\n" + + " 1.0.0\n" + + " \n" + + " \n" + + ""; + + // Act & Assert + assertThatNoException().isThrownBy(() -> { + Parser parser = BomParserFactory.createParser(simpleXml.getBytes()); + Bom bom = parser.parse(simpleXml.getBytes()); + + assertThat(bom).isNotNull(); + assertThat(bom.getMetadata()).isNotNull(); + assertThat(bom.getMetadata().getComponent()).isNotNull(); + assertThat(bom.getMetadata().getComponent().getName()).isEqualTo("Simple App"); + }); + } + + @Test + @DisplayName("Should handle empty metadata component") + public void testParsingEmptyMetadataComponent() throws Exception { + String emptyMetadataXml = "\n" + + "\n" + + " \n" + + " 2023-01-01T12:00:00Z\n" + + " \n" + + ""; + + // Act & Assert + assertThatNoException().isThrownBy(() -> { + Parser parser = BomParserFactory.createParser(emptyMetadataXml.getBytes()); + Bom bom = parser.parse(emptyMetadataXml.getBytes()); + + assertThat(bom).isNotNull(); + assertThat(bom.getMetadata()).isNotNull(); + assertThat(bom.getMetadata().getComponent()).isNull(); + }); + } + + @Test + @DisplayName("Should successfully convert XML with nested components to JSON and back") + public void testXmlToJsonRoundTripWithNestedComponents() throws Exception { + // Arrange + File inputFile = new File("src/test/resources/issue-663-metadata-nested-components.xml"); + XmlParser xmlParser = new XmlParser(); + JsonParser jsonParser = new JsonParser(); + + // Act & Assert + assertThatNoException().isThrownBy(() -> { + // Step 1: Parse original XML + Bom originalBom = xmlParser.parse(inputFile); + + // Step 2: Convert to JSON + BomJsonGenerator jsonGenerator = BomGeneratorFactory.createJson(Version.VERSION_16, originalBom); + String jsonOutput = jsonGenerator.toJsonString(); + assertThat(jsonOutput).isNotNull(); + assertThat(jsonOutput).contains("\"name\" : \"Main Application\""); + assertThat(jsonOutput).contains("\"name\" : \"Nested Library 1\""); + assertThat(jsonOutput).contains("\"name\" : \"Deeply Nested Library\""); + + // Step 3: Parse JSON back to BOM + Bom parsedFromJson = jsonParser.parse(jsonOutput.getBytes()); + + // Step 4: Verify the structure is intact + assertBomStructureEquals(originalBom, parsedFromJson); + }); + } + + @Test + @DisplayName("Should successfully convert JSON with nested components to XML and back") + public void testJsonToXmlRoundTripWithNestedComponents() throws Exception { + // Arrange - Create JSON equivalent of our test XML + String nestedComponentsJson = "{\n" + + " \"bomFormat\": \"CycloneDX\",\n" + + " \"specVersion\": \"1.6\",\n" + + " \"serialNumber\": \"urn:uuid:12345678-1234-1234-1234-123456789abc\",\n" + + " \"version\": 1,\n" + + " \"metadata\": {\n" + + " \"timestamp\": \"2023-01-01T12:00:00Z\",\n" + + " \"component\": {\n" + + " \"type\": \"application\",\n" + + " \"bom-ref\": \"main-app\",\n" + + " \"name\": \"Main Application\",\n" + + " \"version\": \"1.0.0\",\n" + + " \"description\": \"Main application component\",\n" + + " \"components\": [\n" + + " {\n" + + " \"type\": \"library\",\n" + + " \"bom-ref\": \"nested-lib-1\",\n" + + " \"name\": \"Nested Library 1\",\n" + + " \"version\": \"2.1.0\",\n" + + " \"description\": \"First nested library\"\n" + + " },\n" + + " {\n" + + " \"type\": \"library\",\n" + + " \"bom-ref\": \"nested-lib-2\",\n" + + " \"name\": \"Nested Library 2\",\n" + + " \"version\": \"3.2.1\",\n" + + " \"description\": \"Second nested library\",\n" + + " \"components\": [\n" + + " {\n" + + " \"type\": \"library\",\n" + + " \"bom-ref\": \"deeply-nested-lib\",\n" + + " \"name\": \"Deeply Nested Library\",\n" + + " \"version\": \"1.5.0\",\n" + + " \"description\": \"Deeply nested component\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"components\": [\n" + + " {\n" + + " \"type\": \"library\",\n" + + " \"bom-ref\": \"root-lib\",\n" + + " \"name\": \"Root Level Library\",\n" + + " \"version\": \"4.0.0\"\n" + + " }\n" + + " ]\n" + + "}"; + + JsonParser jsonParser = new JsonParser(); + XmlParser xmlParser = new XmlParser(); + + // Act & Assert + assertThatNoException().isThrownBy(() -> { + // Step 1: Parse original JSON + Bom originalBom = jsonParser.parse(nestedComponentsJson.getBytes()); + + // Step 2: Convert to XML + BomXmlGenerator xmlGenerator = BomGeneratorFactory.createXml(Version.VERSION_16, originalBom); + String xmlOutput = xmlGenerator.toXmlString(); + assertThat(xmlOutput).isNotNull(); + assertThat(xmlOutput).contains("Main Application"); + assertThat(xmlOutput).contains("Nested Library 1"); + assertThat(xmlOutput).contains("Deeply Nested Library"); + + // Step 3: Parse XML back to BOM + Bom parsedFromXml = xmlParser.parse(xmlOutput.getBytes()); + + // Step 4: Verify the structure is intact + assertBomStructureEquals(originalBom, parsedFromXml); + }); + } + + @Test + @DisplayName("Should maintain data integrity through complete round-trip XML->JSON->XML->JSON") + public void testCompleteRoundTripDataIntegrity() throws Exception { + // Arrange + File inputFile = new File("src/test/resources/issue-663-metadata-nested-components.xml"); + XmlParser xmlParser = new XmlParser(); + JsonParser jsonParser = new JsonParser(); + + // Act & Assert + assertThatNoException().isThrownBy(() -> { + // Step 1: XML -> BOM + Bom step1Bom = xmlParser.parse(inputFile); + + // Step 2: BOM -> JSON + BomJsonGenerator jsonGenerator1 = BomGeneratorFactory.createJson(Version.VERSION_16, step1Bom); + String step2Json = jsonGenerator1.toJsonString(); + + // Step 3: JSON -> BOM + Bom step3Bom = jsonParser.parse(step2Json.getBytes()); + + // Step 4: BOM -> XML + BomXmlGenerator xmlGenerator = BomGeneratorFactory.createXml(Version.VERSION_16, step3Bom); + String step4Xml = xmlGenerator.toXmlString(); + + // Step 5: XML -> BOM + Bom step5Bom = xmlParser.parse(step4Xml.getBytes()); + + // Step 6: BOM -> JSON (final) + BomJsonGenerator jsonGenerator2 = BomGeneratorFactory.createJson(Version.VERSION_16, step5Bom); + String step6Json = jsonGenerator2.toJsonString(); + + // Verify: All BOMs should be structurally equivalent + assertBomStructureEquals(step1Bom, step3Bom); + assertBomStructureEquals(step3Bom, step5Bom); + assertBomStructureEquals(step1Bom, step5Bom); + + // Verify: JSON outputs should be functionally equivalent (ignoring formatting) + Bom jsonComparison1 = jsonParser.parse(step2Json.getBytes()); + Bom jsonComparison2 = jsonParser.parse(step6Json.getBytes()); + assertBomStructureEquals(jsonComparison1, jsonComparison2); + }); + } + + /** + * Helper method to perform deep comparison of BOM structures, + * focusing on the nested components that were problematic in issue #663 + */ + private void assertBomStructureEquals(Bom expected, Bom actual) { + assertThat(actual.getSerialNumber()).isEqualTo(expected.getSerialNumber()); + assertThat(actual.getVersion()).isEqualTo(expected.getVersion()); + + // Compare metadata component structure + Metadata expectedMetadata = expected.getMetadata(); + Metadata actualMetadata = actual.getMetadata(); + + if (expectedMetadata == null) { + assertThat(actualMetadata).isNull(); + return; + } + + assertThat(actualMetadata).isNotNull(); + + Component expectedMetaComponent = expectedMetadata.getComponent(); + Component actualMetaComponent = actualMetadata.getComponent(); + + if (expectedMetaComponent == null) { + assertThat(actualMetaComponent).isNull(); + } else { + assertThat(actualMetaComponent).isNotNull(); + assertComponentEquals(expectedMetaComponent, actualMetaComponent); + } + + // Compare root-level components + assertThat(actual.getComponents()).hasSameSizeAs(expected.getComponents()); + for (int i = 0; i < expected.getComponents().size(); i++) { + assertComponentEquals(expected.getComponents().get(i), actual.getComponents().get(i)); + } + } + + /** + * Recursively compares Component objects including nested components + */ + private void assertComponentEquals(Component expected, Component actual) { + assertThat(actual.getName()).isEqualTo(expected.getName()); + assertThat(actual.getVersion()).isEqualTo(expected.getVersion()); + assertThat(actual.getType()).isEqualTo(expected.getType()); + assertThat(actual.getBomRef()).isEqualTo(expected.getBomRef()); + assertThat(actual.getDescription()).isEqualTo(expected.getDescription()); + + // Compare nested components recursively + if (expected.getComponents() == null) { + assertThat(actual.getComponents()).isNull(); + } else { + assertThat(actual.getComponents()).isNotNull(); + assertThat(actual.getComponents()).hasSameSizeAs(expected.getComponents()); + + for (int i = 0; i < expected.getComponents().size(); i++) { + assertComponentEquals(expected.getComponents().get(i), actual.getComponents().get(i)); + } + } + } +} \ No newline at end of file diff --git a/src/test/resources/issue-663-metadata-nested-components.xml b/src/test/resources/issue-663-metadata-nested-components.xml new file mode 100644 index 000000000..ce330d4ea --- /dev/null +++ b/src/test/resources/issue-663-metadata-nested-components.xml @@ -0,0 +1,36 @@ + + + + 2023-01-01T12:00:00Z + + Main Application + 1.0.0 + Main application component + + + Nested Library 1 + 2.1.0 + First nested library + + + Nested Library 2 + 3.2.1 + Second nested library + + + Deeply Nested Library + 1.5.0 + Deeply nested component + + + + + + + + + Root Level Library + 4.0.0 + + + \ No newline at end of file From ead5ae148adfcbdafecbe8323ff22ebdcf6e8dbf Mon Sep 17 00:00:00 2001 From: Alex Alzate Date: Mon, 1 Sep 2025 10:08:16 -0500 Subject: [PATCH 2/2] Update tests --- .../org/cyclonedx/Issue663RegressionTest.java | 61 ++----------------- .../issue-663-metadata-nested-components.json | 49 +++++++++++++++ .../issue-663-metadata-nested-components.xml | 0 3 files changed, 55 insertions(+), 55 deletions(-) create mode 100644 src/test/resources/regression/issue-663-metadata-nested-components.json rename src/test/resources/{ => regression}/issue-663-metadata-nested-components.xml (100%) diff --git a/src/test/java/org/cyclonedx/Issue663RegressionTest.java b/src/test/java/org/cyclonedx/Issue663RegressionTest.java index e27fda9ba..03a908ef9 100644 --- a/src/test/java/org/cyclonedx/Issue663RegressionTest.java +++ b/src/test/java/org/cyclonedx/Issue663RegressionTest.java @@ -49,7 +49,7 @@ public class Issue663RegressionTest { @DisplayName("Should successfully parse BOM with nested components in metadata") public void testParsingMetadataWithNestedComponents() throws Exception { // Arrange - File inputFile = new File("src/test/resources/issue-663-metadata-nested-components.xml"); + File inputFile = new File("src/test/resources/regression/issue-663-metadata-nested-components.xml"); Parser parser = BomParserFactory.createParser(inputFile); // Act & Assert - This should not throw an exception @@ -162,7 +162,7 @@ public void testParsingEmptyMetadataComponent() throws Exception { @DisplayName("Should successfully convert XML with nested components to JSON and back") public void testXmlToJsonRoundTripWithNestedComponents() throws Exception { // Arrange - File inputFile = new File("src/test/resources/issue-663-metadata-nested-components.xml"); + File inputFile = new File("src/test/resources/regression/issue-663-metadata-nested-components.xml"); XmlParser xmlParser = new XmlParser(); JsonParser jsonParser = new JsonParser(); @@ -190,64 +190,15 @@ public void testXmlToJsonRoundTripWithNestedComponents() throws Exception { @Test @DisplayName("Should successfully convert JSON with nested components to XML and back") public void testJsonToXmlRoundTripWithNestedComponents() throws Exception { - // Arrange - Create JSON equivalent of our test XML - String nestedComponentsJson = "{\n" + - " \"bomFormat\": \"CycloneDX\",\n" + - " \"specVersion\": \"1.6\",\n" + - " \"serialNumber\": \"urn:uuid:12345678-1234-1234-1234-123456789abc\",\n" + - " \"version\": 1,\n" + - " \"metadata\": {\n" + - " \"timestamp\": \"2023-01-01T12:00:00Z\",\n" + - " \"component\": {\n" + - " \"type\": \"application\",\n" + - " \"bom-ref\": \"main-app\",\n" + - " \"name\": \"Main Application\",\n" + - " \"version\": \"1.0.0\",\n" + - " \"description\": \"Main application component\",\n" + - " \"components\": [\n" + - " {\n" + - " \"type\": \"library\",\n" + - " \"bom-ref\": \"nested-lib-1\",\n" + - " \"name\": \"Nested Library 1\",\n" + - " \"version\": \"2.1.0\",\n" + - " \"description\": \"First nested library\"\n" + - " },\n" + - " {\n" + - " \"type\": \"library\",\n" + - " \"bom-ref\": \"nested-lib-2\",\n" + - " \"name\": \"Nested Library 2\",\n" + - " \"version\": \"3.2.1\",\n" + - " \"description\": \"Second nested library\",\n" + - " \"components\": [\n" + - " {\n" + - " \"type\": \"library\",\n" + - " \"bom-ref\": \"deeply-nested-lib\",\n" + - " \"name\": \"Deeply Nested Library\",\n" + - " \"version\": \"1.5.0\",\n" + - " \"description\": \"Deeply nested component\"\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " }\n" + - " },\n" + - " \"components\": [\n" + - " {\n" + - " \"type\": \"library\",\n" + - " \"bom-ref\": \"root-lib\",\n" + - " \"name\": \"Root Level Library\",\n" + - " \"version\": \"4.0.0\"\n" + - " }\n" + - " ]\n" + - "}"; - + // Arrange + File inputFile = new File("src/test/resources/regression/issue-663-metadata-nested-components.json"); JsonParser jsonParser = new JsonParser(); XmlParser xmlParser = new XmlParser(); // Act & Assert assertThatNoException().isThrownBy(() -> { // Step 1: Parse original JSON - Bom originalBom = jsonParser.parse(nestedComponentsJson.getBytes()); + Bom originalBom = jsonParser.parse(inputFile); // Step 2: Convert to XML BomXmlGenerator xmlGenerator = BomGeneratorFactory.createXml(Version.VERSION_16, originalBom); @@ -269,7 +220,7 @@ public void testJsonToXmlRoundTripWithNestedComponents() throws Exception { @DisplayName("Should maintain data integrity through complete round-trip XML->JSON->XML->JSON") public void testCompleteRoundTripDataIntegrity() throws Exception { // Arrange - File inputFile = new File("src/test/resources/issue-663-metadata-nested-components.xml"); + File inputFile = new File("src/test/resources/regression/issue-663-metadata-nested-components.xml"); XmlParser xmlParser = new XmlParser(); JsonParser jsonParser = new JsonParser(); diff --git a/src/test/resources/regression/issue-663-metadata-nested-components.json b/src/test/resources/regression/issue-663-metadata-nested-components.json new file mode 100644 index 000000000..b7c85a49d --- /dev/null +++ b/src/test/resources/regression/issue-663-metadata-nested-components.json @@ -0,0 +1,49 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:12345678-1234-1234-1234-123456789abc", + "version": 1, + "metadata": { + "timestamp": "2023-01-01T12:00:00Z", + "component": { + "type": "application", + "bom-ref": "main-app", + "name": "Main Application", + "version": "1.0.0", + "description": "Main application component", + "components": [ + { + "type": "library", + "bom-ref": "nested-lib-1", + "name": "Nested Library 1", + "version": "2.1.0", + "description": "First nested library" + }, + { + "type": "library", + "bom-ref": "nested-lib-2", + "name": "Nested Library 2", + "version": "3.2.1", + "description": "Second nested library", + "components": [ + { + "type": "library", + "bom-ref": "deeply-nested-lib", + "name": "Deeply Nested Library", + "version": "1.5.0", + "description": "Deeply nested component" + } + ] + } + ] + } + }, + "components": [ + { + "type": "library", + "bom-ref": "root-lib", + "name": "Root Level Library", + "version": "4.0.0" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/issue-663-metadata-nested-components.xml b/src/test/resources/regression/issue-663-metadata-nested-components.xml similarity index 100% rename from src/test/resources/issue-663-metadata-nested-components.xml rename to src/test/resources/regression/issue-663-metadata-nested-components.xml