diff --git a/CHANGELOG.md b/CHANGELOG.md index cd1c09b99..72dce3785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #3328, #3337 – `/v3/api-docs` fails with a `NullPointerException` when spring-hateoas is on the classpath without `HateoasProperties` - #3314 – `Json Processing Exception occurred` is logged for every constrained parameter whose schema is not a `JsonSchema` - #3317 – An injected `HttpHeaders` parameter is described as a schema +- #3332 – The properties a Kotlin entity inherits from an `@Embeddable` are missing from the Spring Data REST schemas - #3320 – `@Order` and `Ordered` ignored when applying customizers - #3319 – A `Page` nested in another schema is not replaced by `PagedModel` - #3313 – Springdoc auto-configurations rely on unspecified auto-configuration ordering diff --git a/springdoc-openapi-tests/pom.xml b/springdoc-openapi-tests/pom.xml index 7ecf4aaff..608760f8e 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 000000000..ab21548c1 --- /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 000000000..41fc34f8a --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/pom.xml @@ -0,0 +1,136 @@ + + + springdoc-openapi-tests + org.springdoc + 3.1.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-webmvc-test + test + + + org.springframework.boot + spring-boot-starter-data-rest + test + + + org.springframework.boot + spring-boot-starter-hateoas + test + + + org.springframework.boot + spring-boot-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 000000000..d836f3e64 --- /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.webmvc.test.autoconfigure.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 000000000..4bc9a9415 --- /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 000000000..c0cd8d213 --- /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 000000000..6d635d413 --- /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 000000000..544f357e4 --- /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 000000000..a715d5a44 --- /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 000000000..5d7482f7f --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-data-rest-tests/src/test/resources/results/3.1.0/app1.json @@ -0,0 +1,330 @@ +{ + "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/vnd.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/vnd.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" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "deprecation": { + "type": "string" + }, + "profile": { + "type": "string" + }, + "name": { + "type": "string" + }, + "templated": { + "type": "boolean" + } + } + }, + "Links": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Link" + } + } + } + } +}