updateStoredPackage(Tags tags, DbPackage pkg, String tenant) {
+ if (!tags.getTagList().isEmpty()) {
+ return packageRepository.save(pkg, tenant);
+ }
+ return packageRepository.delete(pkg.getId(), pkg.getCredentialsId(), tenant);
+ }
+
+ private DbPackage createDbPackage(String packageId, UUID credentialsId, PackageTagsDataAttributes attributes) {
+ return DbPackage.builder()
+ .id(parsePackageId(packageId))
+ .credentialsId(credentialsId)
+ .name(attributes.getName())
+ .contentType(ConverterConsts.CONTENT_TYPES.inverseBidiMap().get(attributes.getContentType()))
+ .build();
+ }
+}
diff --git a/src/main/java/org/folio/service/sanitizer/HtmlSanitizer.java b/src/main/java/org/folio/service/sanitizer/HtmlSanitizer.java
new file mode 100644
index 00000000..b8909484
--- /dev/null
+++ b/src/main/java/org/folio/service/sanitizer/HtmlSanitizer.java
@@ -0,0 +1,40 @@
+package org.folio.service.sanitizer;
+
+import org.apache.commons.lang3.StringUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.safety.Safelist;
+import org.springframework.stereotype.Component;
+
+/**
+ * Strips HTML content down to a fixed set of allowed tags and attributes using {@link Jsoup}.
+ *
+ * The allowed set is hardcoded rather than externally configurable: {@code p}, {@code strong}, {@code em},
+ * {@code a}, {@code u}, {@code ol}, {@code ul}, {@code li}, {@code h1}, {@code h2}, {@code h3} and {@code br} tags,
+ * plus {@code class}/{@code style} attributes on any allowed tag and {@code href}/{@code rel}/{@code target} on
+ * {@code a}. Any other tag or attribute is removed, though the text content of a stripped tag is kept (with the
+ * exception of tags like {@code script} whose content is data, not text, and is discarded).
+ */
+@Component
+public class HtmlSanitizer implements Sanitizer {
+
+ private static final Safelist SAFELIST = new Safelist()
+ .addTags("p", "strong", "em", "a", "u", "ol", "ul", "li", "h1", "h2", "h3", "br")
+ .addAttributes(":all", "class", "style")
+ .addAttributes("a", "href", "rel", "target");
+
+ private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);
+
+ /**
+ * Sanitizes the given HTML content, removing any tag or attribute not on the allowed list.
+ *
+ * @param target the HTML content to sanitize; a blank or {@code null} value is returned unchanged.
+ * @return the sanitized content, with output pretty-printing disabled so it is not reformatted.
+ */
+ @Override
+ public String sanitize(String target) {
+ return StringUtils.isNotBlank(target)
+ ? Jsoup.clean(target, "", SAFELIST, OUTPUT_SETTINGS)
+ : target;
+ }
+}
diff --git a/src/main/java/org/folio/service/sanitizer/Sanitizer.java b/src/main/java/org/folio/service/sanitizer/Sanitizer.java
new file mode 100644
index 00000000..ad11cdbc
--- /dev/null
+++ b/src/main/java/org/folio/service/sanitizer/Sanitizer.java
@@ -0,0 +1,6 @@
+package org.folio.service.sanitizer;
+
+public interface Sanitizer {
+
+ T sanitize(T target);
+}
diff --git a/src/test/java/org/folio/rest/converter/packages/CustomPackagePostRequestConverterTest.java b/src/test/java/org/folio/rest/converter/packages/CustomPackagePostRequestConverterTest.java
index 6834ad83..0ca2a5b2 100644
--- a/src/test/java/org/folio/rest/converter/packages/CustomPackagePostRequestConverterTest.java
+++ b/src/test/java/org/folio/rest/converter/packages/CustomPackagePostRequestConverterTest.java
@@ -6,6 +6,8 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import java.util.List;
import org.folio.rest.jaxrs.model.ContentType;
@@ -15,11 +17,21 @@
import org.folio.rest.jaxrs.model.PackagePostDataAttributes;
import org.folio.rest.jaxrs.model.PackagePostRequest;
import org.folio.rest.jaxrs.model.ProxyDto;
+import org.folio.service.sanitizer.Sanitizer;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+@ExtendWith(MockitoExtension.class)
class CustomPackagePostRequestConverterTest {
- private final CustomPackagePostRequestConverter converter = new CustomPackagePostRequestConverter();
+ @Mock
+ private Sanitizer htmlSanitizer;
+
+ @InjectMocks
+ private CustomPackagePostRequestConverter converter;
@Test
void shouldMapPackageName() {
@@ -30,11 +42,15 @@ void shouldMapPackageName() {
}
@Test
- void shouldMapCustomDescription() {
+ void shouldSanitizeCustomDescription() {
+ when(htmlSanitizer.sanitize("Some description")).thenReturn("sanitized description");
+
var result = converter.convert(buildRequest(new PackagePostDataAttributes()
.withContentType(ContentType.UNKNOWN)
.withCustomDescription("Some description")));
- assertEquals("Some description", result.getCustomDescription());
+
+ assertEquals("sanitized description", result.getCustomDescription());
+ verify(htmlSanitizer).sanitize("Some description");
}
@Test
diff --git a/src/test/java/org/folio/rest/converter/packages/CustomPackagePutRequestConverterTest.java b/src/test/java/org/folio/rest/converter/packages/CustomPackagePutRequestConverterTest.java
index 91495872..2d276b5d 100644
--- a/src/test/java/org/folio/rest/converter/packages/CustomPackagePutRequestConverterTest.java
+++ b/src/test/java/org/folio/rest/converter/packages/CustomPackagePutRequestConverterTest.java
@@ -6,17 +6,29 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import java.util.List;
import org.folio.rest.jaxrs.model.ContentType;
import org.folio.rest.jaxrs.model.Coverage;
import org.folio.rest.jaxrs.model.PackagePutDataAttributes;
import org.folio.rest.jaxrs.model.PackageVisibility;
+import org.folio.service.sanitizer.Sanitizer;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+@ExtendWith(MockitoExtension.class)
class CustomPackagePutRequestConverterTest {
- private final CustomPackagePutRequestConverter converter = new CustomPackagePutRequestConverter();
+ @Mock
+ private Sanitizer htmlSanitizer;
+
+ @InjectMocks
+ private CustomPackagePutRequestConverter converter;
@Test
void shouldCreateRequestToChangeCustomPackageName() {
@@ -48,6 +60,18 @@ void shouldCreateRequestToChangeCustomPackageCoverageDatesToEmpty() {
assertEquals("", packagePut.getCustomCoverage().getEndCoverage());
}
+ @Test
+ void shouldSanitizeCustomDescription() {
+ when(htmlSanitizer.sanitize("Some description")).thenReturn("sanitized description");
+
+ var packagePut = converter.convert(getPackagePutRequest(
+ new PackagePutDataAttributes()
+ .withCustomDescription("Some description")));
+
+ assertEquals("sanitized description", packagePut.getCustomDescription());
+ verify(htmlSanitizer).sanitize("Some description");
+ }
+
@Test
void shouldCreateRequestToChangeCustomPackageContentType() {
var packagePut = converter.convert(getPackagePutRequest(
diff --git a/src/test/java/org/folio/rest/converter/packages/ManagedPackagePutRequestConverterTest.java b/src/test/java/org/folio/rest/converter/packages/ManagedPackagePutRequestConverterTest.java
index 60982fc3..61b54b48 100644
--- a/src/test/java/org/folio/rest/converter/packages/ManagedPackagePutRequestConverterTest.java
+++ b/src/test/java/org/folio/rest/converter/packages/ManagedPackagePutRequestConverterTest.java
@@ -6,16 +6,28 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import java.util.List;
import org.folio.rest.jaxrs.model.Coverage;
import org.folio.rest.jaxrs.model.PackagePutDataAttributes;
import org.folio.rest.jaxrs.model.PackageVisibility;
+import org.folio.service.sanitizer.Sanitizer;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+@ExtendWith(MockitoExtension.class)
class ManagedPackagePutRequestConverterTest {
- private final ManagedPackagePutRequestConverter converter = new ManagedPackagePutRequestConverter();
+ @Mock
+ private Sanitizer htmlSanitizer;
+
+ @InjectMocks
+ private ManagedPackagePutRequestConverter converter;
@Test
void shouldCreateRequestToSelectPackage() {
@@ -63,6 +75,18 @@ void shouldCreateRequestToAllowKbAddTitlesToPackage() {
assertTrue(packagePut.getAllowEbscoToAddTitles());
}
+ @Test
+ void shouldSanitizeCustomDescription() {
+ when(htmlSanitizer.sanitize("Some description")).thenReturn("sanitized description");
+
+ var packagePut = converter.convert(getPackagePutRequest(
+ new PackagePutDataAttributes()
+ .withCustomDescription("Some description")));
+
+ assertEquals("sanitized description", packagePut.getCustomDescription());
+ verify(htmlSanitizer).sanitize("Some description");
+ }
+
@Test
void shouldCreateRequestToAddCustomCoverage() {
var packagePut = converter.convert(getPackagePutRequest(
diff --git a/src/test/java/org/folio/rest/impl/EholdingsPackagesImplTest.java b/src/test/java/org/folio/rest/impl/EholdingsPackagesImplTest.java
new file mode 100644
index 00000000..bf965faa
--- /dev/null
+++ b/src/test/java/org/folio/rest/impl/EholdingsPackagesImplTest.java
@@ -0,0 +1,316 @@
+package org.folio.rest.impl;
+
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Handler;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Response;
+import org.folio.HttpStatus;
+import org.folio.holdingsiq.model.PackageId;
+import org.folio.holdingsiq.service.exception.ResourceNotFoundException;
+import org.folio.okapi.common.XOkapiHeaders;
+import org.folio.rest.exception.InputValidationException;
+import org.folio.rest.jaxrs.model.KbCredentials;
+import org.folio.rest.jaxrs.model.PackagePostBulkFetchRequest;
+import org.folio.rest.jaxrs.model.PackagePostData;
+import org.folio.rest.jaxrs.model.PackagePostDataAttributes;
+import org.folio.rest.jaxrs.model.PackagePostRequest;
+import org.folio.rest.jaxrs.model.PackagePutData;
+import org.folio.rest.jaxrs.model.PackagePutDataAttributes;
+import org.folio.rest.jaxrs.model.PackagePutRequest;
+import org.folio.rest.jaxrs.model.PackageTagsDataAttributes;
+import org.folio.rest.jaxrs.model.PackageTagsPutData;
+import org.folio.rest.jaxrs.model.PackageTagsPutRequest;
+import org.folio.rest.model.filter.PackageRecordFilter;
+import org.folio.rest.model.filter.ResourceFilter;
+import org.folio.rest.util.template.RmApiTemplate;
+import org.folio.rest.util.template.RmApiTemplateContext;
+import org.folio.rest.util.template.RmApiTemplateFactory;
+import org.folio.rmapi.PackageServiceImpl;
+import org.folio.service.kbcredentials.UserKbCredentialsService;
+import org.folio.service.loader.FilteredEntitiesLoader;
+import org.folio.service.packages.PackageService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.objenesis.ObjenesisStd;
+import org.springframework.test.util.ReflectionTestUtils;
+
+@ExtendWith(MockitoExtension.class)
+class EholdingsPackagesImplTest {
+
+ private static final Map OKAPI_HEADERS = Map.of(
+ XOkapiHeaders.TENANT, "fs",
+ XOkapiHeaders.URL, "http://localhost:8080");
+
+ @Mock
+ private PackageService packageService;
+ @Mock
+ private RmApiTemplateFactory templateFactory;
+ @Mock
+ private FilteredEntitiesLoader filteredEntitiesLoader;
+ @Mock
+ private UserKbCredentialsService userKbCredentialsService;
+ @Mock
+ private RmApiTemplate template;
+ @Mock
+ private Handler> asyncResultHandler;
+ @Mock
+ private PackageServiceImpl packagesRmApiService;
+ @Captor
+ private ArgumentCaptor> responseCaptor;
+
+ private EholdingsPackagesImpl impl;
+ private RmApiTemplateContext context;
+
+ @BeforeEach
+ void setUp() {
+ impl = new ObjenesisStd().newInstance(EholdingsPackagesImpl.class);
+ ReflectionTestUtils.setField(impl, "packageService", packageService);
+ ReflectionTestUtils.setField(impl, "templateFactory", templateFactory);
+ ReflectionTestUtils.setField(impl, "filteredEntitiesLoader", filteredEntitiesLoader);
+ ReflectionTestUtils.setField(impl, "userKbCredentialsService", userKbCredentialsService);
+
+ lenient().when(template.requestAction(any())).thenReturn(template);
+ lenient().when(template.addErrorMapper(any(), any())).thenReturn(template);
+ lenient().when(templateFactory.createTemplate(anyMap(), ArgumentMatchers.>>any()))
+ .thenReturn(template);
+
+ context = RmApiTemplateContext.builder()
+ .credentialsId(UUID.randomUUID().toString())
+ .packagesService(packagesRmApiService)
+ .build();
+ }
+
+ @SuppressWarnings("unchecked")
+ private Function> captureRequestAction() {
+ var captor = ArgumentCaptor.forClass(Function.class);
+ verify(template).requestAction(captor.capture());
+ return captor.getValue();
+ }
+
+ @Test
+ void shouldFetchPackagesByTagFilterWhenTagsFilterIsGiven() {
+ var filter = PackageRecordFilter.builder().filterTags(List.of("tag1")).sort("relevance")
+ .page(1).count(25).build();
+ when(filteredEntitiesLoader.fetchPackagesByTagFilter(any(), any())).thenReturn(completedFuture(null));
+
+ invokeGetEholdingsPackages(filter);
+
+ captureRequestAction().apply(context);
+ verify(filteredEntitiesLoader).fetchPackagesByTagFilter(any(), eq(context));
+ verify(packageService, never()).retrievePackages(any(), any(), any());
+ verify(template).executeWithResult(org.folio.rest.jaxrs.model.PackageCollection.class);
+ }
+
+ @Test
+ void shouldFetchPackagesByAccessTypeFilterWhenAccessTypeFilterIsGiven() {
+ var filter = PackageRecordFilter.builder().filterAccessType(List.of("at1")).sort("relevance")
+ .page(1).count(25).build();
+ when(filteredEntitiesLoader.fetchPackagesByAccessTypeFilter(any(), any())).thenReturn(completedFuture(null));
+
+ invokeGetEholdingsPackages(filter);
+
+ captureRequestAction().apply(context);
+ verify(filteredEntitiesLoader).fetchPackagesByAccessTypeFilter(any(), eq(context));
+ }
+
+ @Test
+ void shouldResolveCustomProviderIdWhenFilterCustomIsTrue() {
+ var filter = PackageRecordFilter.builder().filterCustom("true").sort("relevance").page(1).count(25).build();
+ when(packageService.getCustomProviderIdAndRetrievePackages(filter, context)).thenReturn(completedFuture(null));
+
+ invokeGetEholdingsPackages(filter);
+
+ captureRequestAction().apply(context);
+ verify(packageService).getCustomProviderIdAndRetrievePackages(filter, context);
+ verify(packageService, never()).retrievePackages(any(), any(), any());
+ }
+
+ @Test
+ void shouldRetrieveAllPackagesWhenFilterCustomIsNotSet() {
+ var filter = PackageRecordFilter.builder().sort("relevance").page(1).count(25).build();
+ when(packageService.retrievePackages(null, filter, context)).thenReturn(completedFuture(null));
+
+ invokeGetEholdingsPackages(filter);
+
+ captureRequestAction().apply(context);
+ verify(packageService).retrievePackages(null, filter, context);
+ }
+
+ private void invokeGetEholdingsPackages(PackageRecordFilter filter) {
+ impl.getEholdingsPackages(filter.getFilterCustom(), filter.getQuery(), filter.getQueryField(),
+ filter.getQueryType(), filter.isHighlight(), filter.getFilterSelected(), filter.getFilterType(),
+ filter.getFilterVisibility(), filter.getFilterFreeAccess(), filter.getFilterTags(),
+ filter.getFilterAccessType(), filter.getSort(), filter.getPage(), filter.getCount(), OKAPI_HEADERS,
+ asyncResultHandler, null);
+ }
+
+ @Test
+ void shouldCreateCustomPackage() {
+ var entity = new PackagePostRequest()
+ .withData(new PackagePostData().withAttributes(new PackagePostDataAttributes()));
+ when(packageService.createCustomPackage(entity, context)).thenReturn(completedFuture(null));
+
+ impl.postEholdingsPackages("application/json", entity, OKAPI_HEADERS, asyncResultHandler, null);
+
+ captureRequestAction().apply(context);
+ verify(packageService).createCustomPackage(entity, context);
+ verify(template).addErrorMapper(eq(NotFoundException.class), any());
+ verify(template).executeWithResult(org.folio.rest.jaxrs.model.Package.class);
+ }
+
+ @Test
+ void shouldRetrievePackageWithRelatedData() {
+ when(packageService.retrievePackageWithRelatedData(any(), any(), any())).thenReturn(completedFuture(null));
+
+ impl.getEholdingsPackagesByPackageId("19-3964", "provider,resources", OKAPI_HEADERS, asyncResultHandler, null);
+
+ captureRequestAction().apply(context);
+ verify(packageService).retrievePackageWithRelatedData(new PackageId(19, 3964),
+ List.of("provider", "resources"), context);
+ verify(template).executeWithResult(org.folio.rest.jaxrs.model.Package.class);
+ }
+
+ @Test
+ void shouldUpdatePackage() {
+ var entity = new PackagePutRequest()
+ .withData(new PackagePutData().withAttributes(new PackagePutDataAttributes()));
+ when(packageService.updatePackage(any(), any(), any())).thenReturn(completedFuture(null));
+
+ impl.putEholdingsPackagesByPackageId("19-3964", "application/json", entity, OKAPI_HEADERS, asyncResultHandler,
+ null);
+
+ captureRequestAction().apply(context);
+ verify(packageService).updatePackage(new PackageId(19, 3964), entity, context);
+ verify(template).addErrorMapper(eq(NotFoundException.class), any());
+ verify(template).addErrorMapper(eq(InputValidationException.class), any());
+ verify(template).executeWithResult(org.folio.rest.jaxrs.model.Package.class);
+ }
+
+ @Test
+ void shouldDeletePackage() {
+ when(packageService.deletePackage(any(), any())).thenReturn(completedFuture(null));
+
+ impl.deleteEholdingsPackagesByPackageId("19-3964", OKAPI_HEADERS, asyncResultHandler, null);
+
+ captureRequestAction().apply(context);
+ verify(packageService).deletePackage(new PackageId(19, 3964), context);
+ verify(template).execute();
+ verify(template, never()).executeWithResult(any());
+ }
+
+ @Test
+ void shouldFetchResourcesByTagFilterWhenTagsFilterIsGiven() {
+ var filter = ResourceFilter.builder().packageId("19-3964").filterTags(List.of("tag1"))
+ .page(1).count(25).build();
+ when(filteredEntitiesLoader.fetchResourcesByTagFilter(any(), any())).thenReturn(completedFuture(null));
+
+ invokeGetResources(filter);
+
+ captureRequestAction().apply(context);
+ verify(filteredEntitiesLoader).fetchResourcesByTagFilter(any(), eq(context));
+ verify(template).addErrorMapper(eq(ResourceNotFoundException.class), any());
+ verify(template).executeWithResult(org.folio.rest.jaxrs.model.ResourceCollection.class);
+ }
+
+ @Test
+ void shouldFetchResourcesByAccessTypeFilterWhenAccessTypeFilterIsGiven() {
+ var filter = ResourceFilter.builder().packageId("19-3964").filterAccessType(List.of("at1"))
+ .page(1).count(25).build();
+ when(filteredEntitiesLoader.fetchResourcesByAccessTypeFilter(any(), any())).thenReturn(completedFuture(null));
+
+ invokeGetResources(filter);
+
+ captureRequestAction().apply(context);
+ verify(filteredEntitiesLoader).fetchResourcesByAccessTypeFilter(any(), eq(context));
+ }
+
+ @Test
+ void shouldRetrievePackageTitlesWhenNoTagOrAccessTypeFilterIsGiven() {
+ var filter = ResourceFilter.builder().packageId("19-3964").filterName("test").sort("relevance")
+ .page(1).count(25).build();
+ Function> stubFunction = ctx -> completedFuture(null);
+ when(packageService.retrievePackageTitles(filter)).thenReturn(stubFunction);
+
+ invokeGetResources(filter);
+
+ verify(template).requestAction(same(stubFunction));
+ }
+
+ private void invokeGetResources(ResourceFilter filter) {
+ impl.getEholdingsPackagesResourcesByPackageId(filter.getPackageId(), filter.getFilterTags(),
+ filter.getFilterAccessType(), filter.getFilterSelected(), filter.getFilterType(), filter.getFilterName(),
+ filter.getFilterIsxn(), filter.getFilterSubject(), filter.getFilterPublisher(), filter.getSort(),
+ filter.getPage(), filter.getCount(), OKAPI_HEADERS, asyncResultHandler, null);
+ }
+
+ @Test
+ void shouldUpdateTagsForPackage() {
+ var credentialsId = UUID.randomUUID();
+ var attributes = new PackageTagsDataAttributes().withName("name");
+ var entity = new PackageTagsPutRequest().withData(new PackageTagsPutData().withAttributes(attributes));
+ when(userKbCredentialsService.findByUser(OKAPI_HEADERS))
+ .thenReturn(completedFuture(new KbCredentials().withId(credentialsId.toString())));
+ when(packageService.updateTagsForPackage(entity, credentialsId, "19-3964", "fs"))
+ .thenReturn(completedFuture(attributes));
+
+ impl.putEholdingsPackagesTagsByPackageId("19-3964", "application/json", entity, OKAPI_HEADERS,
+ asyncResultHandler, null);
+
+ verify(asyncResultHandler).handle(responseCaptor.capture());
+ var response = responseCaptor.getValue().result();
+ assertEquals(HttpStatus.SC_OK, response.getStatus());
+ verify(packageService).updateTagsForPackage(entity, credentialsId, "19-3964", "fs");
+ }
+
+ @Test
+ void shouldReturn422WhenTagsUpdateFails() {
+ var credentialsId = UUID.randomUUID();
+ var attributes = new PackageTagsDataAttributes().withName("name");
+ var entity = new PackageTagsPutRequest().withData(new PackageTagsPutData().withAttributes(attributes));
+ when(userKbCredentialsService.findByUser(OKAPI_HEADERS))
+ .thenReturn(completedFuture(new KbCredentials().withId(credentialsId.toString())));
+ when(packageService.updateTagsForPackage(any(), any(), any(), any()))
+ .thenReturn(CompletableFuture.failedFuture(new InputValidationException("invalid", "invalid tags")));
+
+ impl.putEholdingsPackagesTagsByPackageId("19-3964", "application/json", entity, OKAPI_HEADERS,
+ asyncResultHandler, null);
+
+ verify(asyncResultHandler).handle(responseCaptor.capture());
+ var response = responseCaptor.getValue().result();
+ assertEquals(HttpStatus.SC_UNPROCESSABLE_CONTENT, response.getStatus());
+ }
+
+ @Test
+ void shouldRetrievePackagesBulk() {
+ var entity = new PackagePostBulkFetchRequest().withPackages(java.util.Set.of("19-3964"));
+ when(packagesRmApiService.retrievePackagesBulk(entity.getPackages())).thenReturn(completedFuture(null));
+
+ impl.postEholdingsPackagesBulkFetch("application/json", entity, OKAPI_HEADERS, asyncResultHandler, null);
+
+ captureRequestAction().apply(context);
+ verify(packagesRmApiService).retrievePackagesBulk(entity.getPackages());
+ verify(template).executeWithResult(org.folio.rest.jaxrs.model.PackageBulkFetchCollection.class);
+ }
+}
diff --git a/src/test/java/org/folio/service/packages/CustomProviderIdServiceTest.java b/src/test/java/org/folio/service/packages/CustomProviderIdServiceTest.java
new file mode 100644
index 00000000..ad82cb7e
--- /dev/null
+++ b/src/test/java/org/folio/service/packages/CustomProviderIdServiceTest.java
@@ -0,0 +1,88 @@
+package org.folio.service.packages;
+
+import static org.folio.util.TestUtil.result;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import io.vertx.core.Vertx;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import org.folio.cache.VertxCache;
+import org.folio.config.cache.VendorIdCacheKey;
+import org.folio.holdingsiq.model.Configuration;
+import org.folio.holdingsiq.model.RequestContext;
+import org.folio.okapi.common.XOkapiHeaders;
+import org.folio.rest.util.template.RmApiTemplateContext;
+import org.folio.rmapi.ProvidersServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class CustomProviderIdServiceTest {
+
+ @Mock
+ private ProvidersServiceImpl providersService;
+
+ private CustomProviderIdService customProviderIdService;
+ private RmApiTemplateContext context;
+
+ @BeforeEach
+ void setUp() {
+ var vendorIdCache = new VertxCache(Vertx.vertx(), 60, "vendorIdCacheTest");
+ customProviderIdService = new CustomProviderIdService(vendorIdCache);
+
+ var requestContext = new RequestContext(Map.of(
+ XOkapiHeaders.TENANT, "fs",
+ XOkapiHeaders.URL, "http://localhost:8080"));
+ var configuration = Configuration.builder().customerId("customerId").apiKey("apiKey").build();
+
+ context = RmApiTemplateContext.builder()
+ .requestContext(requestContext)
+ .configuration(configuration)
+ .providersService(providersService)
+ .build();
+ }
+
+ @Test
+ void shouldFetchVendorIdWhenNotCached() {
+ when(providersService.getVendorId()).thenReturn(CompletableFuture.completedFuture(111));
+
+ var result = result(customProviderIdService.getCustomProviderId(context));
+
+ assertEquals(111, result);
+ verify(providersService).getVendorId();
+ }
+
+ @Test
+ void shouldReturnCachedVendorIdWithoutCallingRmApiAgain() {
+ when(providersService.getVendorId()).thenReturn(CompletableFuture.completedFuture(111));
+
+ var firstResult = result(customProviderIdService.getCustomProviderId(context));
+ var secondResult = result(customProviderIdService.getCustomProviderId(context));
+
+ assertEquals(111, firstResult);
+ assertEquals(111, secondResult);
+ verify(providersService, times(1)).getVendorId();
+ }
+
+ @Test
+ void shouldNotShareCacheAcrossDifferentTenants() {
+ when(providersService.getVendorId()).thenReturn(CompletableFuture.completedFuture(111));
+
+ result(customProviderIdService.getCustomProviderId(context));
+
+ var otherTenantContext = context.toBuilder()
+ .requestContext(new RequestContext(Map.of(
+ XOkapiHeaders.TENANT, "other",
+ XOkapiHeaders.URL, "http://localhost:8080")))
+ .build();
+ result(customProviderIdService.getCustomProviderId(otherTenantContext));
+
+ verify(providersService, times(2)).getVendorId();
+ }
+}
diff --git a/src/test/java/org/folio/service/packages/PackageAccessTypeServiceTest.java b/src/test/java/org/folio/service/packages/PackageAccessTypeServiceTest.java
new file mode 100644
index 00000000..a44f0ce5
--- /dev/null
+++ b/src/test/java/org/folio/service/packages/PackageAccessTypeServiceTest.java
@@ -0,0 +1,99 @@
+package org.folio.service.packages;
+
+import static org.folio.util.TestUtil.result;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import org.folio.holdingsiq.model.PackageData;
+import org.folio.holdingsiq.model.RequestContext;
+import org.folio.okapi.common.XOkapiHeaders;
+import org.folio.repository.RecordType;
+import org.folio.rest.jaxrs.model.AccessType;
+import org.folio.rest.util.template.RmApiTemplateContext;
+import org.folio.rmapi.result.PackageResult;
+import org.folio.service.accesstypes.AccessTypeMappingsService;
+import org.folio.service.accesstypes.AccessTypesService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class PackageAccessTypeServiceTest {
+
+ private static final String CREDENTIALS_ID = "credentials-id";
+ private static final String ACCESS_TYPE_ID = "access-type-id";
+
+ @Mock
+ private AccessTypesService accessTypesService;
+ @Mock
+ private AccessTypeMappingsService accessTypeMappingsService;
+ @InjectMocks
+ private PackageAccessTypeService packageAccessTypeService;
+ private RmApiTemplateContext context;
+
+ @BeforeEach
+ void setUp() {
+ var requestContext = new RequestContext(Map.of(
+ XOkapiHeaders.TENANT, "fs",
+ XOkapiHeaders.URL, "http://localhost:8080"));
+ context = RmApiTemplateContext.builder()
+ .requestContext(requestContext)
+ .credentialsId(CREDENTIALS_ID)
+ .build();
+ }
+
+ @Test
+ void shouldReturnNullWhenAccessTypeIdIsNull() {
+ var result = result(packageAccessTypeService.fetchAccessType(null, context));
+
+ assertNull(result);
+ verifyNoInteractions(accessTypesService);
+ }
+
+ @Test
+ void shouldFetchAccessTypeByCredentialsAndAccessTypeId() {
+ var accessType = new AccessType();
+ when(accessTypesService.findByCredentialsAndAccessTypeId(CREDENTIALS_ID, ACCESS_TYPE_ID, false,
+ context.getRequestContext().getHeaders())).thenReturn(CompletableFuture.completedFuture(accessType));
+
+ var result = result(packageAccessTypeService.fetchAccessType(ACCESS_TYPE_ID, context));
+
+ assertSame(accessType, result);
+ }
+
+ @Test
+ void shouldAssignAccessTypeToPackageResultAndUpdateMapping() {
+ when(accessTypeMappingsService.update(any(), any(), any(), any(), any()))
+ .thenReturn(CompletableFuture.completedFuture(null));
+ var accessType = new AccessType();
+ var packageData = PackageData.builder().vendorId(19).packageId(3964).build();
+ var packageResult = new PackageResult(packageData);
+
+ var result = result(packageAccessTypeService.assignAccessType(accessType, packageResult, context));
+
+ assertSame(packageResult, result);
+ assertSame(accessType, result.getAccessType());
+ verify(accessTypeMappingsService).update(accessType, "19-3964", RecordType.PACKAGE, CREDENTIALS_ID,
+ context.getRequestContext().getHeaders());
+ }
+
+ @Test
+ void shouldUpdateRecordMappingForGivenRecordId() {
+ when(accessTypeMappingsService.update(any(), any(), any(), any(), any()))
+ .thenReturn(CompletableFuture.completedFuture(null));
+
+ result(packageAccessTypeService.updateRecordMapping(null, "19-3964", context));
+
+ verify(accessTypeMappingsService).update(null, "19-3964", RecordType.PACKAGE, CREDENTIALS_ID,
+ context.getRequestContext().getHeaders());
+ }
+}
diff --git a/src/test/java/org/folio/service/packages/PackageServiceTest.java b/src/test/java/org/folio/service/packages/PackageServiceTest.java
new file mode 100644
index 00000000..062444ff
--- /dev/null
+++ b/src/test/java/org/folio/service/packages/PackageServiceTest.java
@@ -0,0 +1,315 @@
+package org.folio.service.packages;
+
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static org.folio.util.TestUtil.result;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import org.folio.holdingsiq.model.Configuration;
+import org.folio.holdingsiq.model.PackageData;
+import org.folio.holdingsiq.model.PackageId;
+import org.folio.holdingsiq.model.PackagePost;
+import org.folio.holdingsiq.model.PackagePut;
+import org.folio.holdingsiq.model.Packages;
+import org.folio.holdingsiq.model.RequestContext;
+import org.folio.holdingsiq.model.Titles;
+import org.folio.holdingsiq.service.exception.ResourceNotFoundException;
+import org.folio.okapi.common.XOkapiHeaders;
+import org.folio.properties.common.SearchProperties;
+import org.folio.repository.RecordKey;
+import org.folio.repository.RecordType;
+import org.folio.rest.converter.packages.PackageRequestConvertionService;
+import org.folio.rest.exception.InputValidationException;
+import org.folio.rest.jaxrs.model.AccessType;
+import org.folio.rest.jaxrs.model.PackagePostData;
+import org.folio.rest.jaxrs.model.PackagePostDataAttributes;
+import org.folio.rest.jaxrs.model.PackagePostRequest;
+import org.folio.rest.jaxrs.model.PackagePutData;
+import org.folio.rest.jaxrs.model.PackagePutDataAttributes;
+import org.folio.rest.jaxrs.model.PackagePutRequest;
+import org.folio.rest.jaxrs.model.PackageTagsDataAttributes;
+import org.folio.rest.jaxrs.model.PackageTagsPutData;
+import org.folio.rest.jaxrs.model.PackageTagsPutRequest;
+import org.folio.rest.model.filter.PackageRecordFilter;
+import org.folio.rest.util.template.RmApiTemplateContext;
+import org.folio.rest.validator.packages.PackageValidationService;
+import org.folio.rmapi.PackageServiceImpl;
+import org.folio.rmapi.ProvidersServiceImpl;
+import org.folio.rmapi.TitlesServiceImpl;
+import org.folio.rmapi.result.PackageResult;
+import org.folio.rmapi.result.TitleCollectionResult;
+import org.folio.service.loader.RelatedEntitiesLoader;
+import org.folio.util.TestFutureFailedException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.core.convert.converter.Converter;
+
+@ExtendWith(MockitoExtension.class)
+class PackageServiceTest {
+
+ private static final String CREDENTIALS_ID = UUID.randomUUID().toString();
+ private static final String TENANT = "fs";
+
+ @Mock
+ private PackageRequestConvertionService requestConvertionService;
+ @Mock
+ private PackageValidationService validationService;
+ @Mock
+ private PackageAccessTypeService packageAccessTypeService;
+ @Mock
+ private PackageTagsService packageTagsService;
+ @Mock
+ private CustomProviderIdService customProviderIdService;
+ @Mock
+ private RelatedEntitiesLoader relatedEntitiesLoader;
+ @Mock
+ private Converter titleCollectionConverter;
+ @Mock
+ private PackageServiceImpl packagesRmApiService;
+ @Mock
+ private ProvidersServiceImpl providersRmApiService;
+ @Mock
+ private TitlesServiceImpl titlesRmApiService;
+
+ private PackageService packageService;
+ private RmApiTemplateContext context;
+
+ @BeforeEach
+ void setUp() {
+ var searchProperties = new SearchProperties("any", "any", "b");
+ packageService = new PackageService(requestConvertionService, validationService, packageAccessTypeService,
+ packageTagsService, customProviderIdService, relatedEntitiesLoader, titleCollectionConverter, searchProperties);
+
+ var requestContext = new RequestContext(Map.of(
+ XOkapiHeaders.TENANT, TENANT,
+ XOkapiHeaders.URL, "http://localhost:8080"));
+ context = RmApiTemplateContext.builder()
+ .requestContext(requestContext)
+ .configuration(Configuration.builder().build())
+ .credentialsId(CREDENTIALS_ID)
+ .packagesService(packagesRmApiService)
+ .providersService(providersRmApiService)
+ .titlesService(titlesRmApiService)
+ .build();
+ }
+
+ @Test
+ void shouldRetrieveGlobalPackagesWhenProviderIdIsNull() {
+ var packages = Packages.builder().totalResults(0).build();
+ when(packagesRmApiService.retrievePackages(any(), any())).thenReturn(completedFuture(packages));
+ var filter = PackageRecordFilter.builder().filterTags(List.of("tag")).sort("relevance").page(1).count(10).build();
+
+ var result = result(packageService.retrievePackages(null, filter, context));
+
+ assertSame(packages, result);
+ verify(packagesRmApiService).retrievePackages(any(), any());
+ verify(packagesRmApiService, never()).retrievePackages(any(Integer.class), any(), any());
+ }
+
+ @Test
+ void shouldRetrieveProviderScopedPackagesWhenProviderIdIsGiven() {
+ var packages = Packages.builder().totalResults(0).build();
+ when(packagesRmApiService.retrievePackages(eq(42), any(), any())).thenReturn(completedFuture(packages));
+ var filter = PackageRecordFilter.builder().filterTags(List.of("tag")).sort("relevance").page(1).count(10).build();
+
+ var result = result(packageService.retrievePackages(42, filter, context));
+
+ assertSame(packages, result);
+ verify(packagesRmApiService).retrievePackages(eq(42), any(), any());
+ }
+
+ @Test
+ void shouldResolveCustomProviderIdBeforeRetrievingPackages() {
+ var packages = Packages.builder().totalResults(0).build();
+ when(customProviderIdService.getCustomProviderId(context)).thenReturn(completedFuture(111));
+ when(packagesRmApiService.retrievePackages(eq(111), any(), any())).thenReturn(completedFuture(packages));
+ var filter = PackageRecordFilter.builder().filterTags(List.of("tag")).sort("relevance").page(1).count(10).build();
+
+ var result = result(packageService.getCustomProviderIdAndRetrievePackages(filter, context));
+
+ assertSame(packages, result);
+ }
+
+ @Test
+ void shouldCreateCustomPackageWithoutAccessType() {
+ var entity = buildPostRequest(null);
+ var packagePost = mock(PackagePost.class);
+ var postedPackage = PackageData.builder().vendorId(19).packageId(3964).build();
+ when(requestConvertionService.convertCustomPackagePostRequest(entity)).thenReturn(packagePost);
+ when(customProviderIdService.getCustomProviderId(context)).thenReturn(completedFuture(19));
+ when(packagesRmApiService.postPackage(packagePost, 19)).thenReturn(completedFuture(postedPackage));
+
+ var result = result(packageService.createCustomPackage(entity, context));
+
+ verify(validationService).validateCustomPackagePostRequest(entity);
+ assertSame(postedPackage, result.getPackageData());
+ verify(packageAccessTypeService, never()).fetchAccessType(any(), any());
+ verify(packageAccessTypeService, never()).assignAccessType(any(), any(), any());
+ }
+
+ @Test
+ void shouldCreateCustomPackageAndAssignAccessTypeWhenAccessTypeIdIsGiven() {
+ var entity = buildPostRequest("access-type-id");
+ var packagePost = mock(PackagePost.class);
+ var postedPackage = PackageData.builder().vendorId(19).packageId(3964).build();
+ var accessType = new AccessType();
+ when(requestConvertionService.convertCustomPackagePostRequest(entity)).thenReturn(packagePost);
+ when(customProviderIdService.getCustomProviderId(context)).thenReturn(completedFuture(19));
+ when(packagesRmApiService.postPackage(packagePost, 19)).thenReturn(completedFuture(postedPackage));
+ when(packageAccessTypeService.fetchAccessType("access-type-id", context)).thenReturn(completedFuture(accessType));
+ when(packageAccessTypeService.assignAccessType(eq(accessType), any(), eq(context)))
+ .thenAnswer(invocation -> completedFuture(invocation.getArgument(1)));
+
+ var result = result(packageService.createCustomPackage(entity, context));
+
+ assertSame(postedPackage, result.getPackageData());
+ verify(packageAccessTypeService).assignAccessType(eq(accessType), any(), eq(context));
+ }
+
+ @Test
+ void shouldRetrievePackageWithRelatedAccessTypeAndTags() {
+ var packageId = new PackageId(19, 3964);
+ var packageResult = new PackageResult(PackageData.builder().vendorId(19).packageId(3964).build());
+ when(packagesRmApiService.retrievePackage(packageId, List.of("provider")))
+ .thenReturn(completedFuture(packageResult));
+ when(relatedEntitiesLoader.loadAccessType(eq(packageResult), any(), eq(context)))
+ .thenReturn(completedFuture(null));
+ when(relatedEntitiesLoader.loadTags(eq(packageResult), any(), eq(context))).thenReturn(completedFuture(null));
+
+ var result = result(packageService.retrievePackageWithRelatedData(packageId, List.of("provider"), context));
+
+ assertSame(packageResult, result);
+ var expectedRecordKey = RecordKey.builder().recordId("19-3964").recordType(RecordType.PACKAGE).build();
+ verify(relatedEntitiesLoader).loadAccessType(packageResult, expectedRecordKey, context);
+ verify(relatedEntitiesLoader).loadTags(packageResult, expectedRecordKey, context);
+ }
+
+ @Test
+ void shouldThrowWhenIsCustomDoesNotMatchOriginalPackage() {
+ var packageId = new PackageId(19, 3964);
+ var originalPackage = PackageData.builder().vendorId(19).packageId(3964).isCustom(true).build();
+ var entity = buildPutRequest(false, null);
+ when(packagesRmApiService.retrievePackage(3964)).thenReturn(completedFuture(originalPackage));
+
+ var packageUpdateFuture = packageService.updatePackage(packageId, entity, context);
+ var exception = assertThrows(TestFutureFailedException.class, () -> result(packageUpdateFuture));
+
+ assertEquals(InputValidationException.class, exception.getCause().getClass());
+ verify(packagesRmApiService, never()).updatePackage(any(Integer.class), any());
+ }
+
+ @Test
+ void shouldUpdateCustomPackageAndAssignAccessType() {
+ var packageId = new PackageId(19, 3964);
+ var originalPackage = PackageData.builder().vendorId(19).packageId(3964).isCustom(true).build();
+ var entity = buildPutRequest(true, "access-type-id");
+ var packagePut = mock(PackagePut.class);
+ var accessType = new AccessType();
+ when(packagesRmApiService.retrievePackage(3964)).thenReturn(completedFuture(originalPackage));
+ when(packageAccessTypeService.fetchAccessType("access-type-id", context)).thenReturn(completedFuture(accessType));
+ when(requestConvertionService.convertCustomPackagePutRequest(entity)).thenReturn(packagePut);
+ when(packagesRmApiService.updatePackage(3964, packagePut)).thenReturn(completedFuture(null));
+ when(packageAccessTypeService.assignAccessType(eq(accessType), any(), eq(context)))
+ .thenAnswer(invocation -> completedFuture(invocation.getArgument(1)));
+
+ var result = result(packageService.updatePackage(packageId, entity, context));
+
+ assertSame(originalPackage, result.getPackageData());
+ verify(validationService).validateCustomPackagePutRequest(entity);
+ verify(packageAccessTypeService).assignAccessType(eq(accessType), any(), eq(context));
+ }
+
+ @Test
+ void shouldCleanUpLocalDataWhenPackageWasDeletedDuringUpdate() {
+ var packageId = new PackageId(19, 3964);
+ var originalPackage = PackageData.builder().vendorId(19).packageId(3964).isCustom(true).build();
+ var entity = buildPutRequest(true, null);
+ var packagePut = mock(PackagePut.class);
+ var notFound = new ResourceNotFoundException("not found", 404, "Not Found", "", "query");
+ var failedRetrieve = CompletableFuture.failedFuture(notFound);
+ when(packagesRmApiService.retrievePackage(3964))
+ .thenReturn(completedFuture(originalPackage))
+ .thenReturn(failedRetrieve);
+ when(requestConvertionService.convertCustomPackagePutRequest(entity)).thenReturn(packagePut);
+ when(packagesRmApiService.updatePackage(3964, packagePut)).thenReturn(completedFuture(null));
+ when(packageAccessTypeService.updateRecordMapping(null, "19-3964", context)).thenReturn(completedFuture(null));
+ when(packageTagsService.deletePackageTags(eq(packageId), any(), eq(TENANT))).thenReturn(completedFuture(null));
+
+ var packageUpdateFuture = packageService.updatePackage(packageId, entity, context);
+ assertThrows(TestFutureFailedException.class, () -> result(packageUpdateFuture));
+
+ verify(packageAccessTypeService).updateRecordMapping(null, "19-3964", context);
+ verify(packageTagsService).deletePackageTags(eq(packageId), any(), eq(TENANT));
+ }
+
+ @Test
+ void shouldThrowWhenDeletingNonCustomPackage() {
+ var packageId = new PackageId(19, 3964);
+ var packageData = PackageData.builder().vendorId(19).packageId(3964).isCustom(false).build();
+ when(packagesRmApiService.retrievePackage(3964)).thenReturn(completedFuture(packageData));
+
+ var packageDeletionFuture = packageService.deletePackage(packageId, context);
+ var exception = assertThrows(TestFutureFailedException.class, () -> result(packageDeletionFuture));
+
+ assertEquals(InputValidationException.class, exception.getCause().getClass());
+ verify(packagesRmApiService, never()).deletePackage(any(Integer.class));
+ }
+
+ @Test
+ void shouldDeleteCustomPackageAndAssignedResources() {
+ var packageId = new PackageId(19, 3964);
+ var packageData = PackageData.builder().vendorId(19).packageId(3964).isCustom(true).build();
+ when(packagesRmApiService.retrievePackage(3964)).thenReturn(completedFuture(packageData));
+ when(packagesRmApiService.deletePackage(3964)).thenReturn(completedFuture(null));
+ when(packageAccessTypeService.updateRecordMapping(null, "19-3964", context)).thenReturn(completedFuture(null));
+ when(packageTagsService.deletePackageTags(eq(packageId), any(), eq(TENANT))).thenReturn(completedFuture(null));
+
+ result(packageService.deletePackage(packageId, context));
+
+ verify(packagesRmApiService).deletePackage(3964);
+ verify(packageAccessTypeService).updateRecordMapping(null, "19-3964", context);
+ verify(packageTagsService).deletePackageTags(eq(packageId), any(), eq(TENANT));
+ }
+
+ @Test
+ void shouldValidateAndDelegateTagsUpdateToPackageTagsService() {
+ var credentialsId = UUID.randomUUID();
+ var attributes = new PackageTagsDataAttributes().withName("name");
+ var entity = new PackageTagsPutRequest().withData(new PackageTagsPutData().withAttributes(attributes));
+ when(packageTagsService.updatePackageTags("19-3964", credentialsId, attributes, TENANT))
+ .thenReturn(completedFuture(null));
+
+ var result = result(packageService.updateTagsForPackage(entity, credentialsId, "19-3964", TENANT));
+
+ assertSame(attributes, result);
+ verify(validationService).validatePackageTagsPutRequest(entity);
+ }
+
+ private PackagePostRequest buildPostRequest(String accessTypeId) {
+ return new PackagePostRequest()
+ .withData(new PackagePostData()
+ .withAttributes(new PackagePostDataAttributes().withAccessTypeId(accessTypeId)));
+ }
+
+ private PackagePutRequest buildPutRequest(boolean isCustom, String accessTypeId) {
+ return new PackagePutRequest()
+ .withData(new PackagePutData()
+ .withAttributes(new PackagePutDataAttributes()
+ .withIsCustom(isCustom)
+ .withAccessTypeId(accessTypeId)));
+ }
+}
diff --git a/src/test/java/org/folio/service/packages/PackageTagsServiceTest.java b/src/test/java/org/folio/service/packages/PackageTagsServiceTest.java
new file mode 100644
index 00000000..efa9ffd2
--- /dev/null
+++ b/src/test/java/org/folio/service/packages/PackageTagsServiceTest.java
@@ -0,0 +1,121 @@
+package org.folio.service.packages;
+
+import static org.folio.util.TestUtil.result;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import org.folio.holdingsiq.model.PackageId;
+import org.folio.repository.RecordType;
+import org.folio.repository.packages.DbPackage;
+import org.folio.repository.packages.PackageRepository;
+import org.folio.repository.tag.DbTag;
+import org.folio.repository.tag.TagRepository;
+import org.folio.rest.jaxrs.model.ContentType;
+import org.folio.rest.jaxrs.model.PackageTagsDataAttributes;
+import org.folio.rest.jaxrs.model.Tags;
+import org.folio.rmapi.result.TitleResult;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class PackageTagsServiceTest {
+
+ private static final String TENANT = "fs";
+ private static final String PACKAGE_ID = "19-3964";
+ private static final UUID CREDENTIALS_ID = UUID.randomUUID();
+
+ @Mock
+ private PackageRepository packageRepository;
+ @Mock
+ private TagRepository tagRepository;
+ @InjectMocks
+ private PackageTagsService packageTagsService;
+
+ @Test
+ void shouldDoNothingWhenTagsAreNull() {
+ var attributes = new PackageTagsDataAttributes().withName("name").withContentType(ContentType.UNKNOWN);
+
+ var result = result(packageTagsService.updatePackageTags(PACKAGE_ID, CREDENTIALS_ID, attributes, TENANT));
+
+ assertNull(result);
+ verifyNoInteractions(packageRepository);
+ verifyNoInteractions(tagRepository);
+ }
+
+ @Test
+ void shouldSavePackageAndUpdateTagsWhenTagListIsNotEmpty() {
+ when(packageRepository.save(any(), eq(TENANT))).thenReturn(CompletableFuture.completedFuture(null));
+ when(tagRepository.updateRecordTags(eq(TENANT), eq(PACKAGE_ID), eq(RecordType.PACKAGE), any()))
+ .thenReturn(CompletableFuture.completedFuture(true));
+ var attributes = new PackageTagsDataAttributes()
+ .withName("name")
+ .withContentType(ContentType.UNKNOWN)
+ .withTags(new Tags().withTagList(List.of("tag1", "tag2")));
+
+ var result = result(packageTagsService.updatePackageTags(PACKAGE_ID, CREDENTIALS_ID, attributes, TENANT));
+
+ assertNull(result);
+ verify(packageRepository).save(any(DbPackage.class), eq(TENANT));
+ verify(packageRepository, never()).delete(any(), any(), any());
+ verify(tagRepository).updateRecordTags(TENANT, PACKAGE_ID, RecordType.PACKAGE, List.of("tag1", "tag2"));
+ }
+
+ @Test
+ void shouldDeleteStoredPackageAndUpdateTagsWhenTagListIsEmpty() {
+ when(packageRepository.delete(any(), any(), eq(TENANT))).thenReturn(CompletableFuture.completedFuture(null));
+ when(tagRepository.updateRecordTags(eq(TENANT), eq(PACKAGE_ID), eq(RecordType.PACKAGE), any()))
+ .thenReturn(CompletableFuture.completedFuture(true));
+ var attributes = new PackageTagsDataAttributes()
+ .withName("name")
+ .withContentType(ContentType.UNKNOWN)
+ .withTags(new Tags().withTagList(List.of()));
+
+ var result = result(packageTagsService.updatePackageTags(PACKAGE_ID, CREDENTIALS_ID, attributes, TENANT));
+
+ assertNull(result);
+ verify(packageRepository).delete(new PackageId(19, 3964), CREDENTIALS_ID, TENANT);
+ verify(packageRepository, never()).save(any(), any());
+ verify(tagRepository).updateRecordTags(TENANT, PACKAGE_ID, RecordType.PACKAGE, List.of());
+ }
+
+ @Test
+ void shouldDeletePackageAndItsTags() {
+ var packageId = new PackageId(19, 3964);
+ when(packageRepository.delete(packageId, CREDENTIALS_ID, TENANT))
+ .thenReturn(CompletableFuture.completedFuture(null));
+ when(tagRepository.deleteRecordTags(TENANT, PACKAGE_ID, RecordType.PACKAGE))
+ .thenReturn(CompletableFuture.completedFuture(true));
+
+ var result = result(packageTagsService.deletePackageTags(packageId, CREDENTIALS_ID, TENANT));
+
+ assertNull(result);
+ verify(packageRepository).delete(packageId, CREDENTIALS_ID, TENANT);
+ verify(tagRepository).deleteRecordTags(TENANT, PACKAGE_ID, RecordType.PACKAGE);
+ }
+
+ @Test
+ void shouldPopulateResourceTagsFromTagRepository() {
+ var titleResult = new TitleResult(null, false);
+ var resourceIdToTitle = Map.of("resource-1", titleResult);
+ var tags = List.of(DbTag.builder().value("tag1").recordType(RecordType.RESOURCE).build());
+ when(tagRepository.findPerRecord(TENANT, List.of("resource-1"), RecordType.RESOURCE))
+ .thenReturn(CompletableFuture.completedFuture(Map.of("resource-1", tags)));
+
+ result(packageTagsService.loadResourceTags(TENANT, resourceIdToTitle));
+
+ assertEquals(tags, titleResult.getResourceTagList());
+ }
+}
diff --git a/src/test/java/org/folio/service/sanitizer/HtmlSanitizerTest.java b/src/test/java/org/folio/service/sanitizer/HtmlSanitizerTest.java
new file mode 100644
index 00000000..931868ef
--- /dev/null
+++ b/src/test/java/org/folio/service/sanitizer/HtmlSanitizerTest.java
@@ -0,0 +1,65 @@
+package org.folio.service.sanitizer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class HtmlSanitizerTest {
+
+ private final HtmlSanitizer sanitizer = new HtmlSanitizer();
+
+ @Test
+ void shouldReturnNullWhenTargetIsNull() {
+ assertNull(sanitizer.sanitize(null));
+ }
+
+ @Test
+ void shouldReturnBlankValueUnchanged() {
+ assertEquals(" ", sanitizer.sanitize(" "));
+ }
+
+ @ParameterizedTest
+ @MethodSource("testCasesWithoutChanges")
+ void shouldKeepContentWithoutChanges(String desc, String content) {
+ assertEquals(content, sanitizer.sanitize(content), desc);
+ }
+
+ @Test
+ void shouldStripDisallowedTagButKeepItsText() {
+ assertEquals("some text", sanitizer.sanitize("some text
"));
+ }
+
+ @Test
+ void shouldStripScriptTagAndItsContent() {
+ assertEquals("safe
", sanitizer.sanitize("safe
"));
+ }
+
+ @Test
+ void shouldStripDisallowedAttribute() {
+ assertEquals("text
", sanitizer.sanitize("text
"));
+ }
+
+ @Test
+ void shouldStripHrefAttributeOnNonAnchorTag() {
+ assertEquals("text
", sanitizer.sanitize("text
"));
+ }
+
+ private static Stream testCasesWithoutChanges() {
+ return Stream.of(
+ Arguments.of("should keep not html content as is", "some custom description"),
+ Arguments.of("should keep class and style attributes on allowed tag",
+ "text
"),
+ Arguments.of("should keep href rel and target attributes on anchor",
+ "link"),
+ Arguments.of("should not pretty print output", ""),
+ Arguments.of("should keep allowed tags",
+ "paragraph
bolditalicunderline"
+ + "- one
h1
h2
h3
line
break")
+ );
+ }
+}