Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public String getValue(String propertyName) {
pname, Thread.currentThread().getName());
return dto.getValue();
} else {
log.trace(LOG_PREFIX + "Storage returned null.", pname,
log.trace(LOG_PREFIX + "Property not found in storage.", pname,
Thread.currentThread().getName());
}
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.apicurio.registry.noprofile.rest.v3;

import com.fasterxml.jackson.databind.JsonNode;
import io.apicurio.registry.AbstractResourceTestBase;
import io.apicurio.registry.content.ContentHandle;
import io.apicurio.registry.content.util.ContentTypeUtil;
import io.apicurio.registry.model.GroupId;
import io.apicurio.registry.rest.client.models.CreateVersion;
import io.apicurio.registry.rest.client.models.GroupSearchResults;
Expand Down Expand Up @@ -1870,4 +1873,118 @@ public void testGetArtifactVersionWithReferences() throws Exception {
.body("paths.widgets.get.responses.200.content.json.schema.items.properties.description.type", equalTo("string"));
}

/**
* Test that verifies the Content-Type header is preserved when using ?references=REWRITE with YAML content.
* This test reproduces issue #6712 where YAML AsyncAPI content would be rewritten correctly but served with
* mismatched Content-Type header, causing JSON parsing errors.
*
* This test uses the exact structure from the user's report:
* - AsyncAPI 3.0.0 in YAML format
* - References to Avro schema files (JSON format)
* - Repository: https://github.com/ZenWave360/zenwave-playground/tree/main/examples/asyncapi-shopping-cart/apis
*/
@Test
public void testGetArtifactVersionWithReferencesYamlContentType() throws Exception {
String groupId = TestUtils.generateGroupId();
String avroSchemaContent = resourceToString("avro/ShoppingCartCreated.avsc");
String asyncApiContent = resourceToString("asyncapi-shopping-cart.yml");

// Create the Avro schema artifact that will be referenced
createArtifact(groupId, "testYamlReferences/ShoppingCartCreated", ArtifactType.AVRO,
avroSchemaContent, ContentTypes.APPLICATION_JSON);

// Create the AsyncAPI artifact (YAML) that references the Avro schema
List<ArtifactReference> refs = Collections.singletonList(ArtifactReference.builder()
.name("./avro/ShoppingCartCreated.avsc").groupId(groupId)
.artifactId("testYamlReferences/ShoppingCartCreated").version("1").build());
createArtifactWithReferences(groupId, "testYamlReferences/ShoppingCartAPI",
ArtifactType.ASYNCAPI, asyncApiContent, ContentTypes.APPLICATION_YAML, refs);

// Get the content of the artifact preserving external references
// This should return YAML with YAML content type
String preservedContent = given().when().pathParam("groupId", groupId)
.pathParam("artifactId", "testYamlReferences/ShoppingCartAPI")
.get("/registry/v3/groups/{groupId}/artifacts/{artifactId}/versions/branch=latest/content")
.then().statusCode(200)
.contentType(ContentTypes.APPLICATION_YAML)
.extract().asString();

// Verify the preserved content is valid YAML by parsing it
try {
JsonNode preservedYaml = ContentTypeUtil.parseYaml(ContentHandle.create(preservedContent));
Assertions.assertNotNull(preservedYaml, "Preserved content should be valid YAML");
Assertions.assertTrue(preservedYaml.has("asyncapi"), "YAML should have 'asyncapi' field");
Assertions.assertEquals("3.0.0", preservedYaml.get("asyncapi").asText(),
"Should be AsyncAPI 3.0.0");
} catch (IOException e) {
Assertions.fail("Failed to parse preserved content as YAML: " + e.getMessage());
}

// Get the content of the artifact rewriting external references
// CRITICAL: This should return YAML content with YAML content type (not JSON content type)
// Bug #6712: Currently this fails because the response uses artifact.getContentType() instead of
// contentToReturn.getContentType(), causing a mismatch between the content format and Content-Type header
io.restassured.response.Response rawResponse = given().when().pathParam("groupId", groupId)
.pathParam("artifactId", "testYamlReferences/ShoppingCartAPI")
.queryParam("references", "REWRITE")
.get("/registry/v3/groups/{groupId}/artifacts/{artifactId}/versions/branch=latest/content");

// Verify the response status
Assertions.assertEquals(200, rawResponse.getStatusCode(),
"Expected 200 OK but got: " + rawResponse.getStatusCode() + " - " + rawResponse.asString());

// Verify the Content-Type header is YAML (not JSON)
String contentType = rawResponse.getContentType();
Assertions.assertNotNull(contentType, "Content-Type header should not be null");
Assertions.assertTrue(contentType.contains("yaml") || contentType.contains("yml"),
"Content-Type should be YAML but was: " + contentType);

// Verify the content is valid YAML by parsing it
String responseBody = rawResponse.asString();
JsonNode yamlNode = null;
try {
yamlNode = ContentTypeUtil.parseYaml(ContentHandle.create(responseBody));
Assertions.assertNotNull(yamlNode, "Response should be valid YAML");
Assertions.assertTrue(yamlNode.has("asyncapi"), "YAML should have 'asyncapi' field");
Assertions.assertEquals("3.0.0", yamlNode.get("asyncapi").asText(),
"Should be AsyncAPI 3.0.0");
} catch (IOException e) {
Assertions.fail("Failed to parse response as YAML: " + e.getMessage() + ". Body starts with: "
+ responseBody.substring(0, Math.min(100, responseBody.length())));
}

// Verify the reference was rewritten to point to the REST API
// Navigate to the $ref field: components -> messages -> ShoppingCartCreatedMessage -> payload -> schema -> $ref
JsonNode components = yamlNode.get("components");
Assertions.assertNotNull(components, "YAML should have 'components' field");

JsonNode messages = components.get("messages");
Assertions.assertNotNull(messages, "Components should have 'messages' field");

JsonNode shoppingCartCreated = messages.get("ShoppingCartCreatedMessage");
Assertions.assertNotNull(shoppingCartCreated, "Messages should have 'ShoppingCartCreatedMessage' field");

JsonNode payload = shoppingCartCreated.get("payload");
Assertions.assertNotNull(payload, "ShoppingCartCreatedMessage should have 'payload' field");

JsonNode schema = payload.get("schema");
Assertions.assertNotNull(schema, "Payload should have 'schema' field");

JsonNode ref = schema.get("$ref");
Assertions.assertNotNull(ref, "Schema should have '$ref' field");

String rewrittenRef = ref.asText();
Assertions.assertNotNull(rewrittenRef, "Rewritten reference should not be null");

// Verify the reference was rewritten to a REST API URL
Assertions.assertTrue(rewrittenRef.contains("/apis/registry/v3/groups/"),
"Reference should be rewritten to REST API URL but was: " + rewrittenRef);
Assertions.assertTrue(rewrittenRef.contains("/artifacts/"),
"Reference should contain artifact path but was: " + rewrittenRef);
Assertions.assertTrue(rewrittenRef.contains("testYamlReferences%2FShoppingCartCreated"),
"Reference should point to the Avro artifact but was: " + rewrittenRef);
Assertions.assertTrue(rewrittenRef.contains("?references=REWRITE"),
"Reference should contain references=REWRITE parameter but was: " + rewrittenRef);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
asyncapi: 3.0.0
info:
title: "AsyncAPI Shopping Cart Example"
version: 0.0.1
tags:
- name: "ShoppingCart"

defaultContentType: application/json

channels:
ShoppingCartChannel:
address: "shopping-cart"
messages:
ShoppingCartCreatedMessage:
$ref: '#/components/messages/ShoppingCartCreatedMessage'

operations:
onShoppingCartCreated:
action: send
tags:
- name: ShoppingCart
channel:
$ref: '#/channels/ShoppingCartChannel'
messages:
- $ref: '#/channels/ShoppingCartChannel/messages/ShoppingCartCreatedMessage'

components:
messages:
ShoppingCartCreatedMessage:
name: ShoppingCartCreatedMessage
title: "Shopping Cart Created Event"
summary: "Event emitted when a shopping cart is created"
payload:
schemaFormat: application/vnd.apache.avro+json;version=1.9.0
schema:
$ref: "./avro/ShoppingCartCreated.avsc"
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"type": "record",
"name": "ShoppingCartCreated",
"namespace": "io.example.asyncapi.shoppingcart.events.avro",
"fields": [
{
"name": "customerId",
"type": "long"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ private VersionMetaData registerWithAutoRefs(RegistryClient registryClient, Regi
// Read the artifact content.
ContentHandle artifactContent = readContent(artifact.getFile());
String artifactContentType = getContentTypeByExtension(artifact.getFile().getName());
// Set the content type on the artifact if not already explicitly set by the user
if (artifact.getContentType() == null) {
artifact.setContentType(artifactContentType);
}
TypedContent typedArtifactContent = TypedContent.create(artifactContent, artifactContentType);

// Find all references in the content
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

import java.io.File;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
Expand All @@ -31,6 +33,7 @@ public class RegisterAsyncApiAvroAutoRefsTest {
private final File examplesRoot = Paths.get("../../examples/").toAbsolutePath().toFile();
private WireMockServer wireMockServer;
private Set<String> registeredArtifacts = new HashSet<>();
private Map<String, String> artifactContentTypes = new HashMap<>();

@BeforeEach public void setup() {
// Start WireMock server with custom transformer
Expand All @@ -48,6 +51,7 @@ public class RegisterAsyncApiAvroAutoRefsTest {

// clear captured registered artifacts
registeredArtifacts.clear();
artifactContentTypes.clear();
}

@AfterEach public void tearDown() {
Expand Down Expand Up @@ -87,6 +91,19 @@ public class RegisterAsyncApiAvroAutoRefsTest {
assertTrue(registeredArtifacts.contains(
"asyncapi-avro-maven-with-references-auto:io.example.api.dtos.CustomerDeletedEvent"));

// Verify content types - the main AsyncAPI artifact should be YAML, Avro schemas should be JSON
String asyncApiContentType = artifactContentTypes
.get("asyncapi-avro-maven-with-references-auto:CustomersExample");
assertEquals("application/x-yaml", asyncApiContentType,
"AsyncAPI YAML file should be registered with application/x-yaml content-type but was: "
+ asyncApiContentType);

String avroContentType = artifactContentTypes
.get("asyncapi-avro-maven-with-references-auto:io.example.api.dtos.CustomerEvent");
assertEquals("application/json", avroContentType,
"Avro schema file should be registered with application/json content-type but was: "
+ avroContentType);

}

@Test public void testAvroAutoRefs() throws Exception {
Expand Down Expand Up @@ -138,8 +155,11 @@ private String generateResponseBasedOnRequest(Request request) throws JsonProces

String groupId = extractGroupIdFromUrl(url);
String artifactId = extractArtifactIdFromBody(body);
String contentType = extractContentTypeFromBody(body);

registeredArtifacts.add(groupId + ":" + artifactId);
String key = groupId + ":" + artifactId;
registeredArtifacts.add(key);
artifactContentTypes.put(key, contentType);

return """
{
Expand Down Expand Up @@ -178,6 +198,23 @@ private String extractArtifactIdFromBody(String body) throws JsonProcessingExcep
throw new RuntimeException("ArtifactId not found in request body");
}

private String extractContentTypeFromBody(String body) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(body);

JsonNode firstVersionNode = jsonNode.get("firstVersion");
if (firstVersionNode != null && !firstVersionNode.isNull()) {
JsonNode contentNode = firstVersionNode.get("content");
if (contentNode != null && !contentNode.isNull()) {
JsonNode contentTypeNode = contentNode.get("contentType");
if (contentTypeNode != null && !contentTypeNode.isNull()) {
return contentTypeNode.asText();
}
}
}
return "unknown";
}

}
}

Loading