From 3515e905c1c3cf0fef98587e4089dacf9803eef1 Mon Sep 17 00:00:00 2001 From: psmagin Date: Fri, 29 May 2026 14:48:53 +0300 Subject: [PATCH] FOLSPRINGS-228 Replace OAS code generation with hand-maintained tenant API sources --- .github/workflows/api-doc.yml | 4 +- .github/workflows/api-lint.yml | 6 +- .github/workflows/api-schema-lint.yml | 6 +- folio-spring-base/pom.xml | 85 +------ .../spring/controller/TenantController.java | 26 +-- .../org/folio/tenant/domain/dto/Error.java | 62 ++++++ .../org/folio/tenant/domain/dto/Errors.java | 42 ++++ .../folio/tenant/domain/dto/Parameter.java | 34 +++ .../tenant/domain/dto/TenantAttributes.java | 58 +++++ .../folio/tenant/rest/resource/TenantApi.java | 72 ++++++ .../main/resources/swagger.api/tenant.yaml | 209 ------------------ .../folio/tenant/domain/dto/ErrorTest.java | 198 +++++++++++++++++ .../folio/tenant/domain/dto/ErrorsTest.java | 170 ++++++++++++++ .../tenant/domain/dto/ParameterTest.java | 132 +++++++++++ .../domain/dto/TenantAttributesTest.java | 176 +++++++++++++++ .../context/ExecutionContextBuilder.java | 16 +- .../context/SystemUserExecutionContext.java | 2 - .../folio/spring/client/UsersClientTest.java | 9 +- folio-spring-tenant-settings/pom.xml | 6 + pom.xml | 1 + 20 files changed, 995 insertions(+), 319 deletions(-) create mode 100644 folio-spring-base/src/main/java/org/folio/tenant/domain/dto/Error.java create mode 100644 folio-spring-base/src/main/java/org/folio/tenant/domain/dto/Errors.java create mode 100644 folio-spring-base/src/main/java/org/folio/tenant/domain/dto/Parameter.java create mode 100644 folio-spring-base/src/main/java/org/folio/tenant/domain/dto/TenantAttributes.java create mode 100644 folio-spring-base/src/main/java/org/folio/tenant/rest/resource/TenantApi.java delete mode 100644 folio-spring-base/src/main/resources/swagger.api/tenant.yaml create mode 100644 folio-spring-base/src/test/java/org/folio/tenant/domain/dto/ErrorTest.java create mode 100644 folio-spring-base/src/test/java/org/folio/tenant/domain/dto/ErrorsTest.java create mode 100644 folio-spring-base/src/test/java/org/folio/tenant/domain/dto/ParameterTest.java create mode 100644 folio-spring-base/src/test/java/org/folio/tenant/domain/dto/TenantAttributesTest.java diff --git a/.github/workflows/api-doc.yml b/.github/workflows/api-doc.yml index 93dbb38e..fec11e66 100644 --- a/.github/workflows/api-doc.yml +++ b/.github/workflows/api-doc.yml @@ -18,7 +18,7 @@ name: api-doc env: API_TYPES: 'OAS' - API_DIRECTORIES: 'folio-spring-base/src/main/resources/swagger.api' + API_DIRECTORIES: 'folio-spring-tenant-settings/src/main/resources/swagger.api' API_EXCLUDES: '' OUTPUT_DIR: 'folio-api-docs' AWS_S3_BUCKET: 'foliodocs' @@ -31,7 +31,7 @@ on: push: branches: [ main, master ] paths: - - 'folio-spring-base/src/main/resources/swagger.api/**' + - 'folio-spring-tenant-settings/src/main/resources/swagger.api/**' tags: '[vV][0-9]+.[0-9]+.[0-9]+*' jobs: diff --git a/.github/workflows/api-lint.yml b/.github/workflows/api-lint.yml index d0512afe..f827f164 100644 --- a/.github/workflows/api-lint.yml +++ b/.github/workflows/api-lint.yml @@ -22,17 +22,17 @@ name: api-lint env: API_TYPES: 'OAS' - API_DIRECTORIES: 'folio-spring-base/src/main/resources/swagger.api' + API_DIRECTORIES: 'folio-spring-tenant-settings/src/main/resources/swagger.api' API_EXCLUDES: '' API_WARNINGS: false on: push: paths: - - 'folio-spring-base/src/main/resources/swagger.api/**' + - 'folio-spring-tenant-settings/src/main/resources/swagger.api/**' pull_request: paths: - - 'folio-spring-base/src/main/resources/swagger.api/**' + - 'folio-spring-tenant-settings/src/main/resources/swagger.api/**' jobs: api-lint: diff --git a/.github/workflows/api-schema-lint.yml b/.github/workflows/api-schema-lint.yml index aaab0bdd..30ada1e1 100644 --- a/.github/workflows/api-schema-lint.yml +++ b/.github/workflows/api-schema-lint.yml @@ -13,16 +13,16 @@ name: api-schema-lint # e.g. '' env: - API_DIRECTORIES: 'folio-spring-base/src/main/resources/swagger.api' + API_DIRECTORIES: 'folio-spring-tenant-settings/src/main/resources/swagger.api' API_EXCLUDES: '' on: push: paths: - - 'folio-spring-base/src/main/resources/swagger.api/**' + - 'folio-spring-tenant-settings/src/main/resources/swagger.api/**' pull_request: paths: - - 'folio-spring-base/src/main/resources/swagger.api/**' + - 'folio-spring-tenant-settings/src/main/resources/swagger.api/**' jobs: api-schema-lint: diff --git a/folio-spring-base/pom.xml b/folio-spring-base/pom.xml index ab0cf667..2fb5c8e5 100644 --- a/folio-spring-base/pom.xml +++ b/folio-spring-base/pom.xml @@ -12,13 +12,6 @@ folio-spring-base - - ${project.basedir}/src/main/resources/swagger.api/tenant.yaml - - src/main/java/org/folio/**/FolioLiquibaseConfiguration.java - - - @@ -87,33 +80,25 @@ - org.postgresql - postgresql - runtime + com.github.ben-manes.caffeine + caffeine + compile - org.liquibase - liquibase-core + jakarta.validation + jakarta.validation-api - - org.openapitools - openapi-generator - ${openapi-generator.version} - - - org.slf4j - slf4j-simple - - + org.postgresql + postgresql + runtime - org.openapitools - openapi-generator-maven-plugin - ${openapi-generator.version} + org.liquibase + liquibase-core @@ -134,15 +119,6 @@ ${mapstruct.version} - - - org.mozilla - rhino - ${rhino.version} - - org.springframework.boot @@ -195,45 +171,4 @@ - - - - - org.openapitools - openapi-generator-maven-plugin - - - tenantApi - - generate - - - ${tenant.yaml.file} - ${project.build.directory}/generated-sources - spring - ${project.groupId}.tenant.domain.dto - ${project.groupId}.tenant.rest.resource - true - true - true - true - error,errors,parameter,metadata,tenantAttributes - false - true - true - ApiUtil.java - - true - java - true - true - true - - - - - - - - diff --git a/folio-spring-base/src/main/java/org/folio/spring/controller/TenantController.java b/folio-spring-base/src/main/java/org/folio/spring/controller/TenantController.java index 73cabb80..b6be413f 100644 --- a/folio-spring-base/src/main/java/org/folio/spring/controller/TenantController.java +++ b/folio-spring-base/src/main/java/org/folio/spring/controller/TenantController.java @@ -28,19 +28,6 @@ public class TenantController implements TenantApi { private final TenantService tenantService; - @Override - public ResponseEntity deleteTenant(String operationId) { - /* - do nothing. - */ - return noContent().build(); - } - - @Override - public ResponseEntity getTenant(String operationId) { - throw new NotImplementedException("Not implemented"); - } - @Override public ResponseEntity postTenant(@Valid TenantAttributes tenantAttributes) { if (isDisableJob(tenantAttributes)) { @@ -65,6 +52,19 @@ public ResponseEntity postTenant(@Valid TenantAttributes tenantAttributes) return noContent().build(); } + @Override + public ResponseEntity getTenant(String operationId) { + throw new NotImplementedException("Not implemented"); + } + + @Override + public ResponseEntity deleteTenant(String operationId) { + /* + do nothing. + */ + return noContent().build(); + } + private Optional getParameterValue(String paramName, List parameters) { if (parameters.isEmpty()) { return Optional.empty(); diff --git a/folio-spring-base/src/main/java/org/folio/tenant/domain/dto/Error.java b/folio-spring-base/src/main/java/org/folio/tenant/domain/dto/Error.java new file mode 100644 index 00000000..a38de324 --- /dev/null +++ b/folio-spring-base/src/main/java/org/folio/tenant/domain/dto/Error.java @@ -0,0 +1,62 @@ +package org.folio.tenant.domain.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.validation.Valid; +import java.util.ArrayList; +import java.util.List; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.jspecify.annotations.Nullable; + +@Data +@NoArgsConstructor +@JsonTypeName("error") +public final class Error { + + @JsonProperty("message") + private String message; + + @JsonProperty("type") + private @Nullable String type; + + @JsonProperty("code") + private @Nullable String code; + + @Valid + @JsonProperty("parameters") + private List<@Valid Parameter> parameters = new ArrayList<>(); + + public Error(String message) { + this.message = message; + } + + public Error message(String message) { + this.message = message; + return this; + } + + public Error type(@Nullable String type) { + this.type = type; + return this; + } + + public Error code(@Nullable String code) { + this.code = code; + return this; + } + + public Error parameters(List<@Valid Parameter> parameters) { + this.parameters = parameters; + return this; + } + + public Error addParametersItem(Parameter parametersItem) { + if (this.parameters == null) { + this.parameters = new ArrayList<>(); + } + this.parameters.add(parametersItem); + return this; + } +} + diff --git a/folio-spring-base/src/main/java/org/folio/tenant/domain/dto/Errors.java b/folio-spring-base/src/main/java/org/folio/tenant/domain/dto/Errors.java new file mode 100644 index 00000000..987bcde5 --- /dev/null +++ b/folio-spring-base/src/main/java/org/folio/tenant/domain/dto/Errors.java @@ -0,0 +1,42 @@ +package org.folio.tenant.domain.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.validation.Valid; +import java.util.ArrayList; +import java.util.List; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.jspecify.annotations.Nullable; + +@Data +@NoArgsConstructor +@JsonTypeName("errors") +public final class Errors { + + @Valid + @JsonProperty("errors") + private List<@Valid Error> errors = new ArrayList<>(); + + @JsonProperty("total_records") + private @Nullable Integer totalRecords; + + public Errors errors(List<@Valid Error> errors) { + this.errors = errors; + return this; + } + + public Errors addErrorsItem(Error errorsItem) { + if (this.errors == null) { + this.errors = new ArrayList<>(); + } + this.errors.add(errorsItem); + return this; + } + + public Errors totalRecords(@Nullable Integer totalRecords) { + this.totalRecords = totalRecords; + return this; + } +} + diff --git a/folio-spring-base/src/main/java/org/folio/tenant/domain/dto/Parameter.java b/folio-spring-base/src/main/java/org/folio/tenant/domain/dto/Parameter.java new file mode 100644 index 00000000..2cf7fbab --- /dev/null +++ b/folio-spring-base/src/main/java/org/folio/tenant/domain/dto/Parameter.java @@ -0,0 +1,34 @@ +package org.folio.tenant.domain.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.jspecify.annotations.Nullable; + +@Data +@NoArgsConstructor +@JsonTypeName("parameter") +public final class Parameter { + + @JsonProperty("key") + private String key; + + @JsonProperty("value") + private @Nullable String value; + + public Parameter(String key) { + this.key = key; + } + + public Parameter key(String key) { + this.key = key; + return this; + } + + public Parameter value(@Nullable String value) { + this.value = value; + return this; + } +} + diff --git a/folio-spring-base/src/main/java/org/folio/tenant/domain/dto/TenantAttributes.java b/folio-spring-base/src/main/java/org/folio/tenant/domain/dto/TenantAttributes.java new file mode 100644 index 00000000..388d64fa --- /dev/null +++ b/folio-spring-base/src/main/java/org/folio/tenant/domain/dto/TenantAttributes.java @@ -0,0 +1,58 @@ +package org.folio.tenant.domain.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import jakarta.validation.Valid; +import java.util.ArrayList; +import java.util.List; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.jspecify.annotations.Nullable; + +@Data +@NoArgsConstructor +@JsonTypeName("tenantAttributes") +public final class TenantAttributes { + + @JsonProperty("module_from") + private @Nullable String moduleFrom; + + @JsonProperty("module_to") + private @Nullable String moduleTo; + + @JsonProperty("purge") + private Boolean purge = true; + + @Valid + @JsonProperty("parameters") + private List<@Valid Parameter> parameters = new ArrayList<>(); + + public TenantAttributes moduleFrom(@Nullable String moduleFrom) { + this.moduleFrom = moduleFrom; + return this; + } + + public TenantAttributes moduleTo(@Nullable String moduleTo) { + this.moduleTo = moduleTo; + return this; + } + + public TenantAttributes purge(Boolean purge) { + this.purge = purge; + return this; + } + + public TenantAttributes parameters(List<@Valid Parameter> parameters) { + this.parameters = parameters; + return this; + } + + public TenantAttributes addParametersItem(Parameter parametersItem) { + if (this.parameters == null) { + this.parameters = new ArrayList<>(); + } + this.parameters.add(parametersItem); + return this; + } +} + diff --git a/folio-spring-base/src/main/java/org/folio/tenant/rest/resource/TenantApi.java b/folio-spring-base/src/main/java/org/folio/tenant/rest/resource/TenantApi.java new file mode 100644 index 00000000..4430d1e0 --- /dev/null +++ b/folio-spring-base/src/main/java/org/folio/tenant/rest/resource/TenantApi.java @@ -0,0 +1,72 @@ +package org.folio.tenant.rest.resource; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; + +import io.swagger.v3.oas.annotations.Hidden; +import jakarta.validation.Valid; +import org.folio.tenant.domain.dto.TenantAttributes; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * REST API interface for FOLIO tenant lifecycle management. + * + *

