From ef4e221e88dfa2a404a3872bdfd3fef371bd5ed2 Mon Sep 17 00:00:00 2001 From: Alexander Kurash Date: Fri, 21 Jun 2024 15:31:12 +0300 Subject: [PATCH] CIRC-2111 Create API wrapping settings CRUD (#1479) * CIRC-2111 Initial implementation * CIRC-2111 Add interface dependency * CIRC-2111 Fix delete method * CIRC-2111 Add more tests * CIRC-2111 Validate UUID - GET and DELETE * CIRC-2111 Remove query logging * CIRC-2111 Fix code smells * CIRC-2111 Remove redundant UUID parsing * CIRC-2111 Add validation tests * CIRC-2111 Apply suggestions from code review Co-authored-by: OleksandrVidinieiev <56632770+OleksandrVidinieiev@users.noreply.github.com> * CIRC-2111 Add param logging * CIRC-2111 Improve logging * CIRC-2111 Use peek --------- Co-authored-by: OleksandrVidinieiev <56632770+OleksandrVidinieiev@users.noreply.github.com> --- descriptors/ModuleDescriptor-template.json | 84 ++++++++++ ramls/circulation-setting.json | 33 ++++ ramls/circulation-settings.json | 23 +++ ramls/circulation-settings.raml | 105 ++++++++++++ ramls/examples/circulation-setting.json | 7 + ramls/examples/circulation-settings.json | 12 ++ .../circulation/CirculationVerticle.java | 2 + .../domain/CirculationSetting.java | 58 +++++++ .../CirculationSettingsRepository.java | 74 +++++++++ .../CirculationSettingsResource.java | 154 ++++++++++++++++++ .../folio/circulation/support/Clients.java | 13 ++ .../settings/CirculationSettingsTests.java | 106 ++++++++++++ src/test/java/api/support/APITests.java | 3 + .../builders/CirculationSettingBuilder.java | 34 ++++ .../java/api/support/fakes/FakeOkapi.java | 7 + .../java/api/support/http/InterfaceUrls.java | 3 + .../java/api/support/http/ResourceClient.java | 4 + 17 files changed, 722 insertions(+) create mode 100644 ramls/circulation-setting.json create mode 100644 ramls/circulation-settings.json create mode 100644 ramls/circulation-settings.raml create mode 100644 ramls/examples/circulation-setting.json create mode 100644 ramls/examples/circulation-settings.json create mode 100644 src/main/java/org/folio/circulation/domain/CirculationSetting.java create mode 100644 src/main/java/org/folio/circulation/infrastructure/storage/CirculationSettingsRepository.java create mode 100644 src/main/java/org/folio/circulation/resources/CirculationSettingsResource.java create mode 100644 src/test/java/api/settings/CirculationSettingsTests.java create mode 100644 src/test/java/api/support/builders/CirculationSettingBuilder.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 3ec694130d..c7eabed65f 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -682,6 +682,61 @@ } ] }, + { + "id": "circulation-settings", + "version": "1.0", + "handlers": [ + { + "methods": [ + "GET" + ], + "pathPattern": "/circulation/settings", + "permissionsRequired": [ + "circulation.settings.collection.get" + ], + "modulePermissions": [ + "circulation-storage.circulation-settings.collection.get" + ] + }, + { + "methods": ["GET"], + "pathPattern": "/circulation/settings/{id}", + "permissionsRequired": [ + "circulation.settings.item.get" + ], + "modulePermissions": [ + "circulation-storage.circulation-settings.item.get" + ] + }, { + "methods": ["PUT"], + "pathPattern": "/circulation/settings/{id}", + "permissionsRequired": [ + "circulation.settings.item.put" + ], + "modulePermissions": [ + "circulation-storage.circulation-settings.item.put" + ] + }, { + "methods": ["POST"], + "pathPattern": "/circulation/settings", + "permissionsRequired": [ + "circulation.settings.item.post" + ], + "modulePermissions": [ + "circulation-storage.circulation-settings.item.post" + ] + }, { + "methods": ["DELETE"], + "pathPattern": "/circulation/settings/{id}", + "permissionsRequired": [ + "circulation.settings.item.delete" + ], + "modulePermissions": [ + "circulation-storage.circulation-settings.item.delete" + ] + } + ] + }, { "id": "_timer", "version": "1.0", @@ -1259,6 +1314,10 @@ { "id": "settings", "version": "1.0" + }, + { + "id": "circulation-settings-storage", + "version": "1.0" } ], "optional": [ @@ -1518,6 +1577,31 @@ "displayName": "circulation settings - Read configuration", "description": "To read the configuration from mod settings." }, + { + "permissionName": "circulation.settings.collection.get", + "displayName": "circulation - get circulation settings", + "description": "get a collection of circulation settings" + }, + { + "permissionName": "circulation.settings.item.get", + "displayName": "circulation - get an individual circulation setting", + "description": "get an individual circulation setting by ID" + }, + { + "permissionName": "circulation.settings.item.put", + "displayName": "circulation - update circulation setting", + "description": "update circulation setting by ID" + }, + { + "permissionName": "circulation.settings.item.post", + "displayName": "circulation - create circulation setting", + "description": "create a new circulation setting" + }, + { + "permissionName": "circulation.settings.item.delete", + "displayName": "circulation - delete circulation setting", + "description": "delete circulation setting by ID" + }, { "permissionName": "circulation.all", "displayName": "circulation - all permissions", diff --git a/ramls/circulation-setting.json b/ramls/circulation-setting.json new file mode 100644 index 0000000000..0907442e8b --- /dev/null +++ b/ramls/circulation-setting.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Circulation Setting Schema", + "description": "Circulation setting", + "type": "object", + "properties": { + "id": { + "description": "ID of the circulation setting", + "type": "string", + "$ref": "raml-util/schemas/uuid.schema" + }, + "name": { + "description": "Circulation setting name", + "type": "string" + }, + "value": { + "description": "Circulation setting", + "type": "object", + "additionalProperties": true + }, + "metadata": { + "description": "Metadata about creation and changes, provided by the server (client should not provide)", + "type": "object", + "$ref": "raml-util/schemas/metadata.schema" + } + }, + "additionalProperties": false, + "required": [ + "id", + "name", + "value" + ] +} diff --git a/ramls/circulation-settings.json b/ramls/circulation-settings.json new file mode 100644 index 0000000000..99173df7ba --- /dev/null +++ b/ramls/circulation-settings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Collection of Circulation settings", + "type": "object", + "properties": { + "circulationSettings": { + "description": "List of circulation settings", + "id": "circulationSettings", + "type": "array", + "items": { + "type": "object", + "$ref": "circulation-setting.json" + } + }, + "totalRecords": { + "type": "integer" + } + }, + "required": [ + "circulationSettings", + "totalRecords" + ] +} diff --git a/ramls/circulation-settings.raml b/ramls/circulation-settings.raml new file mode 100644 index 0000000000..731b9187c6 --- /dev/null +++ b/ramls/circulation-settings.raml @@ -0,0 +1,105 @@ +#%RAML 1.0 +title: Circulation Settings +version: v1.0 +protocols: [ HTTP, HTTPS ] +baseUri: http://localhost:9130 + +documentation: + - title: Circulation Settings API + content: API for circulation settings + +traits: + language: !include raml-util/traits/language.raml + pageable: !include raml-util/traits/pageable.raml + searchable: !include raml-util/traits/searchable.raml + validate: !include raml-util/traits/validation.raml + +types: + circulation-setting: !include circulation-setting.json + circulation-settings: !include circulation-settings.json + errors: !include raml-util/schemas/errors.schema + parameters: !include raml-util/schemas/parameters.schema + +resourceTypes: + collection: !include raml-util/rtypes/collection.raml + collection-item: !include raml-util/rtypes/item-collection.raml + +/circulation/settings: + type: + collection: + exampleCollection: !include examples/circulation-settings.json + exampleItem: !include examples/circulation-setting.json + schemaCollection: circulation-settings + schemaItem: circulation-setting + post: + is: [validate] + description: Create a new circulation setting + body: + application/json: + type: circulation-setting + responses: + 201: + description: "Circulation setting has been created" + body: + application/json: + type: circulation-setting + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + get: + is: [validate, pageable, searchable: { description: "with valid searchable fields", example: "id=497f6eca-6276-4993-bfeb-98cbbbba8f79" }] + description: Get all circulation settings + responses: + 200: + description: "Circulation settings successfully retreived" + body: + application/json: + type: circulation-settings + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + /{circulationSettingId}: + type: + collection-item: + exampleItem: !include examples/circulation-setting.json + schema: circulation-setting + get: + responses: + 200: + description: "Circulation setting successfully retreived" + body: + application/json: + type: circulation-setting + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + put: + is: [ validate ] + body: + application/json: + type: circulation-setting + responses: + 204: + description: "Circulation settings have been saved" + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + delete: + is: [validate] + responses: + 204: + description: "Circulation settings deleted" + 500: + description: "Internal server error" + body: + text/plain: + example: "Internal server error" + diff --git a/ramls/examples/circulation-setting.json b/ramls/examples/circulation-setting.json new file mode 100644 index 0000000000..35f0aba430 --- /dev/null +++ b/ramls/examples/circulation-setting.json @@ -0,0 +1,7 @@ +{ + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f09", + "name": "Sample settings", + "value": { + "org.folio.circulation.settings": "true" + } +} diff --git a/ramls/examples/circulation-settings.json b/ramls/examples/circulation-settings.json new file mode 100644 index 0000000000..b9b7a9c8b2 --- /dev/null +++ b/ramls/examples/circulation-settings.json @@ -0,0 +1,12 @@ +{ + "circulationSettings": [ + { + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f09", + "name": "Sample settings", + "value": { + "org.folio.circulation.settings": "true" + } + } + ], + "totalRecords": 1 +} diff --git a/src/main/java/org/folio/circulation/CirculationVerticle.java b/src/main/java/org/folio/circulation/CirculationVerticle.java index cd6960808b..f0d2371440 100644 --- a/src/main/java/org/folio/circulation/CirculationVerticle.java +++ b/src/main/java/org/folio/circulation/CirculationVerticle.java @@ -10,6 +10,7 @@ import org.folio.circulation.resources.CheckInByBarcodeResource; import org.folio.circulation.resources.CheckOutByBarcodeResource; import org.folio.circulation.resources.CirculationRulesResource; +import org.folio.circulation.resources.CirculationSettingsResource; import org.folio.circulation.resources.ClaimItemReturnedResource; import org.folio.circulation.resources.DeclareClaimedReturnedItemAsMissingResource; import org.folio.circulation.resources.DeclareLostResource; @@ -150,6 +151,7 @@ public void start(Promise startFuture) { // Handlers new LoanRelatedFeeFineClosedHandlerResource(client).register(router); new FeeFineBalanceChangedHandlerResource(client).register(router); + new CirculationSettingsResource(client).register(router); server.requestHandler(router) .listen(config().getInteger("port"), result -> { diff --git a/src/main/java/org/folio/circulation/domain/CirculationSetting.java b/src/main/java/org/folio/circulation/domain/CirculationSetting.java new file mode 100644 index 0000000000..03f254f1ed --- /dev/null +++ b/src/main/java/org/folio/circulation/domain/CirculationSetting.java @@ -0,0 +1,58 @@ +package org.folio.circulation.domain; + +import static lombok.AccessLevel.PRIVATE; +import static org.folio.circulation.support.json.JsonPropertyFetcher.getObjectProperty; +import static org.folio.circulation.support.json.JsonPropertyFetcher.getProperty; + +import java.lang.invoke.MethodHandles; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import io.vertx.core.json.JsonObject; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@AllArgsConstructor(access = PRIVATE) +@ToString(onlyExplicitlyIncluded = true) +public class CirculationSetting { + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + + public static final String ID_FIELD = "id"; + public static final String NAME_FIELD = "name"; + public static final String VALUE_FIELD = "value"; + public static final String METADATA_FIELD = "metadata"; + + @ToString.Include + @Getter + private final JsonObject representation; + + @Getter + private final String id; + + @Getter + private final String name; + + @Getter + private final JsonObject value; + + public static CirculationSetting from(JsonObject representation) { + final var id = getProperty(representation, ID_FIELD); + final var name = getProperty(representation, NAME_FIELD); + final var value = getObjectProperty(representation, VALUE_FIELD); + + if (id == null || name == null || value == null || !containsOnlyKnownFields(representation)) { + log.warn("from:: Circulation setting JSON is invalid: {}", representation); + return null; + } + + return new CirculationSetting(representation, id, name, value); + } + + private static boolean containsOnlyKnownFields(JsonObject representation) { + return Set.of(ID_FIELD, NAME_FIELD, VALUE_FIELD, METADATA_FIELD) + .containsAll(representation.fieldNames()); + } +} diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/CirculationSettingsRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/CirculationSettingsRepository.java new file mode 100644 index 0000000000..8125f1761b --- /dev/null +++ b/src/main/java/org/folio/circulation/infrastructure/storage/CirculationSettingsRepository.java @@ -0,0 +1,74 @@ +package org.folio.circulation.infrastructure.storage; + +import static org.folio.circulation.support.http.ResponseMapping.forwardOnFailure; +import static org.folio.circulation.support.http.ResponseMapping.mapUsingJson; +import static org.folio.circulation.support.results.Result.failed; +import static org.folio.circulation.support.results.ResultBinding.flatMapResult; + +import java.lang.invoke.MethodHandles; +import java.util.concurrent.CompletableFuture; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.circulation.domain.CirculationSetting; +import org.folio.circulation.domain.MultipleRecords; +import org.folio.circulation.support.Clients; +import org.folio.circulation.support.CollectionResourceClient; +import org.folio.circulation.support.FetchSingleRecord; +import org.folio.circulation.support.RecordNotFoundFailure; +import org.folio.circulation.support.http.client.ResponseInterpreter; +import org.folio.circulation.support.results.Result; + +public class CirculationSettingsRepository { + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + public static final String RECORDS_PROPERTY_NAME = "circulationSettings"; + private final CollectionResourceClient circulationSettingsStorageClient; + + public CirculationSettingsRepository(Clients clients) { + circulationSettingsStorageClient = clients.circulationSettingsStorageClient(); + } + + public CompletableFuture> getById(String id) { + log.debug("getById:: parameters id: {}", id); + + return FetchSingleRecord.forRecord(RECORDS_PROPERTY_NAME) + .using(circulationSettingsStorageClient) + .mapTo(CirculationSetting::from) + .whenNotFound(failed(new RecordNotFoundFailure(RECORDS_PROPERTY_NAME, id))) + .fetch(id); + } + + public CompletableFuture>> findBy(String query) { + return circulationSettingsStorageClient.getManyWithRawQueryStringParameters(query) + .thenApply(flatMapResult(response -> + MultipleRecords.from(response, CirculationSetting::from, RECORDS_PROPERTY_NAME))); + } + + public CompletableFuture> create( + CirculationSetting circulationSetting) { + + log.debug("create:: parameters circulationSetting: {}", circulationSetting); + + final var storageCirculationSetting = circulationSetting.getRepresentation(); + + return circulationSettingsStorageClient.post(storageCirculationSetting) + .thenApply(interpreter()::flatMap); + } + + public CompletableFuture> update( + CirculationSetting circulationSetting) { + + log.debug("update:: parameters circulationSetting: {}", circulationSetting); + + final var storageCirculationSetting = circulationSetting.getRepresentation(); + + return circulationSettingsStorageClient.put(circulationSetting.getId(), storageCirculationSetting) + .thenApply(interpreter()::flatMap); + } + + private ResponseInterpreter interpreter() { + return new ResponseInterpreter() + .flatMapOn(201, mapUsingJson(CirculationSetting::from)) + .otherwise(forwardOnFailure()); + } +} diff --git a/src/main/java/org/folio/circulation/resources/CirculationSettingsResource.java b/src/main/java/org/folio/circulation/resources/CirculationSettingsResource.java new file mode 100644 index 0000000000..bfc6e989b6 --- /dev/null +++ b/src/main/java/org/folio/circulation/resources/CirculationSettingsResource.java @@ -0,0 +1,154 @@ +package org.folio.circulation.resources; + +import static org.folio.circulation.infrastructure.storage.CirculationSettingsRepository.RECORDS_PROPERTY_NAME; +import static org.folio.circulation.support.ValidationErrorFailure.singleValidationError; +import static org.folio.circulation.support.json.JsonPropertyFetcher.getProperty; +import static org.folio.circulation.support.results.MappingFunctions.toFixedValue; +import static org.folio.circulation.support.results.Result.ofAsync; +import static org.folio.circulation.support.results.Result.succeeded; + +import java.lang.invoke.MethodHandles; +import java.util.UUID; +import java.util.function.Function; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.circulation.domain.CirculationSetting; +import org.folio.circulation.infrastructure.storage.CirculationSettingsRepository; +import org.folio.circulation.support.Clients; +import org.folio.circulation.support.http.server.JsonHttpResponse; +import org.folio.circulation.support.http.server.NoContentResponse; +import org.folio.circulation.support.http.server.WebContext; +import org.folio.circulation.support.results.Result; + +import io.vertx.core.http.HttpClient; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; + +public class CirculationSettingsResource extends CollectionResource { + private static final Logger log = LogManager.getLogger(MethodHandles.lookup().lookupClass()); + + public CirculationSettingsResource(HttpClient client) { + super(client, "/circulation/settings"); + } + + @Override + void create(RoutingContext routingContext) { + final var context = new WebContext(routingContext); + final var clients = Clients.create(context, client); + final var circulationSettingsRepository = new CirculationSettingsRepository(clients); + + final var incomingRepresentation = routingContext.body().asJsonObject(); + setRandomIdIfMissing(incomingRepresentation); + final var circulationSetting = CirculationSetting.from(incomingRepresentation); + log.debug("create:: Creating circulation setting: {}", () -> circulationSetting); + + ofAsync(circulationSetting) + .thenApply(refuseWhenCirculationSettingIsInvalid()) + .thenCompose(r -> r.after(circulationSettingsRepository::create)) + .thenApply(r -> r.map(CirculationSetting::getRepresentation)) + .thenApply(r -> r.map(JsonHttpResponse::created)) + .thenAccept(context::writeResultToHttpResponse); + } + + @Override + void replace(RoutingContext routingContext) { + final var context = new WebContext(routingContext); + final var clients = Clients.create(context, client); + final var circulationSettingsRepository = new CirculationSettingsRepository(clients); + + final var incomingRepresentation = routingContext.body().asJsonObject(); + final var circulationSetting = CirculationSetting.from(incomingRepresentation); + log.debug("replace:: Replacing circulation setting : {}", () -> circulationSetting); + + ofAsync(circulationSetting) + .thenApply(refuseWhenCirculationSettingIsInvalid()) + .thenCompose(r -> r.after(circulationSettingsRepository::update)) + .thenApply(r -> r.map(CirculationSetting::getRepresentation)) + .thenApply(r -> r.map(JsonHttpResponse::created)) + .thenAccept(context::writeResultToHttpResponse); + } + + @Override + void get(RoutingContext routingContext) { + final var context = new WebContext(routingContext); + final var clients = Clients.create(context, client); + final var circulationSettingsRepository = new CirculationSettingsRepository(clients); + + ofAsync(routingContext.request().getParam("id")) + .thenApply(refuseWhenIdIsInvalid()) + .thenApply(r -> r.peek(id -> log.debug("get:: parameters id: {}", id))) + .thenCompose(r -> r.after(circulationSettingsRepository::getById)) + .thenApply(r -> r.map(CirculationSetting::getRepresentation)) + .thenApply(r -> r.map(JsonHttpResponse::ok)) + .thenAccept(context::writeResultToHttpResponse); + } + + @Override + void delete(RoutingContext routingContext) { + final var context = new WebContext(routingContext); + final var clients = Clients.create(context, client); + + ofAsync(routingContext.request().getParam("id")) + .thenApply(refuseWhenIdIsInvalid()) + .thenApply(r -> r.peek(id -> log.debug("delete:: parameters id: {}", id))) + .thenCompose(r -> r.after(clients.circulationSettingsStorageClient()::delete)) + .thenApply(r -> r.map(toFixedValue(NoContentResponse::noContent))) + .thenAccept(context::writeResultToHttpResponse); + } + + @Override + void getMany(RoutingContext routingContext) { + final var context = new WebContext(routingContext); + final var clients = Clients.create(context, client); + final var circulationSettingsRepository = new CirculationSettingsRepository(clients); + + final var query = routingContext.request().query(); + log.debug("get:: parameters id: {}", () -> query); + + circulationSettingsRepository.findBy(query) + .thenApply(multipleLoanRecordsResult -> multipleLoanRecordsResult.map(multipleRecords -> + multipleRecords.asJson(CirculationSetting::getRepresentation, RECORDS_PROPERTY_NAME))) + .thenApply(r -> r.map(JsonHttpResponse::ok)) + .thenAccept(context::writeResultToHttpResponse); + } + + @Override + void empty(RoutingContext routingContext) { + WebContext context = new WebContext(routingContext); + Clients clients = Clients.create(context, client); + + clients.loansStorage().delete() + .thenApply(r -> r.map(toFixedValue(NoContentResponse::noContent))) + .thenAccept(context::writeResultToHttpResponse); + } + + private static void setRandomIdIfMissing(JsonObject representation) { + final var providedId = getProperty(representation, "id"); + if (providedId == null) { + representation.put("id", UUID.randomUUID().toString()); + } + } + + private static Function, Result> + refuseWhenCirculationSettingIsInvalid() { + + return r -> r.failWhen(circulationSetting -> succeeded(circulationSetting == null), + circulationSetting -> singleValidationError("Circulation setting JSON is invalid", "", "")); + } + + private static Function, Result> refuseWhenIdIsInvalid() { + return r -> r.failWhen(id -> succeeded(!uuidIsValid(id)), + circulationSetting -> singleValidationError("Circulation setting ID is not a valid UUID", + "", "")); + } + + private static boolean uuidIsValid(String providedId) { + try { + return providedId != null && providedId.equals(UUID.fromString(providedId).toString()); + } catch(IllegalArgumentException e) { + log.warn("uuidIsValid:: Invalid UUID"); + return false; + } + } +} diff --git a/src/main/java/org/folio/circulation/support/Clients.java b/src/main/java/org/folio/circulation/support/Clients.java index 3ffc41941a..404b433461 100644 --- a/src/main/java/org/folio/circulation/support/Clients.java +++ b/src/main/java/org/folio/circulation/support/Clients.java @@ -69,6 +69,7 @@ public class Clients { private final CollectionResourceClient checkOutLockStorageClient; private final CollectionResourceClient circulationItemClient; private final GetManyRecordsClient settingsStorageClient; + private final CollectionResourceClient circulationSettingsStorageClient; public static Clients create(WebContext context, HttpClient httpClient) { return new Clients(context.createHttpClient(httpClient), context); @@ -136,6 +137,7 @@ private Clients(OkapiHttpClient client, WebContext context) { checkOutLockStorageClient = createCheckoutLockClient(client, context); settingsStorageClient = createSettingsStorageClient(client, context); circulationItemClient = createCirculationItemClient(client, context); + circulationSettingsStorageClient = createCirculationSettingsStorageClient(client, context); } catch(MalformedURLException e) { throw new InvalidOkapiLocationException(context.getOkapiLocation(), e); @@ -374,6 +376,10 @@ public CollectionResourceClient circulationItemClient() { return circulationItemClient; } + public CollectionResourceClient circulationSettingsStorageClient() { + return circulationSettingsStorageClient; + } + private static CollectionResourceClient getCollectionResourceClient( OkapiHttpClient client, WebContext context, String path) @@ -801,6 +807,13 @@ private CollectionResourceClient createCirculationItemClient( return getCollectionResourceClient(client, context, "/circulation-item"); } + private CollectionResourceClient createCirculationSettingsStorageClient( + OkapiHttpClient client, WebContext context) throws MalformedURLException { + + return getCollectionResourceClient(client, context, + "/circulation-settings-storage/circulation-settings"); + } + private GetManyRecordsClient createSettingsStorageClient( OkapiHttpClient client, WebContext context) throws MalformedURLException { diff --git a/src/test/java/api/settings/CirculationSettingsTests.java b/src/test/java/api/settings/CirculationSettingsTests.java new file mode 100644 index 0000000000..da176eb5e9 --- /dev/null +++ b/src/test/java/api/settings/CirculationSettingsTests.java @@ -0,0 +1,106 @@ +package api.settings; + +import static api.support.http.InterfaceUrls.circulationSettingsUrl; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.Test; + +import api.support.APITests; +import api.support.builders.CirculationSettingBuilder; +import api.support.http.CqlQuery; +import io.vertx.core.json.JsonObject; + +class CirculationSettingsTests extends APITests { + + public static final String NAME = "name"; + public static final String VALUE = "value"; + public static final String ERRORS = "errors"; + public static final String MESSAGE = "message"; + public static final String INVALID_JSON_MESSAGE = "Circulation setting JSON is invalid"; + + @Test + void crudOperationsTest() { + // Testing POST method + final var setting = circulationSettingsClient.create(new CirculationSettingBuilder() + .withName("initial-name") + .withValue(new JsonObject().put("initial-key", "initial-value"))); + final var settingId = setting.getId(); + + // Testing GET (individual setting) method + final var settingById = circulationSettingsClient.get(settingId); + assertThat(settingById.getJson().getString(NAME), is("initial-name")); + assertThat(settingById.getJson().getJsonObject(VALUE).getString("initial-key"), + is("initial-value")); + + // Testing GET (all) method + final var anotherSetting = circulationSettingsClient.create(new CirculationSettingBuilder() + .withName("another-name") + .withValue(new JsonObject().put("another-key", "another-value"))); + final var allSettings = circulationSettingsClient.getMany(CqlQuery.noQuery()); + assertThat(allSettings.size(), is(2)); + + // Testing DELETE method + circulationSettingsClient.delete(anotherSetting.getId()); + final var allSettingsAfterDeletion = circulationSettingsClient.getMany(CqlQuery.noQuery()); + assertThat(allSettingsAfterDeletion.size(), is(1)); + assertThat(allSettingsAfterDeletion.getFirst().getString(NAME), is("initial-name")); + assertThat(allSettingsAfterDeletion.getFirst().getJsonObject(VALUE).getString("initial-key"), + is("initial-value")); + + // Testing PUT method + circulationSettingsClient.replace(settingId, new CirculationSettingBuilder() + .withId(settingId) + .withName("new-name") + .withValue(new JsonObject().put("new-key", "new-value"))); + + final var updatedSetting = circulationSettingsClient.get(settingId); + + assertThat(updatedSetting.getJson().getString(NAME), is("new-name")); + assertThat(updatedSetting.getJson().getJsonObject(VALUE).getString("new-key"), + is("new-value")); + } + + @Test + void invalidRequestsTest() { + circulationSettingsClient.create(new CirculationSettingBuilder() + .withName("initial-name") + .withValue(new JsonObject().put("initial-key", "initial-value"))); + + // Testing GET with wrong UUID + restAssuredClient.get(circulationSettingsUrl("/" + randomId()), 404, + "get-circulation-setting"); + + // Testing GET with invalid ID (not a UUID) + var getErrors = restAssuredClient.get(circulationSettingsUrl("/not-a-uuid"), 422, + "get-circulation-setting"); + assertThat(getErrors.getJson().getJsonArray(ERRORS).getJsonObject(0).getString(MESSAGE), + is("Circulation setting ID is not a valid UUID")); + + // Testing DELETE with invalid ID + restAssuredClient.delete(circulationSettingsUrl("/" + randomId()), 204, + "delete-circulation-setting"); + + // Testing PUT with malformed JSON + var putErrors = restAssuredClient.put("{\"invalid-field\": \"invalid-value\"}", + circulationSettingsUrl("/" + randomId()), 422, "put-circulation-setting"); + assertThat(putErrors.getJson().getJsonArray(ERRORS).getJsonObject(0).getString(MESSAGE), + is(INVALID_JSON_MESSAGE)); + + var putErrorsNoValue = restAssuredClient.put("{\"name\": \"test-name\"}", + circulationSettingsUrl("/" + randomId()), 422, "put-circulation-setting"); + assertThat(putErrorsNoValue.getJson().getJsonArray(ERRORS).getJsonObject(0).getString(MESSAGE), + is(INVALID_JSON_MESSAGE)); + + // Testing POST with malformed JSON + var postErrors = restAssuredClient.post("{\"invalid-field\": \"invalid-value\"}", + circulationSettingsUrl(""), 422, "put-circulation-setting"); + assertThat(postErrors.getJson().getJsonArray(ERRORS).getJsonObject(0).getString(MESSAGE), + is(INVALID_JSON_MESSAGE)); + + var postErrorsNoValue = restAssuredClient.put("{\"name\": \"test-name\"}", + circulationSettingsUrl("/" + randomId()), 422, "put-circulation-setting"); + assertThat(postErrorsNoValue.getJson().getJsonArray(ERRORS).getJsonObject(0).getString(MESSAGE), + is(INVALID_JSON_MESSAGE)); + } +} diff --git a/src/test/java/api/support/APITests.java b/src/test/java/api/support/APITests.java index ddc2dce289..b872ce7165 100644 --- a/src/test/java/api/support/APITests.java +++ b/src/test/java/api/support/APITests.java @@ -194,6 +194,9 @@ public abstract class APITests { protected final ResourceClient actualCostRecordsClient = ResourceClient.forActualCostRecordsStorage(); + protected final ResourceClient circulationSettingsClient = + ResourceClient.forCirculationSettings(); + protected final ServicePointsFixture servicePointsFixture = new ServicePointsFixture(servicePointsClient); diff --git a/src/test/java/api/support/builders/CirculationSettingBuilder.java b/src/test/java/api/support/builders/CirculationSettingBuilder.java new file mode 100644 index 0000000000..a33fb99345 --- /dev/null +++ b/src/test/java/api/support/builders/CirculationSettingBuilder.java @@ -0,0 +1,34 @@ +package api.support.builders; + +import java.util.UUID; + +import io.vertx.core.json.JsonObject; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.With; + +@NoArgsConstructor +@AllArgsConstructor +@With +public class CirculationSettingBuilder extends JsonBuilder implements Builder { + private UUID id = null; + private String name = null; + private JsonObject value = null; + + @Override + public JsonObject create() { + JsonObject circulationSetting = new JsonObject(); + + if (id != null) { + put(circulationSetting, "id", id); + } + if (name != null) { + put(circulationSetting, "name", name); + } + if (value != null) { + put(circulationSetting, "value", value); + } + + return circulationSetting; + } +} diff --git a/src/test/java/api/support/fakes/FakeOkapi.java b/src/test/java/api/support/fakes/FakeOkapi.java index 9cdfb40600..5cbecb8542 100644 --- a/src/test/java/api/support/fakes/FakeOkapi.java +++ b/src/test/java/api/support/fakes/FakeOkapi.java @@ -415,6 +415,13 @@ public void start(Promise startFuture) throws IOException { .withChangeMetadata() .create().register(router); + new FakeStorageModuleBuilder() + .withRecordName("circulationSettings") + .withCollectionPropertyName("circulationSettings") + .withRootPath("/circulation-settings-storage/circulation-settings") + .withChangeMetadata() + .create().register(router); + new FakeFeeFineOperationsModule().register(router); server.requestHandler(router) diff --git a/src/test/java/api/support/http/InterfaceUrls.java b/src/test/java/api/support/http/InterfaceUrls.java index fedf39d696..59d35de534 100644 --- a/src/test/java/api/support/http/InterfaceUrls.java +++ b/src/test/java/api/support/http/InterfaceUrls.java @@ -334,4 +334,7 @@ public static URL settingsStorageUrl() { return APITestContext.viaOkapiModuleUrl("/settings/entries"); } + public static URL circulationSettingsUrl(String subPath) { + return circulationModuleUrl("/circulation/settings" + subPath); + } } diff --git a/src/test/java/api/support/http/ResourceClient.java b/src/test/java/api/support/http/ResourceClient.java index 844fff542d..8e9b7d63be 100644 --- a/src/test/java/api/support/http/ResourceClient.java +++ b/src/test/java/api/support/http/ResourceClient.java @@ -272,6 +272,10 @@ public static ResourceClient forActualCostRecordsStorage() { return new ResourceClient(InterfaceUrls::actualCostRecordsStorageUrl, "actualCostRecords"); } + public static ResourceClient forCirculationSettings() { + return new ResourceClient(InterfaceUrls::circulationSettingsUrl, "circulationSettings"); + } + private ResourceClient(UrlMaker urlMaker, String collectionArrayPropertyName) { this.urlMaker = urlMaker; this.collectionArrayPropertyName = collectionArrayPropertyName;