diff --git a/src/main/java/org/folio/scheduler/domain/entity/TimerDescriptorEntity.java b/src/main/java/org/folio/scheduler/domain/entity/TimerDescriptorEntity.java index b67ea03..ae637c5 100644 --- a/src/main/java/org/folio/scheduler/domain/entity/TimerDescriptorEntity.java +++ b/src/main/java/org/folio/scheduler/domain/entity/TimerDescriptorEntity.java @@ -1,15 +1,23 @@ package org.folio.scheduler.domain.entity; +import static org.apache.commons.lang3.StringUtils.isEmpty; + import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.Id; import jakarta.persistence.Table; import java.util.UUID; import lombok.Data; import lombok.NoArgsConstructor; +import org.folio.scheduler.domain.dto.RoutingEntry; import org.folio.scheduler.domain.dto.TimerDescriptor; +import org.folio.scheduler.domain.model.TimerType; +import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.annotations.Type; +import org.hibernate.type.SqlTypes; @Data @Entity @@ -19,8 +27,15 @@ public class TimerDescriptorEntity { @Id private UUID id; + @Enumerated(EnumType.STRING) + @JdbcTypeCode(SqlTypes.NAMED_ENUM) + @Column(name = "type", columnDefinition = "timer_type") + private TimerType type; + private String moduleName; + private String moduleId; + private String naturalKey; @Type(JsonBinaryType.class) @@ -32,24 +47,18 @@ public void setTimerDescriptor(TimerDescriptor timerDescriptor) { this.naturalKey = toNaturalKey(timerDescriptor); } - public static TimerDescriptorEntity of(TimerDescriptor timerDescriptor) { - var entity = new TimerDescriptorEntity(); - entity.id = timerDescriptor.getId(); - entity.timerDescriptor = timerDescriptor; - entity.naturalKey = toNaturalKey(timerDescriptor); - return entity; - } - - public static String toNaturalKey(TimerDescriptor timerDescriptor) { - if (timerDescriptor.getRoutingEntry() == null) { + public static String toNaturalKey(TimerDescriptor td) { + RoutingEntry re = td.getRoutingEntry(); + if (re == null) { return null; } - var methods = timerDescriptor.getRoutingEntry().getMethods() != null ? String.join(",", - timerDescriptor.getRoutingEntry().getMethods()) : ""; - var path = timerDescriptor.getRoutingEntry().getPath() != null ? timerDescriptor.getRoutingEntry().getPath() - : timerDescriptor.getRoutingEntry().getPathPattern(); - return String.format("%s#%s#%s", timerDescriptor.getModuleName() != null ? timerDescriptor.getModuleName() : "", - methods, path); + if (isEmpty(td.getModuleName())) { + throw new IllegalArgumentException("Module name is required"); + } + + var methods = re.getMethods() != null ? String.join(",", re.getMethods()) : ""; + var path = re.getPath() != null ? re.getPath() : re.getPathPattern(); + return String.format("%s#%s#%s#%s", td.getType(), td.getModuleName(), methods, path); } } diff --git a/src/main/java/org/folio/scheduler/domain/model/TimerType.java b/src/main/java/org/folio/scheduler/domain/model/TimerType.java new file mode 100644 index 0000000..4281edc --- /dev/null +++ b/src/main/java/org/folio/scheduler/domain/model/TimerType.java @@ -0,0 +1,6 @@ +package org.folio.scheduler.domain.model; + +public enum TimerType { + USER, + SYSTEM +} diff --git a/src/main/java/org/folio/scheduler/integration/OkapiClient.java b/src/main/java/org/folio/scheduler/integration/OkapiClient.java index 22303ab..ce186f0 100644 --- a/src/main/java/org/folio/scheduler/integration/OkapiClient.java +++ b/src/main/java/org/folio/scheduler/integration/OkapiClient.java @@ -8,17 +8,20 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestHeader; @FeignClient(name = "okapi") public interface OkapiClient { + String MODULE_HINT = "x-okapi-module-hint"; + /** * Performs GET HTTP Request. * * @param uri - uniform resource identifier */ @GetMapping(consumes = APPLICATION_JSON_VALUE) - void doGet(URI uri); + void doGet(URI uri, @RequestHeader(MODULE_HINT) String moduleHint); /** * Performs POST HTTP Request. @@ -26,7 +29,7 @@ public interface OkapiClient { * @param uri - uniform resource identifier */ @PostMapping(consumes = APPLICATION_JSON_VALUE) - void doPost(URI uri); + void doPost(URI uri, @RequestHeader(MODULE_HINT) String moduleHint); /** * Performs PUT HTTP Request. @@ -34,7 +37,7 @@ public interface OkapiClient { * @param uri - uniform resource identifier */ @PutMapping(consumes = APPLICATION_JSON_VALUE) - void doPut(URI uri); + void doPut(URI uri, @RequestHeader(MODULE_HINT) String moduleHint); /** * Performs DELETE HTTP Request. @@ -42,5 +45,5 @@ public interface OkapiClient { * @param uri - uniform resource identifier */ @DeleteMapping(consumes = APPLICATION_JSON_VALUE) - void doDelete(URI uri); + void doDelete(URI uri, @RequestHeader(MODULE_HINT) String moduleHint); } diff --git a/src/main/java/org/folio/scheduler/integration/kafka/KafkaMessageListener.java b/src/main/java/org/folio/scheduler/integration/kafka/KafkaMessageListener.java index 4af4b83..c9d43c2 100644 --- a/src/main/java/org/folio/scheduler/integration/kafka/KafkaMessageListener.java +++ b/src/main/java/org/folio/scheduler/integration/kafka/KafkaMessageListener.java @@ -4,6 +4,8 @@ import static java.util.Collections.singletonList; import static org.apache.commons.collections4.CollectionUtils.isEmpty; import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; +import static org.folio.common.utils.CollectionUtils.mapItems; +import static org.folio.scheduler.domain.model.TimerType.SYSTEM; import static org.folio.scheduler.utils.OkapiRequestUtils.getStaticPath; import static org.folio.spring.integration.XOkapiHeaders.TENANT; import static org.folio.spring.integration.XOkapiHeaders.USER_ID; @@ -18,6 +20,7 @@ import org.folio.common.utils.SemverUtils; import org.folio.scheduler.domain.dto.RoutingEntry; import org.folio.scheduler.domain.dto.TimerDescriptor; +import org.folio.scheduler.domain.dto.TimerType; import org.folio.scheduler.integration.kafka.model.ResourceEvent; import org.folio.scheduler.integration.keycloak.SystemUserService; import org.folio.scheduler.service.SchedulerTimerService; @@ -60,12 +63,13 @@ public void handleScheduledJobEvent(ConsumerRecord consum private void createTimers(ResourceEvent event) { try (var ignored = new FolioExecutionContextSetter(folioModuleMetadata, prepareContextHeaders(event.getTenant()))) { - var moduleName = SemverUtils.getName(event.getNewValue().getModuleId()); + var moduleId = event.getNewValue().getModuleId(); + var moduleName = SemverUtils.getName(moduleId); var timers = event.getNewValue().getTimers(); + logCreatingTimers(timers); for (var routingEntry : timers) { - var timerDescriptor = new TimerDescriptor().enabled(TRUE).moduleName(moduleName).routingEntry(routingEntry); - schedulerTimerService.create(timerDescriptor); + schedulerTimerService.create(createTimerDescriptor(routingEntry, moduleName, moduleId)); } } } @@ -73,8 +77,10 @@ private void createTimers(ResourceEvent event) { private void updateTimers(ResourceEvent resourceEvent) { try (var ignored = new FolioExecutionContextSetter(folioModuleMetadata, prepareContextHeaders(resourceEvent.getTenant()))) { - var moduleName = SemverUtils.getName(resourceEvent.getNewValue().getModuleId()); - var timers = schedulerTimerService.findByModuleName(moduleName); + var moduleId = resourceEvent.getNewValue().getModuleId(); + var moduleName = SemverUtils.getName(moduleId); + + var timers = schedulerTimerService.findByModuleNameAndType(moduleName, SYSTEM); if (isNotEmpty(timers)) { logDeletingTimers(timers); for (var timer : timers) { @@ -85,8 +91,7 @@ private void updateTimers(ResourceEvent resourceEvent) { var timerToCreate = resourceEvent.getNewValue(); logCreatingTimers(timerToCreate.getTimers()); for (var routingEntry : timerToCreate.getTimers()) { - var timerDescriptor = new TimerDescriptor().enabled(TRUE).moduleName(moduleName).routingEntry(routingEntry); - schedulerTimerService.create(timerDescriptor); + schedulerTimerService.create(createTimerDescriptor(routingEntry, moduleName, moduleId)); } } } @@ -95,7 +100,7 @@ private void deleteTimers(ResourceEvent resourceEvent) { try (var ignored = new FolioExecutionContextSetter(folioModuleMetadata, prepareContextHeaders(resourceEvent.getTenant()))) { var moduleName = SemverUtils.getName(resourceEvent.getOldValue().getModuleId()); - var timers = schedulerTimerService.findByModuleName(moduleName); + var timers = schedulerTimerService.findByModuleNameAndType(moduleName, SYSTEM); if (isEmpty(timers)) { return; } @@ -114,18 +119,26 @@ private Map> prepareContextHeaders(String tenant) { return headers; } + private static TimerDescriptor createTimerDescriptor(RoutingEntry routingEntry, String moduleName, String moduleId) { + return new TimerDescriptor().enabled(TRUE) + .type(TimerType.SYSTEM) + .moduleName(moduleName) + .moduleId(moduleId) + .routingEntry(routingEntry); + } + private static String getRoutingEntryKey(RoutingEntry routingEntry) { var methods = String.join("|", routingEntry.getMethods()); return methods + " " + getStaticPath(routingEntry); } private static void logCreatingTimers(List entries) { - var methods = entries.stream().map(KafkaMessageListener::getRoutingEntryKey).toList(); - log.info("Processing scheduled job event from kafka: timers = {}", methods); + log.info("Processing scheduled job event from kafka: timers = {}", + () -> mapItems(entries, KafkaMessageListener::getRoutingEntryKey)); } private static void logDeletingTimers(List timers) { - var methods = timers.stream().map(t -> getRoutingEntryKey(t.getRoutingEntry())).toList(); - log.info("Deleting timers: timers = {}", methods); + log.info("Deleting timers: timers = {}", + () -> mapItems(timers, t -> getRoutingEntryKey(t.getRoutingEntry()))); } } diff --git a/src/main/java/org/folio/scheduler/mapper/TimerDescriptorMapper.java b/src/main/java/org/folio/scheduler/mapper/TimerDescriptorMapper.java index 3100025..fc8368f 100644 --- a/src/main/java/org/folio/scheduler/mapper/TimerDescriptorMapper.java +++ b/src/main/java/org/folio/scheduler/mapper/TimerDescriptorMapper.java @@ -8,20 +8,13 @@ @Mapper(componentModel = "spring") public interface TimerDescriptorMapper { - /** - * Converts {@link TimerDescriptorEntity} to {@link TimerDescriptor} object. - * - * @param entity - {@link TimerDescriptorEntity} object - * @return converted {@link TimerDescriptor} object - */ - TimerDescriptor convert(TimerDescriptorEntity entity); - /** * Converts {@link TimerDescriptor} to {@link TimerDescriptorEntity} object. * * @param descriptor - {@link TimerDescriptor} object * @return converted {@link TimerDescriptorEntity} object */ + @Mapping(target = "naturalKey", ignore = true) @Mapping(target = "timerDescriptor", source = "descriptor") TimerDescriptorEntity convert(TimerDescriptor descriptor); } diff --git a/src/main/java/org/folio/scheduler/repository/SchedulerTimerRepository.java b/src/main/java/org/folio/scheduler/repository/SchedulerTimerRepository.java index 2d58ed7..81b4db2 100644 --- a/src/main/java/org/folio/scheduler/repository/SchedulerTimerRepository.java +++ b/src/main/java/org/folio/scheduler/repository/SchedulerTimerRepository.java @@ -4,13 +4,14 @@ import java.util.Optional; import java.util.UUID; import org.folio.scheduler.domain.entity.TimerDescriptorEntity; +import org.folio.scheduler.domain.model.TimerType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface SchedulerTimerRepository extends JpaRepository { - List findByModuleName(String moduleName); + List findByModuleNameAndType(String moduleName, TimerType type); Optional findByNaturalKey(String naturalKey); } diff --git a/src/main/java/org/folio/scheduler/service/SchedulerTimerService.java b/src/main/java/org/folio/scheduler/service/SchedulerTimerService.java index 699d6d0..a5d3ea9 100644 --- a/src/main/java/org/folio/scheduler/service/SchedulerTimerService.java +++ b/src/main/java/org/folio/scheduler/service/SchedulerTimerService.java @@ -1,6 +1,9 @@ package org.folio.scheduler.service; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; +import static org.apache.commons.lang3.StringUtils.isEmpty; +import static org.folio.common.utils.CollectionUtils.mapItems; +import static org.folio.scheduler.utils.TimerDescriptorUtils.evalModuleName; import jakarta.persistence.EntityExistsException; import jakarta.persistence.EntityNotFoundException; @@ -13,6 +16,7 @@ import org.folio.scheduler.domain.dto.TimerDescriptor; import org.folio.scheduler.domain.entity.TimerDescriptorEntity; import org.folio.scheduler.domain.model.SearchResult; +import org.folio.scheduler.domain.model.TimerType; import org.folio.scheduler.exception.RequestValidationException; import org.folio.scheduler.mapper.TimerDescriptorMapper; import org.folio.scheduler.repository.SchedulerTimerRepository; @@ -41,9 +45,9 @@ public Optional findById(UUID uuid) { } @Transactional(readOnly = true) - public List findByModuleName(String moduleName) { - return schedulerTimerRepository.findByModuleName(moduleName).stream().map(TimerDescriptorEntity::getTimerDescriptor) - .toList(); + public List findByModuleNameAndType(String moduleName, TimerType type) { + return mapItems(schedulerTimerRepository.findByModuleNameAndType(moduleName, type), + TimerDescriptorEntity::getTimerDescriptor); } /** @@ -89,16 +93,15 @@ public TimerDescriptor create(TimerDescriptor timerDescriptor) { validate(timerDescriptor); timerDescriptor.setId(defaultIfNull(id, UUID.randomUUID())); + timerDescriptor.setModuleName(evalModuleName(timerDescriptor)); + var naturalKey = TimerDescriptorEntity.toNaturalKey(timerDescriptor); - return schedulerTimerRepository.findByNaturalKey(naturalKey).map(existingTimer -> { - timerDescriptor.setId(existingTimer.getId()); - return doUpdate(timerDescriptor); - }).orElseGet(() -> { - var entity = timerDescriptorMapper.convert(timerDescriptor); - var savedEntity = schedulerTimerRepository.save(entity); - jobSchedulingService.schedule(timerDescriptor); - return savedEntity.getTimerDescriptor(); - }); + return schedulerTimerRepository.findByNaturalKey(naturalKey) + .map(existingTimer -> { + timerDescriptor.setId(existingTimer.getId()); + return doUpdate(timerDescriptor); + }) + .orElseGet(() -> doCreate(timerDescriptor)); } /** @@ -120,25 +123,11 @@ public TimerDescriptor update(UUID uuid, TimerDescriptor newDescriptor) { } validate(newDescriptor); + newDescriptor.setModuleName(evalModuleName(newDescriptor)); return doUpdate(newDescriptor); } - protected TimerDescriptor doUpdate(TimerDescriptor newDescriptor) { - var oldTimerDescriptor = - schedulerTimerRepository.findById(newDescriptor.getId()).map(TimerDescriptorEntity::getTimerDescriptor) - .orElseThrow( - () -> new EntityNotFoundException("Unable to find timer descriptor with id " + newDescriptor.getId())); - - newDescriptor.modified(true); - var convertedValue = timerDescriptorMapper.convert(newDescriptor); - var updatedEntity = schedulerTimerRepository.save(convertedValue); - var timerDescriptor = updatedEntity.getTimerDescriptor(); - jobSchedulingService.reschedule(oldTimerDescriptor, timerDescriptor); - - return timerDescriptor; - } - /** * Deletes timer descriptor by id. * @@ -164,10 +153,40 @@ public void deleteAll() { } } - protected void validate(TimerDescriptor timerDescriptor) { + private void validate(TimerDescriptor timerDescriptor) { if (timerDescriptor.getRoutingEntry().getMethods() != null && timerDescriptor.getRoutingEntry().getMethods().size() > 1) { throw new IllegalArgumentException("Only 1 method is allowed per timer"); } + + if (isEmpty(timerDescriptor.getModuleId()) && isEmpty(timerDescriptor.getModuleName())) { + throw new IllegalArgumentException("Module id or module name is required"); + } + + if (timerDescriptor.getType() == null) { + throw new IllegalArgumentException("Timer type is required"); + } + } + + private TimerDescriptor doCreate(TimerDescriptor timerDescriptor) { + var entity = timerDescriptorMapper.convert(timerDescriptor); + var savedEntity = schedulerTimerRepository.save(entity); + jobSchedulingService.schedule(timerDescriptor); + return savedEntity.getTimerDescriptor(); + } + + private TimerDescriptor doUpdate(TimerDescriptor newDescriptor) { + var oldTimerDescriptor = + schedulerTimerRepository.findById(newDescriptor.getId()).map(TimerDescriptorEntity::getTimerDescriptor) + .orElseThrow( + () -> new EntityNotFoundException("Unable to find timer descriptor with id " + newDescriptor.getId())); + + newDescriptor.modified(true); + var convertedValue = timerDescriptorMapper.convert(newDescriptor); + var updatedEntity = schedulerTimerRepository.save(convertedValue); + var timerDescriptor = updatedEntity.getTimerDescriptor(); + jobSchedulingService.reschedule(oldTimerDescriptor, timerDescriptor); + + return timerDescriptor; } } diff --git a/src/main/java/org/folio/scheduler/service/jobs/OkapiHttpRequestExecutor.java b/src/main/java/org/folio/scheduler/service/jobs/OkapiHttpRequestExecutor.java index e22323f..4e10df5 100644 --- a/src/main/java/org/folio/scheduler/service/jobs/OkapiHttpRequestExecutor.java +++ b/src/main/java/org/folio/scheduler/service/jobs/OkapiHttpRequestExecutor.java @@ -8,7 +8,6 @@ import static java.util.Map.entry; import static java.util.concurrent.ThreadLocalRandom.current; import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; -import static org.folio.spring.integration.XOkapiHeaders.MODULE_ID; import static org.folio.spring.integration.XOkapiHeaders.REQUEST_ID; import static org.folio.spring.integration.XOkapiHeaders.TENANT; import static org.folio.spring.integration.XOkapiHeaders.TOKEN; @@ -23,12 +22,13 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; -import java.util.function.Consumer; +import java.util.function.BiConsumer; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; import org.folio.scheduler.configuration.properties.OkapiConfigurationProperties; import org.folio.scheduler.domain.dto.RoutingEntry; import org.folio.scheduler.domain.dto.TimerDescriptor; +import org.folio.scheduler.domain.dto.TimerType; import org.folio.scheduler.integration.OkapiClient; import org.folio.scheduler.service.SchedulerTimerService; import org.folio.scheduler.service.UserImpersonationService; @@ -46,7 +46,7 @@ public class OkapiHttpRequestExecutor implements Job { private final FolioModuleMetadata folioModuleMetadata; private final SchedulerTimerService schedulerTimerService; private final OkapiConfigurationProperties okapiConfigurationProperties; - private final Map> okapiCallMap; + private final Map> okapiCallMap; private final UserImpersonationService userImpersonationService; /** @@ -105,14 +105,24 @@ private void callHttpMethod(TimerDescriptor timerDescriptor) { } var staticPath = getStaticPath(re); - log.info("Calling specified HTTP method [timerId: {}, method: {}, path: {}]", timerId, httpMethod, staticPath); + var moduleHint = moduleHint(timerDescriptor); + log.info("Calling specified HTTP method [timerId: {}, method: {}, path: {}, moduleHint: {}]", + timerId, httpMethod, staticPath, moduleHint); try { - okapiCallExecutor.accept(fromUriString("http:/" + staticPath).build().toUri()); + okapiCallExecutor.accept(fromUriString("http:/" + staticPath).build().toUri(), moduleHint); } catch (FeignException e) { log.warn("Failed to perform HTTP request [id: {}, method: {}, path: /{}]", timerId, httpMethod, staticPath, e); } } + private static String moduleHint(TimerDescriptor td) { + return td.getType() == TimerType.USER ? td.getModuleName() : moduleIdOrName(td); + } + + private static String moduleIdOrName(TimerDescriptor td) { + return StringUtils.isNotEmpty(td.getModuleId()) ? td.getModuleId() : td.getModuleName(); + } + private static String getStaticPath(RoutingEntry re) { var resolvedPath = StringUtils.isEmpty(re.getPath()) ? re.getPathPattern() : re.getPath(); return resolvedPath.startsWith("/") ? resolvedPath : "/" + resolvedPath; @@ -125,7 +135,6 @@ private Map> prepareAllHeadersMap(JobDataMap jobDataM headers.put(URL, singletonList(okapiConfigurationProperties.getUrl())); headers.put(TOKEN, singletonList(userImpersonationService.impersonate(tenant, userId))); headers.put(REQUEST_ID, singletonList(String.format("%06d", current().nextInt(1000000)))); - headers.put(MODULE_ID, singletonList(folioModuleMetadata.getModuleName())); headers.put(TENANT, singletonList(tenant)); return headers; } diff --git a/src/main/java/org/folio/scheduler/utils/TimerDescriptorUtils.java b/src/main/java/org/folio/scheduler/utils/TimerDescriptorUtils.java new file mode 100644 index 0000000..8c5c38b --- /dev/null +++ b/src/main/java/org/folio/scheduler/utils/TimerDescriptorUtils.java @@ -0,0 +1,16 @@ +package org.folio.scheduler.utils; + +import static org.apache.commons.lang3.StringUtils.isNotEmpty; + +import lombok.experimental.UtilityClass; +import org.folio.common.utils.SemverUtils; +import org.folio.scheduler.domain.dto.TimerDescriptor; + +@UtilityClass +public class TimerDescriptorUtils { + + public static String evalModuleName(TimerDescriptor td) { + var moduleId = td.getModuleId(); + return isNotEmpty(moduleId) ? SemverUtils.getName(moduleId) : td.getModuleName(); + } +} diff --git a/src/main/resources/changelog/changelog-master.xml b/src/main/resources/changelog/changelog-master.xml index f82b36d..16d9250 100644 --- a/src/main/resources/changelog/changelog-master.xml +++ b/src/main/resources/changelog/changelog-master.xml @@ -10,4 +10,6 @@ + + diff --git a/src/main/resources/changelog/changes/06_add_module_id_column_to_timer.xml b/src/main/resources/changelog/changes/06_add_module_id_column_to_timer.xml new file mode 100644 index 0000000..3eb7451 --- /dev/null +++ b/src/main/resources/changelog/changes/06_add_module_id_column_to_timer.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/changelog/changes/07_add_type_column_to_timer.xml b/src/main/resources/changelog/changes/07_add_type_column_to_timer.xml new file mode 100644 index 0000000..5ad35b9 --- /dev/null +++ b/src/main/resources/changelog/changes/07_add_type_column_to_timer.xml @@ -0,0 +1,47 @@ + + + + + CREATE TYPE timer_type AS ENUM ('USER', 'SYSTEM'); + + + + + + + + + + + + + + + + module_name IS NOT NULL AND natural_key NOT LIKE 'SYSTEM%' + + + + + + module_name IS NULL AND natural_key NOT LIKE 'USER%' + + + + + + -- Update 'type' attribute in timer_descriptor jsonb column for those records where 'type' is not null + -- Set 'type' attribute to "user" if type is 'USER' and to "system" if type is 'SYSTEM' + UPDATE timer + SET timer_descriptor = jsonb_set(timer_descriptor, '{type}', to_jsonb(CASE + WHEN type = 'USER' THEN 'user' + WHEN type = 'SYSTEM' THEN 'system' + ELSE NULL + END)) + WHERE type IS NOT NULL; + + + diff --git a/src/main/resources/swagger.api/schemas/timerDescriptor.json b/src/main/resources/swagger.api/schemas/timerDescriptor.json index 3ef732c..792d0af 100644 --- a/src/main/resources/swagger.api/schemas/timerDescriptor.json +++ b/src/main/resources/swagger.api/schemas/timerDescriptor.json @@ -9,6 +9,10 @@ "type": "string", "format": "uuid" }, + "type": { + "$ref": "timerType.json", + "description": "Timer type" + }, "modified": { "description": "Whether modified", "type": "boolean" @@ -22,7 +26,11 @@ "type": "boolean" }, "moduleName": { - "description": "Module name timer belongs to", + "description": "Module name timer belongs to (module id should be used instead)", + "type": "string" + }, + "moduleId": { + "description": "Module id timer belongs to (if present module name is not needed)", "type": "string" } }, diff --git a/src/main/resources/swagger.api/schemas/timerType.json b/src/main/resources/swagger.api/schemas/timerType.json new file mode 100644 index 0000000..18b8f66 --- /dev/null +++ b/src/main/resources/swagger.api/schemas/timerType.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "timerType.json", + "title": "Timer Type Schema", + "description": "Timer type", + "default": "user", + "type": "string", + "enum": [ "user", "system" ], + "x-enum-varnames": [ "USER", "SYSTEM" ] +} diff --git a/src/test/java/org/folio/scheduler/integration/kafka/KafkaMessageListenerTest.java b/src/test/java/org/folio/scheduler/integration/kafka/KafkaMessageListenerTest.java index a5e28b8..2c3e0ad 100644 --- a/src/test/java/org/folio/scheduler/integration/kafka/KafkaMessageListenerTest.java +++ b/src/test/java/org/folio/scheduler/integration/kafka/KafkaMessageListenerTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.folio.scheduler.domain.dto.TimerUnit.MINUTE; +import static org.folio.scheduler.domain.model.TimerType.SYSTEM; import static org.folio.scheduler.integration.kafka.model.ResourceEventType.CREATE; import static org.folio.scheduler.integration.kafka.model.ResourceEventType.DELETE; import static org.folio.scheduler.integration.kafka.model.ResourceEventType.UPDATE; @@ -15,6 +16,7 @@ import org.apache.kafka.clients.consumer.ConsumerRecord; import org.folio.scheduler.domain.dto.RoutingEntry; import org.folio.scheduler.domain.dto.TimerDescriptor; +import org.folio.scheduler.domain.dto.TimerType; import org.folio.scheduler.integration.kafka.model.ResourceEvent; import org.folio.scheduler.integration.kafka.model.ScheduledTimers; import org.folio.scheduler.integration.keycloak.SystemUserService; @@ -36,6 +38,8 @@ class KafkaMessageListenerTest { private static final String SYSTEM_USER_ID = UUID.randomUUID().toString(); private static final UUID TIMER_ID = UUID.randomUUID(); private static final String MODULE_NAME = "mod-foo"; + private static final String MODULE_ID1 = "mod-foo-1.0.0"; + private static final String MODULE_ID2 = "mod-foo-1.0.1"; @InjectMocks private KafkaMessageListener kafkaMessageListener; @Mock private SystemUserService systemUserService; @@ -55,41 +59,44 @@ void handleScheduledJobEvent_positive_create() { kafkaMessageListener.handleScheduledJobEvent(consumerRecord); verify(schedulerTimerService).create( - new TimerDescriptor().enabled(true).moduleName(MODULE_NAME).routingEntry(routingEntry1())); + new TimerDescriptor().enabled(true).type(TimerType.SYSTEM) + .moduleName(MODULE_NAME).moduleId(MODULE_ID1).routingEntry(routingEntry1())); } @Test void handleScheduledJobEvent_positive_update() { when(systemUserService.findSystemUserId(TENANT_ID)).thenReturn(SYSTEM_USER_ID); - when(schedulerTimerService.findByModuleName(MODULE_NAME)).thenReturn( - List.of(new TimerDescriptor().id(TIMER_ID).enabled(true).routingEntry(routingEntry1()))); + when(schedulerTimerService.findByModuleNameAndType(MODULE_NAME, SYSTEM)).thenReturn( + List.of(new TimerDescriptor().id(TIMER_ID).type(TimerType.SYSTEM).enabled(true).routingEntry(routingEntry1()))); var consumerRec = new ConsumerRecord<>(TOPIC_NAME, 0, 0, TENANT_ID, udpateResourceEvent()); kafkaMessageListener.handleScheduledJobEvent(consumerRec); verify(schedulerTimerService).delete(TIMER_ID); - verify(schedulerTimerService).findByModuleName(MODULE_NAME); + verify(schedulerTimerService).findByModuleNameAndType(MODULE_NAME, SYSTEM); verify(schedulerTimerService).create( - new TimerDescriptor().enabled(true).moduleName(MODULE_NAME).routingEntry(routingEntry2())); + new TimerDescriptor().type(TimerType.SYSTEM).enabled(true) + .moduleName(MODULE_NAME).moduleId(MODULE_ID2).routingEntry(routingEntry2())); } @Test void handleScheduledJobEvent_positive_delete() { when(systemUserService.findSystemUserId(TENANT_ID)).thenReturn(SYSTEM_USER_ID); - when(schedulerTimerService.findByModuleName(MODULE_NAME)).thenReturn( + when(schedulerTimerService.findByModuleNameAndType(MODULE_NAME, SYSTEM)).thenReturn( List.of(new TimerDescriptor().id(TIMER_ID).enabled(true).routingEntry(routingEntry1()))); var consumerRec = new ConsumerRecord<>(TOPIC_NAME, 0, 0, TENANT_ID, deleteResourceEvent()); kafkaMessageListener.handleScheduledJobEvent(consumerRec); verify(schedulerTimerService).delete(TIMER_ID); - verify(schedulerTimerService).findByModuleName(MODULE_NAME); + verify(schedulerTimerService).findByModuleNameAndType(MODULE_NAME, SYSTEM); } @Test void handleScheduledJobEvent_negative() { when(systemUserService.findSystemUserId(TENANT_ID)).thenReturn(SYSTEM_USER_ID); - var expectedDescriptor = new TimerDescriptor().enabled(true).moduleName(MODULE_NAME).routingEntry(routingEntry1()); + var expectedDescriptor = new TimerDescriptor().enabled(true).type(TimerType.SYSTEM) + .moduleName(MODULE_NAME).moduleId(MODULE_ID1).routingEntry(routingEntry1()); when(schedulerTimerService.create(expectedDescriptor)).thenThrow(RuntimeException.class); var consumerRec = new ConsumerRecord<>(TOPIC_NAME, 0, 0, TENANT_ID, createResourceEvent()); @@ -126,14 +133,14 @@ private static ResourceEvent deleteResourceEvent() { private static ScheduledTimers scheduledTimersBeforeUpgrade() { return new ScheduledTimers() - .moduleId("mod-foo-1.0.0") + .moduleId(MODULE_ID1) .applicationId("app-foo-1.0.0") .timers(List.of(routingEntry1())); } private static ScheduledTimers scheduledTimersAfterUpgrade() { return new ScheduledTimers() - .moduleId("mod-foo-1.0.1") + .moduleId(MODULE_ID2) .applicationId("app-foo-1.0.1") .timers(List.of(routingEntry2())); } diff --git a/src/test/java/org/folio/scheduler/it/KafkaMessageListenerIT.java b/src/test/java/org/folio/scheduler/it/KafkaMessageListenerIT.java index 971f695..def27ad 100644 --- a/src/test/java/org/folio/scheduler/it/KafkaMessageListenerIT.java +++ b/src/test/java/org/folio/scheduler/it/KafkaMessageListenerIT.java @@ -1,14 +1,13 @@ package org.folio.scheduler.it; import static java.util.Arrays.asList; -import static java.util.stream.Collectors.toList; -import static org.apache.commons.collections4.ListUtils.emptyIfNull; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Durations.ONE_HUNDRED_MILLISECONDS; import static org.awaitility.Durations.ONE_MINUTE; import static org.awaitility.Durations.ONE_SECOND; import static org.awaitility.Durations.TWO_HUNDRED_MILLISECONDS; import static org.awaitility.Durations.TWO_SECONDS; +import static org.folio.common.utils.CollectionUtils.mapItems; import static org.folio.scheduler.domain.dto.TimerUnit.SECOND; import static org.folio.scheduler.integration.kafka.model.ResourceEventType.CREATE; import static org.folio.scheduler.support.TestConstants.TENANT_ID; @@ -33,9 +32,11 @@ import java.util.stream.Stream; import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; +import org.folio.common.utils.SemverUtils; import org.folio.scheduler.domain.dto.RoutingEntry; import org.folio.scheduler.domain.dto.TimerDescriptor; import org.folio.scheduler.domain.dto.TimerDescriptorList; +import org.folio.scheduler.domain.dto.TimerType; import org.folio.scheduler.integration.kafka.model.ResourceEvent; import org.folio.scheduler.integration.kafka.model.ScheduledTimers; import org.folio.scheduler.service.SchedulerTimerService; @@ -74,6 +75,8 @@ class KafkaMessageListenerIT extends BaseIntegrationTest { private static final String SCHEDULED_TIMER_TOPIC = "it.test.mgr-tenant-entitlements.scheduled-job"; + private static final String MODULE_ID = "mod-foo-1.0.0"; + private static final String MODULE_NAME = "mod-foo"; @SpyBean private SchedulerTimerService schedulerTimerService; @Autowired private Scheduler scheduler; @@ -131,13 +134,13 @@ void handleScheduledJobEvent_positive_eventIsSentWhenTenantIsDisabled() { @WireMockStub("/wiremock/stubs/event-timer-endpoint.json") @KeycloakRealms("/json/keycloak/test-realm.json") void handleScheduledJobEvent_positive_upgradeEvent() { - var newTimerEvent = readString("json/events/folio-app1/folio-module1/create-timer-event.json"); + var newTimerEvent = readString("json/events/folio-app1/mod-foo/create-timer-event.json"); kafkaTemplate.send(SCHEDULED_TIMER_TOPIC, newTimerEvent); var scheduledTimers1 = parse(newTimerEvent, ResourceEvent.class); var routingEntries1 = convertValue(scheduledTimers1.getNewValue(), ScheduledTimers.class); await().untilAsserted(() -> getScheduledTimers(timerDescriptorList(routingEntries1))); - var upgradeEvent = readString("json/events/folio-app1/folio-module1/upgrade-timer-event.json"); + var upgradeEvent = readString("json/events/folio-app1/mod-foo/upgrade-timer-event.json"); kafkaTemplate.send(SCHEDULED_TIMER_TOPIC, upgradeEvent); var scheduledTimers2 = parse(upgradeEvent, ResourceEvent.class); var routingEntries2 = convertValue(scheduledTimers2.getNewValue(), ScheduledTimers.class); @@ -155,7 +158,7 @@ void handleScheduledJobEvent_positive_userTimerExistsAfterDeleteEvent() { .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.id").hasJsonPath()); - var createTimerEvent = readString("json/events/folio-app1/folio-module1/create-timer-event.json"); + var createTimerEvent = readString("json/events/folio-app1/mod-foo/create-timer-event.json"); kafkaTemplate.send(SCHEDULED_TIMER_TOPIC, createTimerEvent); var scheduledTimers1 = parse(createTimerEvent, ResourceEvent.class); var routingEntries1 = convertValue(scheduledTimers1.getNewValue(), ScheduledTimers.class); @@ -163,7 +166,7 @@ void handleScheduledJobEvent_positive_userTimerExistsAfterDeleteEvent() { timerDescriptorList(routingEntries1).addTimerDescriptorsItem(userTimerDescriptorRequest).totalRecords(2); await().untilAsserted(() -> getScheduledTimers(timerDescList1)); - var deleteTimerEvent = readString("json/events/folio-app1/folio-module1/delete-timer-event.json"); + var deleteTimerEvent = readString("json/events/folio-app1/mod-foo/delete-timer-event.json"); kafkaTemplate.send(SCHEDULED_TIMER_TOPIC, deleteTimerEvent); var userTimer = timerDescriptorList(userTimerDescriptorRequest); await().untilAsserted(() -> getScheduledTimers(userTimer)); @@ -242,11 +245,13 @@ private static void awaitFor(Duration duration) { } private static TimerDescriptor timerDescriptor() { - return new TimerDescriptor().enabled(true).routingEntry(routingEntry()); + return new TimerDescriptor().type(TimerType.SYSTEM).enabled(true) + .moduleId(MODULE_ID).moduleName(MODULE_NAME).routingEntry(routingEntry()); } private static TimerDescriptor timerDescriptor(RoutingEntry routingEntry) { - return new TimerDescriptor().enabled(true).routingEntry(routingEntry); + return new TimerDescriptor().type(TimerType.SYSTEM).enabled(true) + .moduleId(MODULE_ID).moduleName(MODULE_NAME).routingEntry(routingEntry); } private static TimerDescriptorList timerDescriptorList(TimerDescriptor... timerDescriptors) { @@ -261,8 +266,10 @@ private static TimerDescriptorList timerDescriptorList(ScheduledTimers scheduled } private static List mapToTimerDescriptors(ScheduledTimers scheduledTimers) { - return emptyIfNull(scheduledTimers.getTimers()).stream().map(KafkaMessageListenerIT::timerDescriptor) - .collect(toList()); + return mapItems(scheduledTimers.getTimers(), routingEntry -> { + var moduleId = scheduledTimers.getModuleId(); + return timerDescriptor(routingEntry).moduleId(moduleId).moduleName(SemverUtils.getName(moduleId)); + }); } private static ResourceEvent resourceEvent() { @@ -275,7 +282,7 @@ private static ResourceEvent resourceEvent() { private static ScheduledTimers scheduledTimers(RoutingEntry... routingEntries) { return new ScheduledTimers() - .moduleId("mod-foo-1.0.0") + .moduleId(MODULE_ID) .applicationId("app-foo-1.0.0") .timers(asList(routingEntries)); } diff --git a/src/test/java/org/folio/scheduler/it/SchedulerTimerIT.java b/src/test/java/org/folio/scheduler/it/SchedulerTimerIT.java index d9649a0..318a154 100644 --- a/src/test/java/org/folio/scheduler/it/SchedulerTimerIT.java +++ b/src/test/java/org/folio/scheduler/it/SchedulerTimerIT.java @@ -48,6 +48,7 @@ class SchedulerTimerIT extends BaseIntegrationTest { private static final String UNKNOWN_ID = "51fd5dff-5d51-4169-a296-d441e1d234c9"; private static final UUID TIMER_ID_TO_UPDATE = UUID.fromString("123e4567-e89b-12d3-a456-426614174001"); private static final String TIMER_ID_TO_DELETE = "123e4567-e89b-12d3-a456-426614174002"; + private static final String MODULE_ID = "mod-foo-1.0.0"; @Autowired private Scheduler scheduler; @Autowired private ObjectMapper objectMapper; @@ -82,13 +83,14 @@ void getAll_positive() throws Exception { } @Test - @WireMockStub("/wiremock/stubs/timer-endpoint.json") + @WireMockStub("/wiremock/stubs/user-timer-endpoint.json") @KeycloakRealms("/json/keycloak/test-realm.json") void create_positive_simpleTrigger() throws Exception { var timerId = UUID.randomUUID(); var timerDescriptor = new TimerDescriptor() .id(timerId) .enabled(true) + .moduleId(MODULE_ID) .routingEntry(new RoutingEntry() .methods(List.of("POST")) .pathPattern("/test") @@ -108,13 +110,14 @@ void create_positive_simpleTrigger() throws Exception { } @Test - @WireMockStub("/wiremock/stubs/timer-endpoint.json") + @WireMockStub("/wiremock/stubs/user-timer-endpoint.json") @KeycloakRealms("/json/keycloak/test-realm.json") void create_positive_cronTrigger() throws Exception { var timerId = UUID.randomUUID().toString(); var timerDescriptor = new TimerDescriptor() .id(UUID.fromString(timerId)) .enabled(true) + .moduleId(MODULE_ID) .routingEntry(new RoutingEntry() .methods(List.of("POST")) .pathPattern("/test") @@ -134,7 +137,7 @@ void create_positive_cronTrigger() throws Exception { @Test void update_positive() throws Exception { - var desc = TestValues.timerDescriptor(TIMER_ID_TO_UPDATE); + var desc = TestValues.timerDescriptor(TIMER_ID_TO_UPDATE).moduleId(MODULE_ID); doPut("/scheduler/timers/{id}", desc, TIMER_ID_TO_UPDATE) .andExpect(jsonPath("$.id", notNullValue())) .andExpect(jsonPath("$.enabled", is(true))); @@ -160,6 +163,7 @@ void delete_negative_notFound() throws Exception { void create_duplicate() throws Exception { var timerDescriptor = new TimerDescriptor() .enabled(true) + .moduleId(MODULE_ID) .routingEntry(new RoutingEntry() .methods(List.of("POST")) .pathPattern("/test/sometimer") diff --git a/src/test/java/org/folio/scheduler/migration/SplitTimersByMethodMigrationTest.java b/src/test/java/org/folio/scheduler/migration/SplitTimersByMethodMigrationTest.java index 6467b58..9b81943 100644 --- a/src/test/java/org/folio/scheduler/migration/SplitTimersByMethodMigrationTest.java +++ b/src/test/java/org/folio/scheduler/migration/SplitTimersByMethodMigrationTest.java @@ -2,6 +2,8 @@ import static java.util.List.of; import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.scheduler.domain.entity.TimerDescriptorEntity.toNaturalKey; +import static org.folio.scheduler.support.TestValues.MODULE_NAME; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -67,9 +69,9 @@ void test_execute_positive() throws Exception { when(mockResultSet.getString("id")).thenReturn(uuid1.toString()).thenReturn(uuid2.toString()); when(mockSchedulerTimerRepository.findById(uuid1)).thenReturn( - Optional.of(TimerDescriptorEntity.of(new TimerDescriptor(routingEntry1, true)))); + Optional.of(entity(new TimerDescriptor(routingEntry1, true).moduleName(MODULE_NAME)))); when(mockSchedulerTimerRepository.findById(uuid2)).thenReturn( - Optional.of(TimerDescriptorEntity.of(new TimerDescriptor(routingEntry2, false)))); + Optional.of(entity(new TimerDescriptor(routingEntry2, false).moduleName(MODULE_NAME)))); var methodsPassed = new ArrayList<>(); when(mockSchedulerTimerRepository.save(any())).thenAnswer(inv -> { @@ -113,4 +115,12 @@ protected Database setupDbConnectionMock(Map mockQueryRespons } return mockLiquibaseDbAccess; } + + private static TimerDescriptorEntity entity(TimerDescriptor timerDescriptor) { + var entity = new TimerDescriptorEntity(); + entity.setId(timerDescriptor.getId()); + entity.setTimerDescriptor(timerDescriptor); + entity.setNaturalKey(toNaturalKey(timerDescriptor)); + return entity; + } } diff --git a/src/test/java/org/folio/scheduler/service/SchedulerTimerServiceTest.java b/src/test/java/org/folio/scheduler/service/SchedulerTimerServiceTest.java index a57f5c4..0a2b19f 100644 --- a/src/test/java/org/folio/scheduler/service/SchedulerTimerServiceTest.java +++ b/src/test/java/org/folio/scheduler/service/SchedulerTimerServiceTest.java @@ -38,6 +38,8 @@ @ExtendWith(MockitoExtension.class) class SchedulerTimerServiceTest { + private static final String MODULE_ID = "mod-foo-1.0.0"; + @InjectMocks SchedulerTimerService schedulerTimerService; @Mock private SchedulerTimerRepository schedulerTimerRepository; @Mock private TimerDescriptorMapper timerDescriptorMapper; @@ -88,8 +90,8 @@ void getAll_positive() { @Test void create_positive() { - var descriptor = timerDescriptor(); - var entity = timerDescriptorEntity(); + var descriptor = timerDescriptor().moduleId(MODULE_ID); + var entity = timerDescriptorEntity(descriptor); when(timerDescriptorMapper.convert(descriptor)).thenReturn(entity); when(schedulerTimerRepository.save(entity)).thenReturn(entity); @@ -101,7 +103,7 @@ void create_positive() { @Test void create_positive_entityIdIsNull() { - var descriptor = timerDescriptor(null); + var descriptor = timerDescriptor(null).moduleId(MODULE_ID); when(timerDescriptorMapper.convert(timerDescriptorCaptor.capture())) .thenAnswer(inv -> timerDescriptorEntity(inv.getArgument(0))); when(schedulerTimerRepository.save(any(TimerDescriptorEntity.class))).thenAnswer(inv -> inv.getArgument(0)); @@ -124,9 +126,27 @@ void create_negative_foundEntityById() { .hasMessage("TimerDescriptor already exist for id " + TIMER_UUID); } + @Test + void create_negative_moduleIdAndNameIsEmpty() { + var descriptor = timerDescriptor().moduleId(null).moduleName(null); + + assertThatThrownBy(() -> schedulerTimerService.create(descriptor)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Module id or module name is required"); + } + + @Test + void create_negative_timerTypeIsNull() { + var descriptor = timerDescriptor().type(null); + + assertThatThrownBy(() -> schedulerTimerService.create(descriptor)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Timer type is required"); + } + @Test void update_positive() { - var expectedDescriptor = timerDescriptor().modified(true); + var expectedDescriptor = timerDescriptor().moduleId(MODULE_ID).modified(true); var existingEntity = timerDescriptorEntity(); var entityToUpdate = timerDescriptorEntity(expectedDescriptor); @@ -135,7 +155,7 @@ void update_positive() { when(schedulerTimerRepository.save(entityToUpdate)).thenReturn(entityToUpdate); doNothing().when(jobSchedulingService).reschedule(existingEntity.getTimerDescriptor(), expectedDescriptor); - var actual = schedulerTimerService.update(TIMER_UUID, timerDescriptor()); + var actual = schedulerTimerService.update(TIMER_UUID, timerDescriptor().moduleId(MODULE_ID)); assertThat(actual).isEqualTo(expectedDescriptor); } @@ -158,13 +178,31 @@ void update_negative_differentIdInParameterAndBody() { @Test void update_negative_entityNotFound() { - var descriptor = timerDescriptor(); + var descriptor = timerDescriptor().moduleId(MODULE_ID); when(schedulerTimerRepository.findById(TIMER_UUID)).thenReturn(Optional.empty()); assertThatThrownBy(() -> schedulerTimerService.update(TIMER_UUID, descriptor)) .isInstanceOf(EntityNotFoundException.class) .hasMessage("Unable to find timer descriptor with id " + TIMER_UUID); } + @Test + void update_negative_moduleIdAndNameIsEmpty() { + var descriptor = timerDescriptor().moduleId(null).moduleName(null); + + assertThatThrownBy(() -> schedulerTimerService.update(TIMER_UUID, descriptor)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Module id or module name is required"); + } + + @Test + void update_negative_timerTypeIsNull() { + var descriptor = timerDescriptor().type(null); + + assertThatThrownBy(() -> schedulerTimerService.update(TIMER_UUID, descriptor)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Timer type is required"); + } + @Test void delete_positive() { var timerDescriptorEntity = timerDescriptorEntity(); @@ -199,9 +237,9 @@ void deleteAll_positive() { @Test void create_duplicate() { - var descriptor = timerDescriptor(); + var descriptor = timerDescriptor().moduleId(MODULE_ID); descriptor.setId(null); - var entity = timerDescriptorEntity(); + var entity = timerDescriptorEntity(descriptor); when(timerDescriptorMapper.convert(descriptor)).thenReturn(entity); when(schedulerTimerRepository.save(entity)).thenReturn(entity); diff --git a/src/test/java/org/folio/scheduler/service/jobs/OkapiHttpRequestExecutorTest.java b/src/test/java/org/folio/scheduler/service/jobs/OkapiHttpRequestExecutorTest.java index 2460d34..3eb0797 100644 --- a/src/test/java/org/folio/scheduler/service/jobs/OkapiHttpRequestExecutorTest.java +++ b/src/test/java/org/folio/scheduler/service/jobs/OkapiHttpRequestExecutorTest.java @@ -6,7 +6,6 @@ import static org.folio.scheduler.support.TestConstants.TIMER_ID; import static org.folio.scheduler.support.TestConstants.TIMER_UUID; import static org.folio.scheduler.support.TestConstants.USER_TOKEN; -import static org.folio.scheduler.support.TestValues.timerDescriptor; import static org.folio.spring.integration.XOkapiHeaders.TENANT; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verify; @@ -23,10 +22,13 @@ import java.util.Optional; import org.folio.scheduler.configuration.properties.OkapiConfigurationProperties; import org.folio.scheduler.domain.dto.RoutingEntry; +import org.folio.scheduler.domain.dto.TimerDescriptor; +import org.folio.scheduler.domain.dto.TimerType; import org.folio.scheduler.integration.OkapiClient; import org.folio.scheduler.service.SchedulerTimerService; import org.folio.scheduler.service.UserImpersonationService; import org.folio.scheduler.support.TestConstants; +import org.folio.scheduler.support.TestValues; import org.folio.spring.FolioModuleMetadata; import org.folio.spring.integration.XOkapiHeaders; import org.folio.test.types.UnitTest; @@ -44,8 +46,11 @@ @ExtendWith(MockitoExtension.class) class OkapiHttpRequestExecutorTest { - private final String okapiUrl = "http://okapi:9130"; - private final String moduleName = "mod-scheduler"; + private static final String TEST_MODULE_ID = "mod-test-1.0"; + private static final String TEST_MODULE_NAME = "mod-test"; + + private static final String OKAPI_URL = "http://okapi:9130"; + private static final String MODULE_NAME = "mod-scheduler"; @InjectMocks private OkapiHttpRequestExecutor job; @Mock private OkapiClient okapiClient; @@ -69,41 +74,55 @@ void tearDown() { @Test void execute_positive_userTokenIsNull() { var re = new RoutingEntry().methods(List.of("GET")).pathPattern("/test-endpoint"); - when(folioModuleMetadata.getModuleName()).thenReturn(moduleName); - when(okapiConfigurationProperties.getUrl()).thenReturn(okapiUrl); + when(folioModuleMetadata.getModuleName()).thenReturn(MODULE_NAME); + when(okapiConfigurationProperties.getUrl()).thenReturn(OKAPI_URL); when(jobExecutionContext.getJobDetail()).thenReturn(jobDetail()); - when(schedulerTimerService.findById(TIMER_UUID)).thenReturn(of(timerDescriptor().routingEntry(re))); + when(schedulerTimerService.findById(TIMER_UUID)).thenReturn(of(timerDescriptor(re))); when(userImpersonationService.impersonate(TENANT_ID, TestConstants.USER_ID)).thenReturn(null); job.execute(jobExecutionContext); - verify(okapiClient).doGet(fromUriString("http://test-endpoint").build().toUri()); + verify(okapiClient).doGet(fromUriString("http://test-endpoint").build().toUri(), TEST_MODULE_ID); } @Test void execute_positive_userTokenIsNotNull() { var re = new RoutingEntry().methods(List.of("GET")).pathPattern("/test-endpoint"); - when(folioModuleMetadata.getModuleName()).thenReturn(moduleName); + when(folioModuleMetadata.getModuleName()).thenReturn(MODULE_NAME); when(jobExecutionContext.getJobDetail()).thenReturn(jobDetail()); - when(okapiConfigurationProperties.getUrl()).thenReturn(okapiUrl); - when(schedulerTimerService.findById(TIMER_UUID)).thenReturn(of(timerDescriptor().routingEntry(re))); + when(okapiConfigurationProperties.getUrl()).thenReturn(OKAPI_URL); + when(schedulerTimerService.findById(TIMER_UUID)).thenReturn(of(timerDescriptor(re))); job.execute(jobExecutionContext); - verify(okapiClient).doGet(fromUriString("http://test-endpoint").build().toUri()); + verify(okapiClient).doGet(fromUriString("http://test-endpoint").build().toUri(), TEST_MODULE_ID); } @Test void execute_positive_methodNotDefined() { var re = new RoutingEntry().path("test-endpoint"); - when(folioModuleMetadata.getModuleName()).thenReturn(moduleName); - when(okapiConfigurationProperties.getUrl()).thenReturn(okapiUrl); + when(folioModuleMetadata.getModuleName()).thenReturn(MODULE_NAME); + when(okapiConfigurationProperties.getUrl()).thenReturn(OKAPI_URL); when(jobExecutionContext.getJobDetail()).thenReturn(jobDetail()); - when(schedulerTimerService.findById(TIMER_UUID)).thenReturn(of(timerDescriptor().routingEntry(re))); + when(schedulerTimerService.findById(TIMER_UUID)).thenReturn(of(timerDescriptor(re))); job.execute(jobExecutionContext); - verify(okapiClient).doPost(fromUriString("http://test-endpoint").build().toUri()); + verify(okapiClient).doPost(fromUriString("http://test-endpoint").build().toUri(), TEST_MODULE_ID); + } + + @Test + void execute_positive_moduleNameAsHint() { + var re = new RoutingEntry().methods(List.of("GET")).pathPattern("/test-endpoint"); + when(folioModuleMetadata.getModuleName()).thenReturn(MODULE_NAME); + when(jobExecutionContext.getJobDetail()).thenReturn(jobDetail()); + when(okapiConfigurationProperties.getUrl()).thenReturn(OKAPI_URL); + when(schedulerTimerService.findById(TIMER_UUID)).thenReturn(of( + TestValues.timerDescriptor().routingEntry(re).moduleName(TEST_MODULE_NAME))); + + job.execute(jobExecutionContext); + + verify(okapiClient).doGet(fromUriString("http://test-endpoint").build().toUri(), TEST_MODULE_NAME); } @Test @@ -112,26 +131,27 @@ void execute_negative_feignException() { var expectedUri = fromUriString("http://test-endpoint").build().toUri(); var request = Request.create(HttpMethod.DELETE, "http://test-endpoint", emptyMap(), null, (RequestTemplate) null); - when(folioModuleMetadata.getModuleName()).thenReturn(moduleName); - when(okapiConfigurationProperties.getUrl()).thenReturn(okapiUrl); + when(folioModuleMetadata.getModuleName()).thenReturn(MODULE_NAME); + when(okapiConfigurationProperties.getUrl()).thenReturn(OKAPI_URL); when(jobExecutionContext.getJobDetail()).thenReturn(jobDetail()); - when(okapiConfigurationProperties.getUrl()).thenReturn(okapiUrl); - when(schedulerTimerService.findById(TIMER_UUID)).thenReturn(of(timerDescriptor().routingEntry(re))); - doThrow(new NotFound("Not Found", request, null, emptyMap())).when(okapiClient).doDelete(expectedUri); + when(okapiConfigurationProperties.getUrl()).thenReturn(OKAPI_URL); + when(schedulerTimerService.findById(TIMER_UUID)).thenReturn(of(timerDescriptor(re))); + doThrow(new NotFound("Not Found", request, null, emptyMap())).when(okapiClient).doDelete(expectedUri, + TEST_MODULE_ID); job.execute(jobExecutionContext); - verify(okapiClient).doDelete(expectedUri); + verify(okapiClient).doDelete(expectedUri, TEST_MODULE_ID); } @Test void execute_negative_unsupportedMethod() { var re = new RoutingEntry().path("/test-endpoint").methods(List.of("PATCH")); - when(folioModuleMetadata.getModuleName()).thenReturn(moduleName); - when(okapiConfigurationProperties.getUrl()).thenReturn(okapiUrl); + when(folioModuleMetadata.getModuleName()).thenReturn(MODULE_NAME); + when(okapiConfigurationProperties.getUrl()).thenReturn(OKAPI_URL); when(jobExecutionContext.getJobDetail()).thenReturn(jobDetail()); - when(schedulerTimerService.findById(TIMER_UUID)).thenReturn(of(timerDescriptor().routingEntry(re))); + when(schedulerTimerService.findById(TIMER_UUID)).thenReturn(of(timerDescriptor(re))); job.execute(jobExecutionContext); @@ -140,8 +160,8 @@ void execute_negative_unsupportedMethod() { @Test void execute_negative_timerDescriptorNotFound() { - when(folioModuleMetadata.getModuleName()).thenReturn(moduleName); - when(okapiConfigurationProperties.getUrl()).thenReturn(okapiUrl); + when(folioModuleMetadata.getModuleName()).thenReturn(MODULE_NAME); + when(okapiConfigurationProperties.getUrl()).thenReturn(OKAPI_URL); when(jobExecutionContext.getJobDetail()).thenReturn(jobDetail()); when(schedulerTimerService.findById(TIMER_UUID)).thenReturn(Optional.empty()); @@ -157,4 +177,8 @@ private static JobDetailImpl jobDetail() { jobDetail.getJobDataMap().put(XOkapiHeaders.USER_ID, TestConstants.USER_ID); return jobDetail; } + + private static TimerDescriptor timerDescriptor(RoutingEntry re) { + return TestValues.timerDescriptor().type(TimerType.SYSTEM).routingEntry(re).moduleId(TEST_MODULE_ID); + } } diff --git a/src/test/java/org/folio/scheduler/support/TestValues.java b/src/test/java/org/folio/scheduler/support/TestValues.java index 872b292..349d93b 100644 --- a/src/test/java/org/folio/scheduler/support/TestValues.java +++ b/src/test/java/org/folio/scheduler/support/TestValues.java @@ -14,6 +14,9 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class TestValues { + public static final String MODULE_NAME = "mod-foo"; + public static final String MODULE_ID = "mod-foo-1.0.0"; + public static UUID randomUuid() { return UUID.randomUUID(); } @@ -26,6 +29,7 @@ public static TimerDescriptor timerDescriptor(UUID uuid) { return new TimerDescriptor() .id(uuid) .enabled(true) + .moduleName(MODULE_NAME) .routingEntry(new RoutingEntry() .methods(List.of("POST")) .pathPattern("/testb/timer/20") @@ -41,6 +45,8 @@ public static TimerDescriptorEntity timerDescriptorEntity(TimerDescriptor descri var entity = new TimerDescriptorEntity(); entity.setId(TIMER_UUID); entity.setTimerDescriptor(descriptor); + entity.setModuleId(descriptor.getModuleId()); + entity.setModuleName(descriptor.getModuleName()); return entity; } } diff --git a/src/test/java/org/folio/scheduler/utils/TimerDescriptorUtilsTest.java b/src/test/java/org/folio/scheduler/utils/TimerDescriptorUtilsTest.java new file mode 100644 index 0000000..4492ac3 --- /dev/null +++ b/src/test/java/org/folio/scheduler/utils/TimerDescriptorUtilsTest.java @@ -0,0 +1,36 @@ +package org.folio.scheduler.utils; + +import static org.folio.scheduler.support.TestValues.MODULE_ID; +import static org.folio.scheduler.support.TestValues.MODULE_NAME; +import static org.folio.scheduler.utils.TimerDescriptorUtils.evalModuleName; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.of; + +import java.util.stream.Stream; +import org.folio.scheduler.domain.dto.TimerDescriptor; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class TimerDescriptorUtilsTest { + + @ParameterizedTest + @MethodSource("evalModuleNameArguments") + void testEvalModuleName(String moduleId, String moduleName, String expected) { + var td = new TimerDescriptor() + .moduleId(moduleId).moduleName(moduleName); + + String result = evalModuleName(td); + + assertEquals(expected, result); + } + + static Stream evalModuleNameArguments() { + return Stream.of( + of(MODULE_ID, MODULE_NAME, MODULE_NAME), + of(MODULE_ID, null, MODULE_NAME), + of(null, MODULE_NAME, MODULE_NAME), + of(null, null, null) + ); + } +} diff --git a/src/test/resources/json/events/folio-app1/folio-module1/create-timer-event.json b/src/test/resources/json/events/folio-app1/mod-foo/create-timer-event.json similarity index 73% rename from src/test/resources/json/events/folio-app1/folio-module1/create-timer-event.json rename to src/test/resources/json/events/folio-app1/mod-foo/create-timer-event.json index c6b66f3..33815ad 100644 --- a/src/test/resources/json/events/folio-app1/folio-module1/create-timer-event.json +++ b/src/test/resources/json/events/folio-app1/mod-foo/create-timer-event.json @@ -3,12 +3,12 @@ "tenant": "test", "resourceName": "Scheduled Job", "new": { - "moduleId": "folio-module1-1.0.0", + "moduleId": "mod-foo-1.0.0", "applicationId": "folio-app1-1.1.1", "timers": [ { "methods": [ "GET" ], - "pathPattern": "/folio-module1/v3/another-scheduled-timer", + "pathPattern": "/mod-foo/v3/another-scheduled-timer", "schedule": { "cron": "0 6,18 * * *", "zone": "CET" diff --git a/src/test/resources/json/events/folio-app1/folio-module1/delete-timer-event.json b/src/test/resources/json/events/folio-app1/mod-foo/delete-timer-event.json similarity index 74% rename from src/test/resources/json/events/folio-app1/folio-module1/delete-timer-event.json rename to src/test/resources/json/events/folio-app1/mod-foo/delete-timer-event.json index a32d40b..39f0cc8 100644 --- a/src/test/resources/json/events/folio-app1/folio-module1/delete-timer-event.json +++ b/src/test/resources/json/events/folio-app1/mod-foo/delete-timer-event.json @@ -3,12 +3,12 @@ "tenant": "test", "resourceName": "Scheduled Job", "old": { - "moduleId": "folio-module1-1.0.0", + "moduleId": "mod-foo-1.0.0", "applicationId": "folio-app1-1.1.1", "timers": [ { "methods": [ "GET" ], - "pathPattern": "/folio-module1/v3/scheduled-timer", + "pathPattern": "/mod-foo/v3/scheduled-timer", "schedule": { "cron": "0 6,18 * * *", "zone": "CET" diff --git a/src/test/resources/json/events/folio-app1/folio-module1/upgrade-timer-event.json b/src/test/resources/json/events/folio-app1/mod-foo/upgrade-timer-event.json similarity index 70% rename from src/test/resources/json/events/folio-app1/folio-module1/upgrade-timer-event.json rename to src/test/resources/json/events/folio-app1/mod-foo/upgrade-timer-event.json index 16f0163..cde7e9b 100644 --- a/src/test/resources/json/events/folio-app1/folio-module1/upgrade-timer-event.json +++ b/src/test/resources/json/events/folio-app1/mod-foo/upgrade-timer-event.json @@ -3,12 +3,12 @@ "tenant": "test", "resourceName": "Scheduled Job", "old": { - "moduleId": "folio-module1-1.0.0", + "moduleId": "mod-foo-1.0.0", "applicationId": "folio-app1-1.1.1", "timers": [ { "methods": [ "GET" ], - "pathPattern": "/folio-module1/v3/scheduled-timer", + "pathPattern": "/mod-foo/v3/scheduled-timer", "schedule": { "cron": "0 6,18 * * *", "zone": "CET" @@ -17,12 +17,12 @@ ] }, "new": { - "moduleId": "folio-module1-1.0.2", + "moduleId": "mod-foo-1.0.2", "applicationId": "folio-app1-1.1.2", "timers": [ { "methods": [ "GET" ], - "pathPattern": "/folio-module1/v3/scheduled-timer", + "pathPattern": "/mod-foo/v3/scheduled-timer", "unit": "second", "delay": "20" } diff --git a/src/test/resources/json/user/timer/user-timer-request.json b/src/test/resources/json/user/timer/user-timer-request.json index af5ca8d..9107291 100644 --- a/src/test/resources/json/user/timer/user-timer-request.json +++ b/src/test/resources/json/user/timer/user-timer-request.json @@ -1,8 +1,9 @@ { "enabled": true, + "moduleId": "mod-foo-1.0.0", "routingEntry": { "methods": [ "GET" ], - "pathPattern": "/folio-module1/v3/scheduled-timer", + "pathPattern": "/mod-foo/v3/scheduled-timer", "schedule": { "cron": "0 6,18 * * *", "zone": "CET" diff --git a/src/test/resources/sql/timer-descriptor-it.sql b/src/test/resources/sql/timer-descriptor-it.sql index 6f76695..9f4115b 100644 --- a/src/test/resources/sql/timer-descriptor-it.sql +++ b/src/test/resources/sql/timer-descriptor-it.sql @@ -1,9 +1,12 @@ -insert into test_mod_scheduler.timer(id, timer_descriptor) +insert into test_mod_scheduler.timer(id, module_id, module_name, type, timer_descriptor) values - ('123e4567-e89b-12d3-a456-426614174000', '{ + ('123e4567-e89b-12d3-a456-426614174000', 'mod-foo-1.0.0', 'mod-foo', 'USER', '{ "id":"123e4567-e89b-12d3-a456-426614174000", "modified": "false", "enabled": "true", + "moduleId": "mod-foo-1.0.0", + "moduleName": "mod-foo", + "type": "user", "routingEntry": { "methods": [ "POST" @@ -14,9 +17,12 @@ values } }' ), - ('123e4567-e89b-12d3-a456-426614174001', '{ + ('123e4567-e89b-12d3-a456-426614174001', 'mod-foo-1.0.0', 'mod-foo', 'USER', '{ "id": "123e4567-e89b-12d3-a456-426614174001", "modified": "false", + "moduleId": "mod-foo-1.0.0", + "moduleName": "mod-foo", + "type": "user", "routingEntry": { "methods": [ "POST" @@ -28,9 +34,12 @@ values } }' ), - ('123e4567-e89b-12d3-a456-426614174002', '{ + ('123e4567-e89b-12d3-a456-426614174002', 'mod-foo-1.0.0', 'mod-foo', 'USER', '{ "id": "123e4567-e89b-12d3-a456-426614174002", "modified": "false", + "moduleId": "mod-foo-1.0.0", + "moduleName": "mod-foo", + "type": "user", "routingEntry": { "methods": [ "POST" diff --git a/src/test/resources/wiremock/stubs/event-timer-endpoint.json b/src/test/resources/wiremock/stubs/event-timer-endpoint.json index dbd0ff5..7c6b0f5 100644 --- a/src/test/resources/wiremock/stubs/event-timer-endpoint.json +++ b/src/test/resources/wiremock/stubs/event-timer-endpoint.json @@ -1,10 +1,22 @@ { "request": { "method": "GET", - "urlPattern": "/folio-module1/v3/scheduled-timer", + "urlPattern": "/mod-foo/v3/scheduled-timer", "headers": { "Content-Type": { "equalTo": "application/json" + }, + "x-okapi-module-hint": { + "matches" : "^mod\\-foo\\-\\d\\.\\d\\.\\d$" + }, + "x-okapi-tenant": { + "equalTo": "test" + }, + "x-okapi-request-id": { + "matches" : "^\\d{6}$" + }, + "x-okapi-url": { + "matches" : "^http://[a-zA-Z0-9.-]+:\\d{1,5}$" } } }, diff --git a/src/test/resources/wiremock/stubs/timer-endpoint.json b/src/test/resources/wiremock/stubs/timer-endpoint.json index b864630..c2d0e32 100644 --- a/src/test/resources/wiremock/stubs/timer-endpoint.json +++ b/src/test/resources/wiremock/stubs/timer-endpoint.json @@ -5,6 +5,18 @@ "headers": { "Content-Type": { "equalTo": "application/json" + }, + "x-okapi-module-hint": { + "equalTo": "mod-foo-1.0.0" + }, + "x-okapi-tenant": { + "equalTo": "test" + }, + "x-okapi-request-id": { + "matches" : "^\\d{6}$" + }, + "x-okapi-url": { + "matches" : "^http://[a-zA-Z0-9.-]+:\\d{1,5}$" } } }, diff --git a/src/test/resources/wiremock/stubs/user-timer-endpoint.json b/src/test/resources/wiremock/stubs/user-timer-endpoint.json new file mode 100644 index 0000000..b475e22 --- /dev/null +++ b/src/test/resources/wiremock/stubs/user-timer-endpoint.json @@ -0,0 +1,32 @@ +{ + "request": { + "method": "POST", + "url": "/test", + "headers": { + "Content-Type": { + "equalTo": "application/json" + }, + "x-okapi-module-hint": { + "equalTo": "mod-foo" + }, + "x-okapi-tenant": { + "equalTo": "test" + }, + "x-okapi-request-id": { + "matches" : "^\\d{6}$" + }, + "x-okapi-url": { + "matches" : "^http://[a-zA-Z0-9.-]+:\\d{1,5}$" + } + } + }, + "response": { + "status": 201, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "message": "success!" + } + } +}