From c1e51f777e48d07c4ed39670fb2831b07008109c Mon Sep 17 00:00:00 2001 From: Badr NASS LAHSEN Date: Sun, 6 Sep 2026 04:53:14 +0200 Subject: [PATCH] test: cover Spring Data REST with Kotlin entities An entity holding an @Embeddable lost that property, and its schema, because swagger-core keyed it under a null property name. The swagger-core 2.2.53 upgrade already fixes it; this pins the behaviour down, as no test module so far combined Kotlin with Spring Data REST. Fixes #3332 Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 1 + springdoc-openapi-tests/pom.xml | 1 + .../.gitignore | 144 ++++++++ .../pom.xml | 131 +++++++ .../api/v31/AbstractKotlinDataRestTest.kt | 54 +++ .../test/org/springdoc/api/v31/app1/Person.kt | 24 ++ .../api/v31/app1/PersonRepository.kt | 7 + .../api/v31/app1/SpringDocApp1Test.kt | 14 + .../src/test/resources/application-test.yml | 9 + .../src/test/resources/logback-test.xml | 6 + .../test/resources/results/3.1.0/app1.json | 348 ++++++++++++++++++ 11 files changed, 739 insertions(+) create mode 100644 springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/.gitignore create mode 100644 springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/pom.xml create mode 100644 springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/AbstractKotlinDataRestTest.kt create mode 100644 springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/app1/Person.kt create mode 100644 springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/app1/PersonRepository.kt create mode 100644 springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/app1/SpringDocApp1Test.kt create mode 100644 springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/resources/application-test.yml create mode 100644 springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/resources/logback-test.xml create mode 100644 springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/resources/results/3.1.0/app1.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e1ce892f3..54411c1724 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #3300 – TYPE_USE annotations on `@ParameterObject` fields are not passed along - #3341 – Stabilize Spring Data `Sort` and `Pageable` schema property order - #3338 – Kotlin nullability interpretation of the `Any?` type +- #3332 – The properties a Kotlin entity inherits from an `@Embeddable` are missing from the Spring Data REST schemas ## [2.9.0] - 2026-07-31 diff --git a/springdoc-openapi-tests/pom.xml b/springdoc-openapi-tests/pom.xml index 5ee7df2e3e..5eb6b93c61 100644 --- a/springdoc-openapi-tests/pom.xml +++ b/springdoc-openapi-tests/pom.xml @@ -24,6 +24,7 @@ springdoc-openapi-actuator-webmvc-tests springdoc-openapi-kotlin-webflux-tests springdoc-openapi-kotlin-webmvc-tests + springdoc-openapi-kotlin-data-rest-tests springdoc-openapi-hateoas-tests springdoc-openapi-data-rest-tests diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/.gitignore b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/.gitignore new file mode 100644 index 0000000000..ab21548c1e --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/.gitignore @@ -0,0 +1,144 @@ +###################### +# Project Specific +###################### +/target/www/** +/src/test/javascript/coverage/ + +###################### +# Node +###################### +/node/ +node_tmp/ +node_modules/ +npm-debug.log.* +/.awcache/* +/.cache-loader/* + +###################### +# SASS +###################### +.sass-cache/ + +###################### +# Eclipse +###################### +*.pydevproject +.project +.metadata +tmp/ +tmp/**/* +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath +.factorypath +/src/main/resources/rebel.xml + +# External tool builders +.externalToolBuilders/** + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + +###################### +# Intellij +###################### +.idea/ +*.iml +*.iws +*.ipr +*.ids +*.orig +classes/ +out/ + +###################### +# Visual Studio Code +###################### +.vscode/ + +###################### +# Maven +###################### +/log/ +/target/ + +###################### +# Gradle +###################### +.gradle/ +/build/ + +###################### +# Package Files +###################### +*.jar +*.war +*.ear +*.db + +###################### +# Windows +###################### +# Windows image file caches +Thumbs.db + +# Folder config file +Desktop.ini + +###################### +# Mac OSX +###################### +.DS_Store +.svn + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +###################### +# Directories +###################### +/bin/ +/deploy/ + +###################### +# Logs +###################### +*.log* + +###################### +# Others +###################### +*.class +*.*~ +*~ +.merge_file* + +###################### +# Gradle Wrapper +###################### +!gradle/wrapper/gradle-wrapper.jar + +###################### +# Maven Wrapper +###################### +!.mvn/wrapper/maven-wrapper.jar + +###################### +# ESLint +###################### +.eslintcache \ No newline at end of file diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/pom.xml b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/pom.xml new file mode 100644 index 0000000000..cb459e5d18 --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/pom.xml @@ -0,0 +1,131 @@ + + + springdoc-openapi-tests + org.springdoc + 2.9.1-SNAPSHOT + + 4.0.0 + springdoc-openapi-kotlin-data-rest-tests + ${project.artifactId} + + + + org.springdoc + springdoc-openapi-starter-webmvc-api + ${project.version} + test + + + com.fasterxml.jackson.module + jackson-module-kotlin + test + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + test + + + org.springframework.boot + spring-boot-starter-web + test + + + org.springframework.boot + spring-boot-starter-data-rest + test + + + org.springframework.boot + spring-boot-starter-data-jpa + test + + + com.h2database + h2 + test + + + + + + + kotlin-maven-plugin + org.jetbrains.kotlin + + + compile + process-sources + + compile + + + + ${project.basedir}/src/main/kotlin + + + + + test-compile + + test-compile + + + + ${project.basedir}/src/test/kotlin + + + + + + + spring + + + -java-parameters + + -Xemit-jvm-type-annotations + + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + + compile + + + + java-test-compile + test-compile + + testCompile + + + + + + + diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/AbstractKotlinDataRestTest.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/AbstractKotlinDataRestTest.kt new file mode 100644 index 0000000000..15f625a3a6 --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/AbstractKotlinDataRestTest.kt @@ -0,0 +1,54 @@ +package test.org.springdoc.api.v31 + +import org.junit.jupiter.api.Test +import org.skyscreamer.jsonassert.JSONAssert +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.result.MockMvcResultMatchers +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Paths + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +abstract class AbstractKotlinDataRestTest { + + @Autowired + val mockMvc: MockMvc? = null + + private val logger = LoggerFactory.getLogger(AbstractKotlinDataRestTest::class.java) + + @Test + fun testApp() { + var result: String? = null + try { + val response = mockMvc!!.perform(MockMvcRequestBuilders.get("/v3/api-docs")) + .andExpect(MockMvcResultMatchers.status().isOk).andReturn() + + result = response.response.contentAsString + val className = javaClass.simpleName + val testNumber = className.replace("[^0-9]".toRegex(), "") + + val expected = getContent("results/3.1.0/app$testNumber.json") + JSONAssert.assertEquals(expected, result, true) + } catch (e: AssertionError) { + logger.error(result) + throw e + } + } + + companion object { + fun getContent(fileName: String): String { + val path = Paths.get( + AbstractKotlinDataRestTest::class.java.classLoader.getResource(fileName)!!.toURI() + ) + return String(Files.readAllBytes(path), StandardCharsets.UTF_8) + } + } +} diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/app1/Person.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/app1/Person.kt new file mode 100644 index 0000000000..4bc9a94157 --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/app1/Person.kt @@ -0,0 +1,24 @@ +package test.org.springdoc.api.v31.app1 + +import jakarta.persistence.Embeddable +import jakarta.persistence.Embedded +import jakarta.persistence.Entity +import jakarta.persistence.Id + +@Entity +class Person { + + @Id + var id: Long? = null + + @Embedded + var name: Name? = null +} + +@Embeddable +class Name { + + var firstName: String? = null + + var lastName: String? = null +} diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/app1/PersonRepository.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/app1/PersonRepository.kt new file mode 100644 index 0000000000..c0cd8d213d --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/app1/PersonRepository.kt @@ -0,0 +1,7 @@ +package test.org.springdoc.api.v31.app1 + +import org.springframework.data.repository.PagingAndSortingRepository +import org.springframework.data.rest.core.annotation.RepositoryRestResource + +@RepositoryRestResource(collectionResourceRel = "people", path = "people") +interface PersonRepository : PagingAndSortingRepository diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/app1/SpringDocApp1Test.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/app1/SpringDocApp1Test.kt new file mode 100644 index 0000000000..6d635d4135 --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/kotlin/test/org/springdoc/api/v31/app1/SpringDocApp1Test.kt @@ -0,0 +1,14 @@ +package test.org.springdoc.api.v31.app1 + +import org.springframework.boot.autoconfigure.SpringBootApplication +import test.org.springdoc.api.v31.AbstractKotlinDataRestTest + +/** + * The properties an entity inherits from a Kotlin `@Embeddable` have to be described, + * see https://github.com/springdoc/springdoc-openapi/issues/3332 + */ +class SpringDocApp1Test : AbstractKotlinDataRestTest() { + + @SpringBootApplication + class SpringDocTestApp +} diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/resources/application-test.yml b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/resources/application-test.yml new file mode 100644 index 0000000000..544f357e4a --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/resources/application-test.yml @@ -0,0 +1,9 @@ +spring: + main: + banner-mode: "off" + lazy-initialization: true +logging: + level: + root: ERROR + pattern: + console: '%m%n' \ No newline at end of file diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/resources/logback-test.xml b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..a715d5a442 --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/resources/logback-test.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/resources/results/3.1.0/app1.json b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/resources/results/3.1.0/app1.json new file mode 100644 index 0000000000..f3a2b828f3 --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/resources/results/3.1.0/app1.json @@ -0,0 +1,348 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/people": { + "get": { + "tags": [ + "person-entity-controller" + ], + "description": "get-person", + "operationId": "getCollectionResource-person-get", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "Zero-based page index (0..N)", + "required": false, + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + } + }, + { + "name": "size", + "in": "query", + "description": "The size of the page to be returned", + "required": false, + "schema": { + "type": "integer", + "default": 20, + "minimum": 1 + } + }, + { + "name": "sort", + "in": "query", + "description": "Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/hal+json": { + "schema": { + "$ref": "#/components/schemas/PagedModelEntityModelPerson" + } + }, + "application/x-spring-data-compact+json": { + "schema": { + "$ref": "#/components/schemas/PagedModelEntityModelPerson" + } + }, + "text/uri-list": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/profile": { + "get": { + "tags": [ + "profile-controller" + ], + "operationId": "listAllFormsOfMetadata", + "responses": { + "200": { + "description": "OK", + "content": { + "application/hal+json": { + "schema": { + "$ref": "#/components/schemas/RepresentationModelObject" + } + } + } + } + } + } + }, + "/profile/people": { + "get": { + "tags": [ + "profile-controller" + ], + "operationId": "descriptor", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + }, + "application/alps+json": { + "schema": { + "type": "string" + } + }, + "application/schema+json": { + "schema": { + "$ref": "#/components/schemas/JsonSchema" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "AbstractJsonSchemaPropertyObject": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "readOnly": { + "type": "boolean" + } + } + }, + "Item": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/AbstractJsonSchemaPropertyObject" + } + }, + "requiredProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "JsonSchema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/AbstractJsonSchemaPropertyObject" + } + }, + "requiredProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Item" + } + }, + "type": { + "type": "string" + }, + "$schema": { + "type": "string" + } + } + }, + "RepresentationModelObject": { + "type": "object", + "properties": { + "_links": { + "$ref": "#/components/schemas/Links" + } + } + }, + "EntityModelPerson": { + "type": "object", + "properties": { + "id": { + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "name": { + "oneOf": [ + { + "$ref": "#/components/schemas/Name" + }, + { + "type": "null" + } + ] + }, + "_links": { + "$ref": "#/components/schemas/Links" + } + } + }, + "Name": { + "type": "object", + "properties": { + "firstName": { + "type": [ + "string", + "null" + ] + }, + "lastName": { + "type": [ + "string", + "null" + ] + } + } + }, + "PageMetadata": { + "type": "object", + "properties": { + "size": { + "type": "integer", + "format": "int64" + }, + "totalElements": { + "type": "integer", + "format": "int64" + }, + "totalPages": { + "type": "integer", + "format": "int64" + }, + "number": { + "type": "integer", + "format": "int64" + } + } + }, + "PagedModelEntityModelPerson": { + "type": "object", + "properties": { + "_embedded": { + "type": "object", + "properties": { + "people": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityModelPerson" + } + } + } + }, + "_links": { + "$ref": "#/components/schemas/Links" + }, + "page": { + "$ref": "#/components/schemas/PageMetadata" + } + } + }, + "Link": { + "type": "object", + "properties": { + "href": { + "type": "string" + }, + "hreflang": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": [ + "string", + "null" + ] + }, + "deprecation": { + "type": [ + "string", + "null" + ] + }, + "profile": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "templated": { + "type": "boolean" + } + } + }, + "Links": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Link" + } + } + } + } +}