Issue5187Test.java

package io.swagger.v3.core.converting;

import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverter;
import io.swagger.v3.core.converter.ModelConverterContext;
import io.swagger.v3.core.util.AnnotationsUtils;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.JsonSchema;
import io.swagger.v3.oas.models.media.Schema;
import org.testng.annotations.Test;

import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;

/**
 * Regression for <a href="https://github.com/swagger-api/swagger-core/issues/5187">gh-5187</a>:
 * when the cycle guard added in #5004 fires, {@code AnnotationsUtils.getArraySchema}
 * is invoked with {@code processSchemaImplementation = false} and used to leave the
 * array's {@code items} unset entirely (degrading the schema to {@code type: array}
 * with no items, or dropping the property when downstream code stripped the empty
 * schema). The fallback should emit a {@code $ref} to the implementation type that
 * is already registered in the {@link ModelConverterContext}.
 */
public class Issue5187Test {

    @io.swagger.v3.oas.annotations.media.Schema
    static class Inner {
        public String name;
    }

    @io.swagger.v3.oas.annotations.media.Schema(name = "RenamedInner")
    static class RenamedInner {
        public String name;
    }

    @io.swagger.v3.oas.annotations.media.ArraySchema(
            schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = Inner.class))
    static class HolderUsage {
    }

    @io.swagger.v3.oas.annotations.media.ArraySchema(
            schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = RenamedInner.class))
    static class RenamedHolderUsage {
    }

    @Test
    public void cycleGuard_oas31_emitsRefToRegisteredImplementation() {
        ModelConverterContext context = new StubContext();
        context.defineModel("Inner", new JsonSchema());

        io.swagger.v3.oas.annotations.media.ArraySchema ann =
                HolderUsage.class.getAnnotation(io.swagger.v3.oas.annotations.media.ArraySchema.class);

        Schema existing = new JsonSchema().typesItem("array");
        Optional<Schema> result = AnnotationsUtils.getArraySchema(
                ann, null, null, /* openapi31 */ true, existing,
                /* processSchemaImplementation */ false, context);

        assertNotNull(result.orElse(null), "getArraySchema should produce a schema");
        Schema items = result.get().getItems();
        assertNotNull(items, "items must be populated via the cycle-guard fallback");
        assertEquals(items.get$ref(), "#/components/schemas/Inner");
    }

    @Test
    public void cycleGuard_oas30_emitsRefToRegisteredImplementation() {
        ModelConverterContext context = new StubContext();
        context.defineModel("Inner", new Schema());

        io.swagger.v3.oas.annotations.media.ArraySchema ann =
                HolderUsage.class.getAnnotation(io.swagger.v3.oas.annotations.media.ArraySchema.class);

        Optional<Schema> result = AnnotationsUtils.getArraySchema(
                ann, null, null, /* openapi31 */ false, /* existingSchema */ null,
                /* processSchemaImplementation */ false, context);

        assertNotNull(result.orElse(null), "getArraySchema should produce a schema");
        Schema items = result.get().getItems();
        assertNotNull(items, "items must be populated via the cycle-guard fallback");
        assertEquals(items.get$ref(), "#/components/schemas/Inner");
    }

    @Test
    public void cycleGuard_honoursExplicitSchemaName() {
        ModelConverterContext context = new StubContext();
        context.defineModel("RenamedInner", new Schema());

        io.swagger.v3.oas.annotations.media.ArraySchema ann =
                RenamedHolderUsage.class.getAnnotation(io.swagger.v3.oas.annotations.media.ArraySchema.class);

        Optional<Schema> result = AnnotationsUtils.getArraySchema(
                ann, null, null, false, null, false, context);

        Schema items = result.get().getItems();
        assertNotNull(items);
        assertEquals(items.get$ref(), "#/components/schemas/RenamedInner");
    }

    @Test
    public void cycleGuard_doesNotOverwriteExistingItems() {
        ModelConverterContext context = new StubContext();
        context.defineModel("Inner", new Schema());

        io.swagger.v3.oas.annotations.media.ArraySchema ann =
                HolderUsage.class.getAnnotation(io.swagger.v3.oas.annotations.media.ArraySchema.class);

        Schema preExisting = new JsonSchema().typesItem("array");
        preExisting.setItems(new JsonSchema().$ref("#/components/schemas/SomethingElse"));

        Optional<Schema> result = AnnotationsUtils.getArraySchema(
                ann, null, null, true, preExisting, false, context);

        // The fallback only runs when items is null; pre-existing items must be preserved.
        assertEquals(result.get().getItems().get$ref(), "#/components/schemas/SomethingElse");
    }

    @Test
    public void cycleGuard_doesNothingWhenTypeNotRegistered() {
        ModelConverterContext context = new StubContext();
        // Note: do not define Inner in the context.

        io.swagger.v3.oas.annotations.media.ArraySchema ann =
                HolderUsage.class.getAnnotation(io.swagger.v3.oas.annotations.media.ArraySchema.class);

        Optional<Schema> result = AnnotationsUtils.getArraySchema(
                ann, null, null, false, null, false, context);

        assertNull(result.get().getItems(),
                "without a registered schema we cannot invent a $ref; leave items unset");
    }

    /**
     * Minimal {@link ModelConverterContext} that records defined models ��� enough to
     * exercise the lookup used by the cycle-guard fallback without spinning up a
     * full {@code ModelConverters} chain.
     */
    private static final class StubContext implements ModelConverterContext {
        private final Map<String, Schema> models = new HashMap<>();

        @Override
        public void defineModel(String name, Schema model) {
            models.put(name, model);
        }

        @Override
        public void defineModel(String name, Schema model, AnnotatedType type, String previousName) {
            models.put(name, model);
        }

        @Override
        public void defineModel(String name, Schema model, java.lang.reflect.Type type, String previousName) {
            models.put(name, model);
        }

        @Override
        public Map<String, Schema> getDefinedModels() {
            return models;
        }

        @Override
        public Schema resolve(AnnotatedType type) {
            return null;
        }

        @Override
        public Iterator<ModelConverter> getConverters() {
            return java.util.Collections.<ModelConverter>emptyList().iterator();
        }
    }
}