Ticket5126Test.java
package io.swagger.v3.core.resolving;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.core.util.AnnotationsUtils;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.core.jackson.ModelResolver;
import io.swagger.v3.oas.models.media.Schema;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
/**
* Defensive invariant tests related to
* <a href="https://github.com/swagger-api/swagger-core/issues/5126">#5126</a>.
*
* <p>{@link ModelResolver#handleUnwrapped} must not forward null-named property schemas into the
* outer model's properties list. The later {@code modelProps.put(prop.getName(), prop)} step uses
* those names as map keys, and a {@code null} key makes Jackson fail when serializing the schema.
*
* <p>These tests exercise the private method directly because the reported Spring HATEOAS path has
* not been reproduced as a swagger-core-only resolver test. The clone-based case demonstrates the
* concrete intermediate state this guard is intended to tolerate: {@code AnnotationsUtils.clone}
* keeps the properties-map keys while losing nested {@code Schema.name} values because
* {@code Schema.getName()} is {@code @JsonIgnore}.
*/
public class Ticket5126Test extends SwaggerTestBase {
private ModelResolver modelResolver;
private Method handleUnwrapped;
@BeforeMethod
public void setup() throws Exception {
modelResolver = new ModelResolver(new ObjectMapper());
handleUnwrapped = ModelResolver.class.getDeclaredMethod(
"handleUnwrapped", List.class, Schema.class, String.class, String.class, List.class);
handleUnwrapped.setAccessible(true);
}
@Test
public void noPrefixOrSuffix_restoresNameFromClonedInnerPropertiesMapKey() throws Exception {
Schema<Object> inner = cloneLosingNestedPropertyNames(namedInnerSchema());
assertEquals(inner.getProperties().keySet(), new LinkedHashSet<>(Arrays.asList("name", "count")));
assertNull(((Schema<?>) inner.getProperties().get("name")).getName());
assertNull(((Schema<?>) inner.getProperties().get("count")).getName());
List<Schema> props = invokeHandleUnwrapped(inner, "", "");
assertPropertyNames(props, "name", "count");
Schema<Object> outer = schemaWithModelProps(props);
assertFalse(outer.getProperties().containsKey(null));
Json.mapper().writeValueAsString(outer);
}
@Test
public void noPrefixOrSuffix_restoresNameFromInnerPropertiesMapKeyForNullNameState() throws Exception {
Schema<Object> inner = innerSchemaWithNullPropertyNames();
List<Schema> props = invokeHandleUnwrapped(inner, "", "");
assertPropertyNames(props, "name", "count");
}
@Test
public void noPrefixOrSuffix_preservesExistingPropertyName() throws Exception {
Schema<?> nameSchema = new Schema<>().type("string");
nameSchema.setName("schemaName");
Schema<Object> inner = new Schema<>();
Map<String, Schema> innerProps = new LinkedHashMap<>();
innerProps.put("mapName", nameSchema);
inner.setProperties(innerProps);
List<Schema> props = invokeHandleUnwrapped(inner, "", "");
assertPropertyNames(props, "schemaName");
}
@Test
public void withPrefixAndSuffix_combinesMapKeyWhenClonedInnerNameIsNull() throws Exception {
Schema<Object> inner = cloneLosingNestedPropertyNames(namedInnerSchema());
List<Schema> props = invokeHandleUnwrapped(inner, "p_", "_s");
assertPropertyNames(props, "p_name_s", "p_count_s");
Schema<Object> outer = schemaWithModelProps(props);
assertFalse(outer.getProperties().containsKey(null));
Json.mapper().writeValueAsString(outer);
}
@Test
public void withPrefixAndSuffix_combinesMapKeyWhenInnerNameIsNull() throws Exception {
Schema<Object> inner = innerSchemaWithNullPropertyNames();
List<Schema> props = invokeHandleUnwrapped(inner, "p_", "_s");
assertPropertyNames(props, "p_name_s", "p_count_s");
}
private Schema<Object> namedInnerSchema() {
Schema<?> nameSchema = new Schema<>().type("string");
nameSchema.setName("name");
Schema<?> countSchema = new Schema<>().type("integer");
countSchema.setName("count");
Schema<Object> inner = new Schema<>();
inner.setName("Inner");
Map<String, Schema> innerProps = new LinkedHashMap<>();
innerProps.put("name", nameSchema);
innerProps.put("count", countSchema);
inner.setProperties(innerProps);
return inner;
}
private Schema<Object> innerSchemaWithNullPropertyNames() {
Schema<?> nameSchema = new Schema<>().type("string");
Schema<?> countSchema = new Schema<>().type("integer");
Schema<Object> inner = new Schema<>();
Map<String, Schema> innerProps = new LinkedHashMap<>();
innerProps.put("name", nameSchema);
innerProps.put("count", countSchema);
inner.setProperties(innerProps);
return inner;
}
private Schema<Object> cloneLosingNestedPropertyNames(Schema<Object> inner) {
return AnnotationsUtils.clone(inner, false);
}
private List<Schema> invokeHandleUnwrapped(Schema<Object> inner, String prefix, String suffix) throws Exception {
List<Schema> props = new ArrayList<>();
List<String> requiredProps = new ArrayList<>();
handleUnwrapped.invoke(modelResolver, props, inner, prefix, suffix, requiredProps);
return props;
}
private Schema<Object> schemaWithModelProps(List<Schema> props) {
Schema<Object> outer = new Schema<>();
Map<String, Schema> modelProps = new LinkedHashMap<>();
for (Schema prop : props) {
modelProps.put(prop.getName(), prop);
}
outer.setProperties(modelProps);
return outer;
}
private void assertPropertyNames(List<Schema> props, String... expectedNames) {
assertEquals(props.size(), expectedNames.length);
LinkedHashSet<String> names = new LinkedHashSet<>();
for (Schema p : props) {
assertNotNull(p.getName(),
"Each unwrapped property must carry a non-null name to avoid null keys in the outer's properties map");
names.add(p.getName());
}
assertEquals(names, new LinkedHashSet<>(Arrays.asList(expectedNames)));
}
}