Exposes endpoints under the {@code /_/tenant} path to create, upgrade, purge, and query + * tenants in accordance with the FOLIO module descriptor contract.

+ */ +@Hidden +@Validated +public interface TenantApi { + + String PATH_DELETE_TENANT = "/_/tenant/{operationId}"; + String PATH_GET_TENANT = "/_/tenant/{operationId}"; + String PATH_POST_TENANT = "/_/tenant"; + + /** + * Submits a tenant job (create, upgrade, or delete) with the supplied attributes. + * + *

Corresponds to {@code POST /_/tenant}.

+ * + * @param tenantAttributes attributes describing the tenant job to perform; must not be null + * @return {@code 204 No Content} when the job completes synchronously, + * {@code 400 Bad Request} if the request body is malformed, + * {@code 422 Unprocessable Entity} on validation errors, + * {@code 500 Internal Server Error} on unexpected failure + */ + @PostMapping(value = TenantApi.PATH_POST_TENANT, + produces = {APPLICATION_JSON_VALUE, TEXT_PLAIN_VALUE}, + consumes = APPLICATION_JSON_VALUE) + ResponseEntity postTenant(@Valid @RequestBody TenantAttributes tenantAttributes); + + /** + * Checks whether the tenant job identified by the given ID has completed. + * + *

Corresponds to {@code GET /_/tenant/{operationId}}.

+ * + * @param operationId the ID of the tenant job to query + * @return {@code 200 OK} with {@code "true"} if the operation exists and is complete, + * {@code "false"} otherwise; + * {@code 500 Internal Server Error} on unexpected failure + */ + @GetMapping(value = TenantApi.PATH_GET_TENANT, produces = TEXT_PLAIN_VALUE) + ResponseEntity getTenant(@PathVariable String operationId); + + /** + * Deletes the tenant job identified by the given operation ID. + * + *

Corresponds to {@code DELETE /_/tenant/{operationId}}.

+ * + * @param operationId the ID of the tenant job to delete + * @return {@code 204 No Content} on success, + * {@code 400 Bad Request} if the operation ID is invalid, + * {@code 500 Internal Server Error} on unexpected failure + */ + @DeleteMapping(value = TenantApi.PATH_DELETE_TENANT, produces = TEXT_PLAIN_VALUE) + ResponseEntity deleteTenant(@PathVariable String operationId); +} diff --git a/folio-spring-base/src/main/resources/swagger.api/tenant.yaml b/folio-spring-base/src/main/resources/swagger.api/tenant.yaml deleted file mode 100644 index c44badcd..00000000 --- a/folio-spring-base/src/main/resources/swagger.api/tenant.yaml +++ /dev/null @@ -1,209 +0,0 @@ -openapi: 3.0.0 -info: - title: Tenant API - version: v1 - -paths: - /_/tenant: - post: - description: Create tenant job (create, upgrade, delete) - operationId: postTenant - tags: - - tenant - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/tenantAttributes' - examples: - tenantAttributesForUpgrade: - $ref: '#/components/examples/tenantAttributesForUpgrade' - tenantAttributesForEnable: - $ref: '#/components/examples/tenantAttributesForEnable' - tenantAttributesForDisable: - $ref: '#/components/examples/tenantAttributesForDisable' - required: true - responses: - "204": - description: Job completed - "400": - description: Bad request - content: - application/json: - schema: - type: string - text/plain: - schema: - type: string - "422": - $ref: '#/components/responses/trait_validate_422' - "500": - description: Internal server error - content: - application/json: - schema: - type: string - example: Internal server error, contact administrator - text/plain: - schema: - type: string - example: Internal server error, contact administrator - /_/tenant/{operationId}: - parameters: - - name: operationId - in: path - required: true - schema: - type: string - description: Operation ID - get: - description: Does tenant id already exist - operationId: getTenant - tags: - - tenant - responses: - "200": - description: true or false indicator - content: - text/plain: - schema: - type: string - example: true - "500": - description: Internal server error - content: - text/plain: - schema: - type: string - example: Internal server error, contact administrator - delete: - description: drop tenant id - operationId: deleteTenant - tags: - - tenant - responses: - "204": - description: Delete succeeded - "400": - description: Bad request - "500": - description: Internal server error - content: - text/plain: - schema: - type: string - example: Internal server error, contact administrator - -components: - responses: - trait_validate_422: - description: Validation errors - content: - application/json: - schema: - $ref: '#/components/schemas/errors' - examples: - response: - value: - errors: - - message: may not be null - type: 1 - code: -1 - parameters: - - key: moduleTo - value: null - schemas: - parameter: - type: object - description: Key/value parameter - properties: - key: - type: string - description: The key for this parameter - value: - type: string - description: The value of this parameter - required: - - key - - parameters: - type: object - description: List of key/value parameters - items: - $ref: '#/components/schemas/parameter' - - tenantAttributes: - type: object - title: Tenant job attributes - description: Configuration how to install, upgrade or delete a module for a tenant - properties: - module_from: - type: string - description: Existing module ID. If omitted, the module is not enabled already - module_to: - type: string - description: Target module ID. If omitted, the existing module is disabled - purge: - type: boolean - description: On disable should data also be purged - default: true - parameters: - $ref: '#/components/schemas/parameters' - description: Parameters that controls what tenant init (eg reference data) - - error: - type: object - description: An error message - properties: - message: - type: string - description: Error message text - type: - type: string - description: Error message type - code: - type: string - description: Error message code - parameters: - $ref: '#/components/schemas/parameters' - description: Error message parameters - required: - - message - - errors: - type: object - description: A set of error messages - properties: - errors: - type: array - description: List of error messages - items: - $ref: '#/components/schemas/error' - total_records: - type: integer - description: Total number of errors - - examples: - tenantAttributesForUpgrade: - summary: Upgrading module for tenant - value: - module_to: mod-sample-1.3.1 - module_from: mod-sample-1.0.2 - parameters: - - key: loadReference - value: true - - key: loadSample - value: true - tenantAttributesForEnable: - summary: Enabling module for tenant - value: - module_to: mod-sample-1.3.1 - parameters: - - key: loadReference - value: true - tenantAttributesForDisable: - summary: Disabling module for tenant - value: - module_from: mod-sample-1.0.2 - purge: false - diff --git a/folio-spring-base/src/test/java/org/folio/tenant/domain/dto/ErrorTest.java b/folio-spring-base/src/test/java/org/folio/tenant/domain/dto/ErrorTest.java new file mode 100644 index 00000000..5e81b1fe --- /dev/null +++ b/folio-spring-base/src/test/java/org/folio/tenant/domain/dto/ErrorTest.java @@ -0,0 +1,198 @@ +package org.folio.tenant.domain.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.folio.spring.testing.type.UnitTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.ObjectMapper; + +@UnitTest +class ErrorTest { + + private ObjectMapper mapper; + + @BeforeEach + void setUp() { + mapper = new ObjectMapper(); + } + + @Test + void defaultValuesOnNoArgConstruction() { + var error = new Error(); + + assertThat(error.getMessage()).isNull(); + assertThat(error.getType()).isNull(); + assertThat(error.getCode()).isNull(); + assertThat(error.getParameters()).isEmpty(); + } + + @Test + void constructorWithMessageSetsMessage() { + var error = new Error("something went wrong"); + + assertThat(error.getMessage()).isEqualTo("something went wrong"); + assertThat(error.getType()).isNull(); + assertThat(error.getCode()).isNull(); + assertThat(error.getParameters()).isEmpty(); + } + + @Test + void fluentSettersReturnSameInstance() { + var error = new Error(); + + assertThat(error.message("msg")).isSameAs(error); + assertThat(error.type("t")).isSameAs(error); + assertThat(error.code("c")).isSameAs(error); + assertThat(error.parameters(List.of())).isSameAs(error); + } + + @Test + void fluentSettersStoreValues() { + var param = new Parameter().key("k").value("v"); + var error = new Error() + .message("msg") + .type("ERROR") + .code("ERR-001") + .parameters(List.of(param)); + + assertThat(error.getMessage()).isEqualTo("msg"); + assertThat(error.getType()).isEqualTo("ERROR"); + assertThat(error.getCode()).isEqualTo("ERR-001"); + assertThat(error.getParameters()).containsExactly(param); + } + + @Test + void addParametersItemAppendsToList() { + var first = new Parameter().key("a"); + var second = new Parameter().key("b"); + var error = new Error() + .addParametersItem(first) + .addParametersItem(second); + + assertThat(error.getParameters()).containsExactly(first, second); + } + + @Test + void addParametersItemInitializesListWhenNull() { + var error = new Error(); + error.setParameters(null); + + var param = new Parameter().key("x"); + error.addParametersItem(param); + + assertThat(error.getParameters()).containsExactly(param); + } + + @Test + void nullTypeAndCodeAreAccepted() { + var error = new Error().type(null).code(null); + + assertThat(error.getType()).isNull(); + assertThat(error.getCode()).isNull(); + } + + @Test + void equalObjectsHaveSameHashCode() { + var a = new Error().message("msg").type("t").code("c"); + var b = new Error().message("msg").type("t").code("c"); + + assertThat(a).isEqualTo(b).hasSameHashCodeAs(b); + } + + @Test + void toStringContainsFieldValues() { + var error = new Error().message("msg").type("t").code("c"); + + assertThat(error.toString()).contains("msg", "t", "c"); + } + + @Test + void serializesFieldNamesCorrectly() { + var error = new Error().message("msg").type("ERROR").code("ERR-001"); + + var json = mapper.writeValueAsString(error); + + assertThat(json) + .isEqualTo(""" + {"message":"msg","code":"ERR-001","parameters":[],"type":"ERROR"}"""); + } + + @Test + void serializesNullTypeAndCodeAsNullNodes() { + var error = new Error().message("msg").type(null).code(null); + + var json = mapper.writeValueAsString(error); + + assertThat(json).contains("\"type\":null", "\"code\":null"); + } + + @Test + void serializesParametersList() { + var error = new Error().message("msg") + .addParametersItem(new Parameter().key("k").value("v")); + + var json = mapper.writeValueAsString(error); + + assertThat(json) + .isEqualTo(""" + {"message":"msg","code":null,"parameters":[{"key":"k","value":"v"}],"type":null}"""); + } + + @Test + void deserializesFromJson() { + var json = """ + { + "message": "msg", + "type": "ERROR", + "code": "ERR-001", + "parameters": [ + { + "key": "k", + "value": "v" + } + ] + } + """; + + var error = mapper.readValue(json, Error.class); + + assertThat(error.getMessage()).isEqualTo("msg"); + assertThat(error.getType()).isEqualTo("ERROR"); + assertThat(error.getCode()).isEqualTo("ERR-001"); + assertThat(error.getParameters()).hasSize(1); + assertThat(error.getParameters().getFirst()) + .extracting(Parameter::getKey, Parameter::getValue) + .containsExactly("k", "v"); + } + + @Test + void deserializesNullTypeAndCode() { + var json = """ + { + "message": "msg", + "type": null, + "code": null + } + """; + + var error = mapper.readValue(json, Error.class); + + assertThat(error.getType()).isNull(); + assertThat(error.getCode()).isNull(); + } + + @Test + void roundTripPreservesValues() { + var original = new Error() + .message("msg") + .type("ERROR") + .code("ERR-001") + .addParametersItem(new Parameter().key("k").value("v")); + + var error = mapper.readValue(mapper.writeValueAsString(original), Error.class); + + assertThat(error).isEqualTo(original); + } +} diff --git a/folio-spring-base/src/test/java/org/folio/tenant/domain/dto/ErrorsTest.java b/folio-spring-base/src/test/java/org/folio/tenant/domain/dto/ErrorsTest.java new file mode 100644 index 00000000..ace8aade --- /dev/null +++ b/folio-spring-base/src/test/java/org/folio/tenant/domain/dto/ErrorsTest.java @@ -0,0 +1,170 @@ +package org.folio.tenant.domain.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.folio.spring.testing.type.UnitTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.ObjectMapper; + +@UnitTest +class ErrorsTest { + + private ObjectMapper mapper; + + @BeforeEach + void setUp() { + mapper = new ObjectMapper(); + } + + @Test + void defaultValuesOnNoArgConstruction() { + var errors = new Errors(); + + assertThat(errors.getErrors()).isEmpty(); + assertThat(errors.getTotalRecords()).isNull(); + } + + @Test + void fluentSettersReturnSameInstance() { + var errors = new Errors(); + + assertThat(errors.errors(List.of())).isSameAs(errors); + assertThat(errors.totalRecords(0)).isSameAs(errors); + } + + @Test + void fluentSettersStoreValues() { + var error = new Error("msg"); + var errors = new Errors() + .errors(List.of(error)) + .totalRecords(1); + + assertThat(errors.getErrors()).containsExactly(error); + assertThat(errors.getTotalRecords()).isEqualTo(1); + } + + @Test + void addErrorsItemAppendsToList() { + var first = new Error("first"); + var second = new Error("second"); + var errors = new Errors() + .addErrorsItem(first) + .addErrorsItem(second); + + assertThat(errors.getErrors()).containsExactly(first, second); + } + + @Test + void addErrorsItemInitializesListWhenNull() { + var errors = new Errors(); + errors.setErrors(null); + + var error = new Error("msg"); + errors.addErrorsItem(error); + + assertThat(errors.getErrors()).containsExactly(error); + } + + @Test + void nullTotalRecordsIsAccepted() { + var errors = new Errors().totalRecords(null); + + assertThat(errors.getTotalRecords()).isNull(); + } + + @Test + void equalObjectsHaveSameHashCode() { + var a = new Errors().addErrorsItem(new Error("msg")).totalRecords(1); + var b = new Errors().addErrorsItem(new Error("msg")).totalRecords(1); + + assertThat(a).isEqualTo(b).hasSameHashCodeAs(b); + } + + @Test + void toStringContainsFieldValues() { + var errors = new Errors().totalRecords(5); + + assertThat(errors.toString()).contains("5"); + } + + @Test + void serializesSnakeCaseTotalRecords() { + var errors = new Errors().totalRecords(3); + + var json = mapper.writeValueAsString(errors); + + assertThat(json).contains("\"total_records\":3"); + } + + @Test + void serializesErrorsList() { + var errors = new Errors().addErrorsItem(new Error("msg").type("t").code("c")); + + var json = mapper.writeValueAsString(errors); + + assertThat(json) + .isEqualTo(""" + {"errors":[{"message":"msg","code":"c","parameters":[],"type":"t"}],"total_records":null}"""); + } + + @Test + void serializesNullTotalRecordsAsNullNode() { + var errors = new Errors().totalRecords(null); + + var json = mapper.writeValueAsString(errors); + + assertThat(json).contains("\"total_records\":null"); + } + + @Test + void deserializesFromJson() { + var json = """ + { + "errors": [ + { + "message": "msg", + "type": "t", + "code": "c", + "parameters": [ ] + } + ], + "total_records": 1 + } + """; + + var errors = mapper.readValue(json, Errors.class); + + assertThat(errors.getTotalRecords()).isEqualTo(1); + assertThat(errors.getErrors()).hasSize(1); + assertThat(errors.getErrors().getFirst()) + .extracting(Error::getMessage, Error::getType, Error::getCode) + .containsExactly("msg", "t", "c"); + } + + @Test + void deserializesNullTotalRecords() { + var json = """ + { + "errors": [ ], + "total_records": null + } + """; + + var errors = mapper.readValue(json, Errors.class); + + assertThat(errors.getTotalRecords()).isNull(); + } + + @Test + void roundTripPreservesValues() { + var original = new Errors() + .addErrorsItem(new Error("msg").type("t").code("c")) + .totalRecords(1); + + var errors = mapper.readValue(mapper.writeValueAsString(original), Errors.class); + + assertThat(errors).isEqualTo(original); + } +} diff --git a/folio-spring-base/src/test/java/org/folio/tenant/domain/dto/ParameterTest.java b/folio-spring-base/src/test/java/org/folio/tenant/domain/dto/ParameterTest.java new file mode 100644 index 00000000..8153f1a4 --- /dev/null +++ b/folio-spring-base/src/test/java/org/folio/tenant/domain/dto/ParameterTest.java @@ -0,0 +1,132 @@ +package org.folio.tenant.domain.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.folio.spring.testing.type.UnitTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.ObjectMapper; + +@UnitTest +class ParameterTest { + + private ObjectMapper mapper; + + @BeforeEach + void setUp() { + mapper = new ObjectMapper(); + } + + @Test + void defaultValuesOnNoArgConstruction() { + var param = new Parameter(); + + assertThat(param.getKey()).isNull(); + assertThat(param.getValue()).isNull(); + } + + @Test + void constructorWithKeySetsKey() { + var param = new Parameter("myKey"); + + assertThat(param.getKey()).isEqualTo("myKey"); + assertThat(param.getValue()).isNull(); + } + + @Test + void fluentSettersReturnSameInstance() { + var param = new Parameter(); + + assertThat(param.key("k")).isSameAs(param); + assertThat(param.value("v")).isSameAs(param); + } + + @Test + void fluentSettersStoreValues() { + var param = new Parameter().key("myKey").value("myValue"); + + assertThat(param.getKey()).isEqualTo("myKey"); + assertThat(param.getValue()).isEqualTo("myValue"); + } + + @Test + void nullValueIsAccepted() { + var param = new Parameter().key("k").value(null); + + assertThat(param.getValue()).isNull(); + } + + @Test + void equalObjectsHaveSameHashCode() { + var a = new Parameter().key("k").value("v"); + var b = new Parameter().key("k").value("v"); + + assertThat(a).isEqualTo(b).hasSameHashCodeAs(b); + } + + @Test + void toStringContainsFieldValues() { + var param = new Parameter().key("k").value("v"); + + assertThat(param.toString()).contains("k", "v"); + } + + @Test + void serializesFieldNamesCorrectly() { + var param = new Parameter().key("myKey").value("myValue"); + + var json = mapper.writeValueAsString(param); + + assertThat(json).isEqualTo(""" + {"key":"myKey","value":"myValue"}"""); + } + + @Test + void serializesNullValueAsNullNode() { + var param = new Parameter().key("k").value(null); + + var json = mapper.writeValueAsString(param); + + assertThat(json).contains(""" + {"key":"k","value":null}"""); + } + + @Test + void deserializesFromJson() { + var json = """ + { + "key": "myKey", + "value": "myValue" + } + """; + + var param = mapper.readValue(json, Parameter.class); + + assertThat(param.getKey()).isEqualTo("myKey"); + assertThat(param.getValue()).isEqualTo("myValue"); + } + + @Test + void deserializesNullValueField() { + var json = """ + { + "key": "k", + "value": null + } + """; + + var param = mapper.readValue(json, Parameter.class); + + assertThat(param.getKey()).isEqualTo("k"); + assertThat(param.getValue()).isNull(); + } + + @Test + void roundTripPreservesValues() { + var original = new Parameter().key("k").value("v"); + + var param = mapper.readValue(mapper.writeValueAsString(original), Parameter.class); + + assertThat(param).isEqualTo(original); + } +} diff --git a/folio-spring-base/src/test/java/org/folio/tenant/domain/dto/TenantAttributesTest.java b/folio-spring-base/src/test/java/org/folio/tenant/domain/dto/TenantAttributesTest.java new file mode 100644 index 00000000..46db36fa --- /dev/null +++ b/folio-spring-base/src/test/java/org/folio/tenant/domain/dto/TenantAttributesTest.java @@ -0,0 +1,176 @@ +package org.folio.tenant.domain.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.folio.spring.testing.type.UnitTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.ObjectMapper; + +@UnitTest +class TenantAttributesTest { + + private ObjectMapper mapper; + + @BeforeEach + void setUp() { + mapper = new ObjectMapper(); + } + + @Test + void defaultValuesOnNoArgConstruction() { + var attrs = new TenantAttributes(); + + assertThat(attrs.getModuleFrom()).isNull(); + assertThat(attrs.getModuleTo()).isNull(); + assertThat(attrs.getPurge()).isTrue(); + assertThat(attrs.getParameters()).isEmpty(); + } + + @Test + void fluentSettersReturnSameInstance() { + var param = new Parameter().key("k").value("v"); + var attrs = new TenantAttributes(); + + assertThat(attrs.moduleFrom("mod-1.0.0")).isSameAs(attrs); + assertThat(attrs.moduleTo("mod-2.0.0")).isSameAs(attrs); + assertThat(attrs.purge(false)).isSameAs(attrs); + assertThat(attrs.parameters(List.of(param))).isSameAs(attrs); + } + + @Test + void fluentSettersStoreValues() { + var param = new Parameter().key("k").value("v"); + var attrs = new TenantAttributes() + .moduleFrom("mod-1.0.0") + .moduleTo("mod-2.0.0") + .purge(false) + .parameters(List.of(param)); + + assertThat(attrs.getModuleFrom()).isEqualTo("mod-1.0.0"); + assertThat(attrs.getModuleTo()).isEqualTo("mod-2.0.0"); + assertThat(attrs.getPurge()).isFalse(); + assertThat(attrs.getParameters()).containsExactly(param); + } + + @Test + void addParametersItemAppendsToList() { + var first = new Parameter().key("a"); + var second = new Parameter().key("b"); + var attrs = new TenantAttributes() + .addParametersItem(first) + .addParametersItem(second); + + assertThat(attrs.getParameters()).containsExactly(first, second); + } + + @Test + void addParametersItemInitializesListWhenNull() { + var attrs = new TenantAttributes(); + attrs.setParameters(null); + + var param = new Parameter().key("x"); + attrs.addParametersItem(param); + + assertThat(attrs.getParameters()).containsExactly(param); + } + + @Test + void equalObjectsHaveSameHashCode() { + var a = new TenantAttributes().moduleFrom("m").moduleTo("n").purge(true); + var b = new TenantAttributes().moduleFrom("m").moduleTo("n").purge(true); + + assertThat(a).isEqualTo(b).hasSameHashCodeAs(b); + } + + @Test + void toStringContainsFieldValues() { + var attrs = new TenantAttributes().moduleFrom("mod-1.0.0").moduleTo("mod-2.0.0"); + + assertThat(attrs.toString()).contains("mod-1.0.0", "mod-2.0.0"); + } + + @Test + void nullModuleFromAndModuleToAreAccepted() { + var attrs = new TenantAttributes().moduleFrom(null).moduleTo(null); + + assertThat(attrs.getModuleFrom()).isNull(); + assertThat(attrs.getModuleTo()).isNull(); + } + + @Test + void serializesSnakeCaseFieldNames() { + var attrs = new TenantAttributes().moduleFrom("mod-1.0.0").moduleTo("mod-2.0.0").purge(false); + + var json = mapper.writeValueAsString(attrs); + + assertThat(json) + .isEqualTo(""" + {"module_from":"mod-1.0.0","module_to":"mod-2.0.0","parameters":[],"purge":false}"""); + } + + @Test + void serializesParametersList() { + var attrs = new TenantAttributes() + .addParametersItem(new Parameter().key("k").value("v")); + + var json = mapper.writeValueAsString(attrs); + + assertThat(json).isEqualTo(""" + {"module_from":null,"module_to":null,"parameters":[{"key":"k","value":"v"}],"purge":true}"""); + } + + @Test + void deserializesFromJson() { + var json = """ + { + "module_from": "mod-1.0.0", + "module_to": "mod-2.0.0", + "purge": false, + "parameters": [ + { + "key": "k", + "value": "v" + } + ] + } + """; + + var attrs = mapper.readValue(json, TenantAttributes.class); + + assertThat(attrs.getModuleFrom()).isEqualTo("mod-1.0.0"); + assertThat(attrs.getModuleTo()).isEqualTo("mod-2.0.0"); + assertThat(attrs.getPurge()).isFalse(); + assertThat(attrs.getParameters()).hasSize(1); + assertThat(attrs.getParameters().getFirst().getKey()).isEqualTo("k"); + } + + @Test + void deserializesNullModuleFields() { + var json = """ + { + "module_from": null, + "module_to": null + } + """; + + var attrs = mapper.readValue(json, TenantAttributes.class); + + assertThat(attrs.getModuleFrom()).isNull(); + assertThat(attrs.getModuleTo()).isNull(); + } + + @Test + void roundTripPreservesValues() { + var original = new TenantAttributes() + .moduleFrom("mod-1.0.0") + .moduleTo("mod-2.0.0") + .purge(false) + .addParametersItem(new Parameter().key("k").value("v")); + + var attrs = mapper.readValue(mapper.writeValueAsString(original), TenantAttributes.class); + + assertThat(attrs).isEqualTo(original); + } +} diff --git a/folio-spring-system-user/src/main/java/org/folio/spring/context/ExecutionContextBuilder.java b/folio-spring-system-user/src/main/java/org/folio/spring/context/ExecutionContextBuilder.java index 6c780f1f..d86ff79a 100644 --- a/folio-spring-system-user/src/main/java/org/folio/spring/context/ExecutionContextBuilder.java +++ b/folio-spring-system-user/src/main/java/org/folio/spring/context/ExecutionContextBuilder.java @@ -3,12 +3,10 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.singleton; -import com.google.common.collect.Maps; import java.util.Collection; -import java.util.HashMap; import java.util.Map; import java.util.function.Supplier; -import javax.annotation.CheckForNull; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; @@ -32,10 +30,10 @@ public class ExecutionContextBuilder { * Creates an execution context for sending requests to FOLIO on behalf of a system user. * * @param systemUser the user to send requests as - * @param refresher a supplier which should, upon the {@code systemUser}'s expiration, return a new - * {@link SystemUser} with a fresh access token + * @param refresher a supplier which should, upon the {@code systemUser}'s expiration, return a new + * {@link SystemUser} with a fresh access token */ - public FolioExecutionContext forSystemUser(SystemUser systemUser, @CheckForNull Supplier refresher) { + public FolioExecutionContext forSystemUser(SystemUser systemUser, Supplier refresher) { return new SystemUserExecutionContext(moduleMetadata, systemUser, refresher); } @@ -45,7 +43,11 @@ public FolioExecutionContext buildContext(String tenantId) { public FolioExecutionContext buildContext(String tenantId, Map> headers) { var okapiUrl = folioEnvironment.getOkapiUrl(); - var contextHeaders = new HashMap<>(Maps.filterValues(MapUtils.emptyIfNull(headers), CollectionUtils::isNotEmpty)); + var contextHeaders = MapUtils.emptyIfNull(headers).entrySet() + .stream() + .filter(entry -> CollectionUtils.isNotEmpty(entry.getValue())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + contextHeaders.put(XOkapiHeaders.URL, singleton(okapiUrl)); contextHeaders.put(XOkapiHeaders.TENANT, singleton(tenantId)); return new DefaultFolioExecutionContext( diff --git a/folio-spring-system-user/src/main/java/org/folio/spring/context/SystemUserExecutionContext.java b/folio-spring-system-user/src/main/java/org/folio/spring/context/SystemUserExecutionContext.java index 2245603f..ed313882 100644 --- a/folio-spring-system-user/src/main/java/org/folio/spring/context/SystemUserExecutionContext.java +++ b/folio-spring-system-user/src/main/java/org/folio/spring/context/SystemUserExecutionContext.java @@ -8,7 +8,6 @@ import java.util.Optional; import java.util.UUID; import java.util.function.Supplier; -import javax.annotation.CheckForNull; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; import org.folio.spring.FolioExecutionContext; @@ -28,7 +27,6 @@ public class SystemUserExecutionContext implements FolioExecutionContext { private final FolioModuleMetadata moduleMetadata; - @CheckForNull private final Supplier refresher; private SystemUser systemUser; diff --git a/folio-spring-system-user/src/test/java/org/folio/spring/client/UsersClientTest.java b/folio-spring-system-user/src/test/java/org/folio/spring/client/UsersClientTest.java index 696e2c75..da57d805 100644 --- a/folio-spring-system-user/src/test/java/org/folio/spring/client/UsersClientTest.java +++ b/folio-spring-system-user/src/test/java/org/folio/spring/client/UsersClientTest.java @@ -3,13 +3,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.folio.spring.client.UsersClient.User; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.HashMap; import java.util.Map; import org.folio.spring.testing.type.UnitTest; import org.junit.jupiter.api.Test; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; @UnitTest class UsersClientTest { @@ -17,7 +16,7 @@ class UsersClientTest { private final ObjectMapper objectMapper = new ObjectMapper(); @Test - void testDeserialization() throws JsonProcessingException { + void testDeserialization() { User user = objectMapper.readValue( """ { @@ -38,7 +37,7 @@ void testDeserialization() throws JsonProcessingException { } @Test - void testSerialization() throws JsonProcessingException { + void testSerialization() { Map user = objectMapper.readValue( objectMapper.writeValueAsString( new User("id", "username", null, true, null, diff --git a/folio-spring-tenant-settings/pom.xml b/folio-spring-tenant-settings/pom.xml index d1a3e359..2b78c99d 100644 --- a/folio-spring-tenant-settings/pom.xml +++ b/folio-spring-tenant-settings/pom.xml @@ -33,6 +33,12 @@ spring-boot-starter-webmvc-test + + org.projectlombok + lombok + true + + org.hibernate.validator hibernate-validator diff --git a/pom.xml b/pom.xml index 1c50b9e0..947af31f 100644 --- a/pom.xml +++ b/pom.xml @@ -39,6 +39,7 @@ 78.3 1.6.0 3.20.0 + 3.2.4 5.0.0