diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/orgrolemapping/domain/service/RefreshJobConfigServiceIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/orgrolemapping/domain/service/RefreshJobConfigServiceIntegrationTest.java new file mode 100644 index 000000000..0232ec5e1 --- /dev/null +++ b/src/integrationTest/java/uk/gov/hmcts/reform/orgrolemapping/domain/service/RefreshJobConfigServiceIntegrationTest.java @@ -0,0 +1,193 @@ +package uk.gov.hmcts.reform.orgrolemapping.domain.service; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; +import uk.gov.hmcts.reform.orgrolemapping.controller.BaseTestIntegration; +import uk.gov.hmcts.reform.orgrolemapping.controller.advice.exception.UnprocessableEntityException; +import uk.gov.hmcts.reform.orgrolemapping.data.RefreshJobEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SuppressWarnings("UnnecessaryLocalVariable") +class RefreshJobConfigServiceIntegrationTest extends BaseTestIntegration { + + private static final String JOB_CONFIG_1 = "LEGAL_OPERATIONS-PUBLICLAW-NEW-0"; + private static final String JOB_CONFIG_2 = "JUDICIAL-CIVIL-ABORTED-4"; + private static final String JOB_CONFIG_BAD = "BAD_JOB-INCORRECT_FORMAT-NOT_ENOUGH_PARTS"; + + private static final Long NEXT_JOB_ID = 5L; // NB: this is next sequence number in `sql/insert_refresh_jobs.sql` + + @Autowired + private PersistenceService persistenceService; + + @Autowired + private RefreshJobConfigService sut; + + @Test + @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:sql/insert_refresh_jobs.sql"}) + void testProcessJobDetail_singleJob_noJobId() { + + // GIVEN + String jobDetail = JOB_CONFIG_1; + + // WHEN + sut.processJobDetail(jobDetail, false); + + // THEN + var jobOutput = persistenceService.fetchRefreshJobById(NEXT_JOB_ID); + assertTrue(jobOutput.isPresent()); + assertJobEqualsJobConfig1(jobOutput.get()); + } + + @Test + @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:sql/insert_refresh_jobs.sql"}) + void testProcessJobDetail_singleJob_withJobId_new() { + + // GIVEN + String jobDetail = createJobConfigWithId(JOB_CONFIG_1, NEXT_JOB_ID); + + // WHEN + sut.processJobDetail(jobDetail, false); + + // THEN + var jobOutput = persistenceService.fetchRefreshJobById(NEXT_JOB_ID); + assertTrue(jobOutput.isPresent()); + assertEquals(NEXT_JOB_ID, jobOutput.get().getJobId()); + assertJobEqualsJobConfig1(jobOutput.get()); + } + + @Test + @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:sql/insert_refresh_jobs.sql"}) + void testProcessJobDetail_singleJob_withJobId_new_mismatched() { + + // GIVEN + String jobDetail = createJobConfigWithId(JOB_CONFIG_1, NEXT_JOB_ID + 1); + + // WHEN / THEN + UnprocessableEntityException exception = assertThrows(UnprocessableEntityException.class, () -> + sut.processJobDetail(jobDetail, false) + ); + + // THEN + assertTrue(exception.getMessage().startsWith(RefreshJobConfigService.ERROR_REJECTED_JOB_ID_MISMATCH)); + // verify new job config entry not created + assertTrue(persistenceService.fetchRefreshJobById(NEXT_JOB_ID).isEmpty()); // i.e. mismatched removed + assertTrue(persistenceService.fetchRefreshJobById(NEXT_JOB_ID + 1).isEmpty()); // requested not created + } + + @Test + @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:sql/insert_refresh_jobs.sql"}) + void testProcessJobDetail_multipleJobs_withJobId_updateAllowed() { + + // GIVEN + String jobDetail = createMultipleJobDetailsList( + createJobConfigWithId(JOB_CONFIG_1, 1L), // this is an update 1 < NEXT_JOB_ID + createJobConfigWithId(JOB_CONFIG_2, 2L) // this is an update 2 < NEXT_JOB_ID + ); + + // WHEN + sut.processJobDetail(jobDetail, true); // NB: allows update + + // THEN + // verify updated job is saved OK + var jobOutput1 = persistenceService.fetchRefreshJobById(1L); + assertTrue(jobOutput1.isPresent()); + assertEquals(1L, jobOutput1.get().getJobId()); + assertJobEqualsJobConfig1(jobOutput1.get()); + + // verify updated job is saved OK + var jobOutput2 = persistenceService.fetchRefreshJobById(2L); + assertTrue(jobOutput2.isPresent()); + assertEquals(2L, jobOutput2.get().getJobId()); + assertJobEqualsJobConfig2(jobOutput2.get()); + } + + @Test + @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:sql/insert_refresh_jobs.sql"}) + void testProcessJobDetail_multipleJobs_withJobId_noUpdateAllowed_includingSkipped() { + + // GIVEN + String jobDetail = createMultipleJobDetailsList( + createJobConfigWithId(JOB_CONFIG_1, 4L), // this update will be skipped + createJobConfigWithId(JOB_CONFIG_2, NEXT_JOB_ID) // this new job will be saved + ); + + // WHEN + sut.processJobDetail(jobDetail, false); + + // THEN + // verify update not applied + var jobOutput1 = persistenceService.fetchRefreshJobById(4L); + assertTrue(jobOutput1.isPresent()); + assertJobEqualsJob4Unchanged(jobOutput1.get()); + + // verify new job is saved OK + var jobOutput2 = persistenceService.fetchRefreshJobById(NEXT_JOB_ID); + assertTrue(jobOutput2.isPresent()); + assertEquals(NEXT_JOB_ID, jobOutput2.get().getJobId()); + assertJobEqualsJobConfig2(jobOutput2.get()); + } + + @Test + @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:sql/insert_refresh_jobs.sql"}) + void testProcessJobDetail_multipleJobs_badJob_noSaveOrUpdate() { + + // GIVEN + String jobDetail = createMultipleJobDetailsList( + createJobConfigWithId(JOB_CONFIG_1, 4L), // this is an update + createJobConfigWithId(JOB_CONFIG_2, NEXT_JOB_ID), // this is a new job + JOB_CONFIG_BAD + ); + + // WHEN / THEN + assertThrows(UnprocessableEntityException.class, () -> + sut.processJobDetail(jobDetail, true) // NB: allows update + ); + + // THEN + // verify update has been rolled back + var jobOutput1 = persistenceService.fetchRefreshJobById(4L); + assertTrue(jobOutput1.isPresent()); + assertJobEqualsJob4Unchanged(jobOutput1.get()); + + // verify new job has been rolled back + assertTrue(persistenceService.fetchRefreshJobById(NEXT_JOB_ID).isEmpty()); + } + + private void assertJobEqualsJobConfig1(RefreshJobEntity job) { + // assert entries match: JOB_CONFIG_1 + assertEquals("LEGAL_OPERATIONS", job.getRoleCategory()); + assertEquals("PUBLICLAW", job.getJurisdiction()); + assertEquals("NEW", job.getStatus()); + assertEquals(0, job.getLinkedJobId()); + } + + private void assertJobEqualsJobConfig2(RefreshJobEntity job) { + // assert entries match: JOB_CONFIG_2 + assertEquals("JUDICIAL", job.getRoleCategory()); + assertEquals("CIVIL", job.getJurisdiction()); + assertEquals("ABORTED", job.getStatus()); + assertEquals(4, job.getLinkedJobId()); + } + + private void assertJobEqualsJob4Unchanged(RefreshJobEntity job) { + // verify update not applied: i.e. must match jobId=4 in `sql/insert_refresh_jobs.sql` + assertEquals(4L, job.getJobId()); + assertEquals("LEGAL_OPERATIONS", job.getRoleCategory()); + assertEquals("IA", job.getJurisdiction()); + assertEquals("COMPLETED", job.getStatus()); + assertEquals(2, job.getLinkedJobId()); + } + + private String createJobConfigWithId(String jobConfig, Long jobId) { + return jobConfig + RefreshJobConfigService.REFRESH_JOBS_CONFIG_SPLITTER + jobId; + } + + private String createMultipleJobDetailsList(String... jobConfigs) { + return String.join(RefreshJobConfigService.REFRESH_JOBS_DETAILS_SPLITTER, jobConfigs); + } + +} \ No newline at end of file diff --git a/src/integrationTest/resources/sql/insert_refresh_jobs.sql b/src/integrationTest/resources/sql/insert_refresh_jobs.sql new file mode 100644 index 000000000..104fb3774 --- /dev/null +++ b/src/integrationTest/resources/sql/insert_refresh_jobs.sql @@ -0,0 +1,12 @@ +DELETE FROM refresh_jobs; + +INSERT INTO public.refresh_jobs (job_id, role_category, jurisdiction, status, user_ids, linked_job_id, created) +VALUES(1, 'LEGAL_OPERATIONS', 'IA', 'NEW', NULL, 0, NULL); +INSERT INTO public.refresh_jobs (job_id, role_category, jurisdiction, status, user_ids, linked_job_id, created) +VALUES(2, 'LEGAL_OPERATIONS', 'IA', 'ABORTED', '{"7c12a4bc-450e-4290-8063-b387a5d5e0b7"}', NULL, NULL); +INSERT INTO public.refresh_jobs (job_id, role_category, jurisdiction, status, user_ids, linked_job_id, created) +VALUES(3, 'LEGAL_OPERATIONS', 'IA', 'NEW', NULL, 2, NULL); +INSERT INTO public.refresh_jobs (job_id, role_category, jurisdiction, status, user_ids, linked_job_id, created) +VALUES(4, 'LEGAL_OPERATIONS', 'IA', 'COMPLETED', NULL, 2, NULL); + +ALTER SEQUENCE JOB_ID_SEQ RESTART WITH 5; diff --git a/src/main/java/uk/gov/hmcts/reform/orgrolemapping/config/JobConfiguration.java b/src/main/java/uk/gov/hmcts/reform/orgrolemapping/config/JobConfiguration.java index 3fae07a2f..29ae2f567 100644 --- a/src/main/java/uk/gov/hmcts/reform/orgrolemapping/config/JobConfiguration.java +++ b/src/main/java/uk/gov/hmcts/reform/orgrolemapping/config/JobConfiguration.java @@ -1,67 +1,58 @@ package uk.gov.hmcts.reform.orgrolemapping.config; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; -import uk.gov.hmcts.reform.orgrolemapping.data.RefreshJobEntity; -import uk.gov.hmcts.reform.orgrolemapping.data.RefreshJobsRepository; +import uk.gov.hmcts.reform.orgrolemapping.controller.advice.exception.UnprocessableEntityException; +import uk.gov.hmcts.reform.orgrolemapping.domain.service.RefreshJobConfigService; import uk.gov.hmcts.reform.orgrolemapping.launchdarkly.FeatureConditionEvaluator; -import java.time.ZonedDateTime; -import java.util.Optional; - @Component @Slf4j public class JobConfiguration implements CommandLineRunner { - private final RefreshJobsRepository refreshJobsRepository; + static final String ERROR_ABORTED_JOB_IMPORT = "Aborted the job configuration import: {}"; + + private final RefreshJobConfigService refreshJobConfigService; private final FeatureConditionEvaluator featureConditionEvaluator; private final String jobDetail; + private final boolean jobDetailAllowUpdate; @Autowired - public JobConfiguration(RefreshJobsRepository refreshJobsRepository, + public JobConfiguration(RefreshJobConfigService refreshJobConfigService, @Value("${refresh.job.update}") String jobDetail, + @Value("${refresh.Job.updateOverride}") Boolean jobDetailAllowUpdate, FeatureConditionEvaluator featureConditionEvaluator ) { - this.refreshJobsRepository = refreshJobsRepository; + this.refreshJobConfigService = refreshJobConfigService; this.featureConditionEvaluator = featureConditionEvaluator; this.jobDetail = jobDetail; + this.jobDetailAllowUpdate = BooleanUtils.isTrue(jobDetailAllowUpdate); } @Override public void run(String... args) { - if (StringUtils.isNotEmpty(jobDetail) && featureConditionEvaluator - .isFlagEnabled("am_org_role_mapping_service", "orm-refresh-job-enable")) { - String[] jobAttributes = jobDetail.split("-"); - log.info("Job {} inserting into refresh table", jobDetail); - if (jobAttributes.length < 4) { - return; - } - RefreshJobEntity refreshJobEntity = RefreshJobEntity.builder().build(); - if (jobAttributes.length > 4) { - Optional refreshJob = refreshJobsRepository.findById(Long.valueOf(jobAttributes[4])); - refreshJobEntity = refreshJob.orElse(refreshJobEntity); - } - refreshJobEntity.setRoleCategory(jobAttributes[0]); - refreshJobEntity.setJurisdiction(jobAttributes[1]); - refreshJobEntity.setStatus(jobAttributes[2]); - refreshJobEntity.setLinkedJobId(Long.valueOf(jobAttributes[3])); - refreshJobEntity.setCreated(ZonedDateTime.now()); - - persistJobDetail(refreshJobEntity); + if (StringUtils.isNotEmpty(jobDetail)) { + if (featureConditionEvaluator.isFlagEnabled("am_org_role_mapping_service", "orm-refresh-job-enable")) { + try { + this.refreshJobConfigService.processJobDetail(this.jobDetail, this.jobDetailAllowUpdate); + } catch (UnprocessableEntityException ex) { + log.error(ERROR_ABORTED_JOB_IMPORT, ex.getMessage(), ex); + } + } else { + log.warn("LD flag 'orm-refresh-job-enable' is not enabled"); + } } else { - log.warn("LD flag 'orm-refresh-job-enable' is not enabled"); + log.info("No Job Configuration to create"); } } - private void persistJobDetail(RefreshJobEntity refreshJobEntity) { - refreshJobsRepository.save(refreshJobEntity); - } } diff --git a/src/main/java/uk/gov/hmcts/reform/orgrolemapping/domain/service/RefreshJobConfigService.java b/src/main/java/uk/gov/hmcts/reform/orgrolemapping/domain/service/RefreshJobConfigService.java new file mode 100644 index 000000000..625781770 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/orgrolemapping/domain/service/RefreshJobConfigService.java @@ -0,0 +1,142 @@ +package uk.gov.hmcts.reform.orgrolemapping.domain.service; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.math.NumberUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import uk.gov.hmcts.reform.orgrolemapping.controller.advice.exception.UnprocessableEntityException; +import uk.gov.hmcts.reform.orgrolemapping.data.RefreshJobEntity; + +import java.time.ZonedDateTime; +import java.util.Objects; +import java.util.Optional; + +@Service +@Slf4j +public class RefreshJobConfigService { + + static final String REFRESH_JOBS_DETAILS_SPLITTER = ":"; // splitting multiple job configs from one job detail + static final String REFRESH_JOBS_CONFIG_SPLITTER = "-"; // splitting individual job config into attributes + + private static final String ERROR_MESSAGE = "%s: '%s'"; + static final String ERROR_JOBS_DETAILS_TOO_FEW_PARTS = "Job config does not have enough arguments"; + static final String ERROR_JOBS_DETAILS_TOO_MANY_PARTS = "Job config has too many arguments"; + static final String ERROR_JOB_ID_NON_NUMERIC = "Job ID is non-numeric"; + static final String ERROR_LINKED_JOB_ID_NON_NUMERIC = "Linked Job ID is non-numeric"; + static final String ERROR_REJECTED_JOB_ID_MISMATCH = "Job ID does not match requested value"; + + private static final int ARGS_INDEX_0_ROLE_CATEGORY = 0; + private static final int ARGS_INDEX_1_JURISDICTION = 1; + private static final int ARGS_INDEX_2_STATUS = 2; + private static final int ARGS_INDEX_3_LINKED_JOB_ID = 3; + private static final int ARGS_INDEX_4_JOB_ID = 4; + + private final PersistenceService persistenceService; + + public RefreshJobConfigService(PersistenceService persistenceService) { + this.persistenceService = persistenceService; + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void processJobDetail(String jobDetail, boolean allowUpdate) { + + if (StringUtils.isNotEmpty(jobDetail)) { + + String[] refreshJobsConfig = jobDetail.split(REFRESH_JOBS_DETAILS_SPLITTER); + for (String refreshJobConfig:refreshJobsConfig) { + String[] refreshJobAttributes = splitJobConfigInToArguments(refreshJobConfig); + processJobConfig(refreshJobConfig, refreshJobAttributes, allowUpdate); + } + } + } + + private void processJobConfig(String refreshJobConfig, + String[] refreshJobAttributes, + boolean allowUpdate) { + + RefreshJobEntity refreshJobEntity = RefreshJobEntity.builder() + .created(ZonedDateTime.now()) // set created only if new job + .build(); + + Long useJobId = null; + + if (refreshJobAttributes.length > 4) { + useJobId = Long.valueOf(refreshJobAttributes[ARGS_INDEX_4_JOB_ID]); + Optional refreshJob = persistenceService.fetchRefreshJobById(useJobId); + + if (refreshJob.isPresent()) { + if (allowUpdate) { + log.warn("UPDATING Job {} as JOB_ID={} already present in database.", refreshJobConfig, useJobId); + refreshJobEntity = refreshJob.get(); // switch to using existing job + } else { + // NB: default config will not allow update of existing job entities + log.warn("SKIPPING Job {} as JOB_ID={} already present in database.", refreshJobConfig, useJobId); + return; // i.e. abort the processing of this config + } + } + } + + if (refreshJobEntity.getJobId() == null) { + log.info("INSERTING Job {} into refresh table", refreshJobConfig); + } + + // NB: we do not set jobId as for new records it will always match the next sequence number. + refreshJobEntity.setRoleCategory(refreshJobAttributes[ARGS_INDEX_0_ROLE_CATEGORY]); + refreshJobEntity.setJurisdiction(refreshJobAttributes[ARGS_INDEX_1_JURISDICTION]); + refreshJobEntity.setStatus(refreshJobAttributes[ARGS_INDEX_2_STATUS]); + refreshJobEntity.setLinkedJobId(Long.valueOf(refreshJobAttributes[ARGS_INDEX_3_LINKED_JOB_ID])); + + RefreshJobEntity savedRefreshJob = persistenceService.persistRefreshJob(refreshJobEntity); + + // reject if job ID after save doesn't match the expected value + if (useJobId != null && !Objects.equals(useJobId, savedRefreshJob.getJobId())) { + throw new UnprocessableEntityException( + String.format("%s: %s != %s", ERROR_REJECTED_JOB_ID_MISMATCH, useJobId, savedRefreshJob.getJobId()) + ); + } + } + + private String[] splitJobConfigInToArguments(String refreshJobConfig) { + + String[] refreshJobAttributes = refreshJobConfig.split(REFRESH_JOBS_CONFIG_SPLITTER); + + if (refreshJobAttributes.length < 4) { + throw new UnprocessableEntityException( + String.format(ERROR_MESSAGE, ERROR_JOBS_DETAILS_TOO_FEW_PARTS, refreshJobConfig) + ); + } else if (refreshJobAttributes.length > 5) { + throw new UnprocessableEntityException( + String.format(ERROR_MESSAGE, ERROR_JOBS_DETAILS_TOO_MANY_PARTS, refreshJobConfig) + ); + } else { + + // validate Job-ID is numeric + if (refreshJobAttributes.length == 5 + && (!NumberUtils.isNumber(refreshJobAttributes[ARGS_INDEX_4_JOB_ID]))) { + throw new UnprocessableEntityException( + String.format( + ERROR_MESSAGE, + ERROR_JOB_ID_NON_NUMERIC, + refreshJobAttributes[ARGS_INDEX_4_JOB_ID] + ) + ); + } + + // validate Linked Job-ID is numeric + if (!NumberUtils.isNumber(refreshJobAttributes[ARGS_INDEX_3_LINKED_JOB_ID])) { + throw new UnprocessableEntityException( + String.format( + ERROR_MESSAGE, + ERROR_LINKED_JOB_ID_NON_NUMERIC, + refreshJobAttributes[ARGS_INDEX_3_LINKED_JOB_ID] + ) + ); + } + } + + return refreshJobAttributes; + } + +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index d921fb4cc..45f3f47c6 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -166,6 +166,7 @@ launchdarkly: refresh: Job: update: ${REFRESH_JOB:} + updateOverride: ${REFRESH_JOB_ALLOW_UPDATE:false} pageSize: ${REFRESH_JOB_PAGE_SIZE:400} sortDirection: ${REFRESH_JOB_SORT_DIR:ASC} sortColumn: ${REFRESH_JOB_SORT_COL:} diff --git a/src/test/java/uk/gov/hmcts/reform/orgrolemapping/config/JobConfigurationTest.java b/src/test/java/uk/gov/hmcts/reform/orgrolemapping/config/JobConfigurationTest.java new file mode 100644 index 000000000..748e8c814 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/orgrolemapping/config/JobConfigurationTest.java @@ -0,0 +1,131 @@ +package uk.gov.hmcts.reform.orgrolemapping.config; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.LoggerFactory; +import uk.gov.hmcts.reform.orgrolemapping.controller.advice.exception.UnprocessableEntityException; +import uk.gov.hmcts.reform.orgrolemapping.domain.service.RefreshJobConfigService; +import uk.gov.hmcts.reform.orgrolemapping.launchdarkly.FeatureConditionEvaluator; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SuppressWarnings("UnnecessaryLocalVariable") +@ExtendWith(MockitoExtension.class) +class JobConfigurationTest { + + private static final String JOB_CONFIG = "LEGAL_OPERATIONS-PUBLICLAW-NEW-0-11"; + + @Mock(lenient = true) + private FeatureConditionEvaluator featureConditionEvaluator; + + @Mock + private RefreshJobConfigService refreshJobConfigService; + + Logger logger; + ListAppender listAppender; + List logsList; + + @BeforeEach + public void setUp() { + // attach appender to logger for assertions + logger = (Logger) LoggerFactory.getLogger(JobConfiguration.class); + listAppender = new ListAppender<>(); + listAppender.start(); + logger.addAppender(listAppender); + logsList = listAppender.list; + } + + @Test + void testRun_featureFlagDisabled() { + + // GIVEN + setUpFeatureFlag(false); + String jobDetail = JOB_CONFIG; + JobConfiguration jobConfigurationRunner = new JobConfiguration(refreshJobConfigService, + jobDetail, false, featureConditionEvaluator); + + // WHEN + jobConfigurationRunner.run("input.txt"); + + // THEN + verify(refreshJobConfigService, never()).processJobDetail(any(), anyBoolean()); + } + + @ParameterizedTest + @NullAndEmptySource + void testRun_jobDetailEmptyOrNull(String jobDetail) { + + // GIVEN + setUpFeatureFlag(true); + JobConfiguration jobConfigurationRunner = new JobConfiguration(refreshJobConfigService, + jobDetail, false, featureConditionEvaluator); + + // WHEN + jobConfigurationRunner.run("input.txt"); + + // THEN + verify(refreshJobConfigService, never()).processJobDetail(any(), anyBoolean()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testRun_withJobDetails(boolean allowUpdate) { + + // GIVEN + setUpFeatureFlag(true); + String jobDetail = JOB_CONFIG; + JobConfiguration jobConfigurationRunner = new JobConfiguration(refreshJobConfigService, + jobDetail, allowUpdate, featureConditionEvaluator); + + // WHEN + jobConfigurationRunner.run("input.txt"); + + // THEN + verify(refreshJobConfigService, atLeastOnce()).processJobDetail(jobDetail, allowUpdate); + } + + @Test + void testRun_withJobDetailsException() { + + // GIVEN + setUpFeatureFlag(true); + String jobDetail = JOB_CONFIG; + boolean allowUpdate = false; + JobConfiguration jobConfigurationRunner = new JobConfiguration(refreshJobConfigService, + jobDetail, allowUpdate, featureConditionEvaluator); + Mockito.doThrow(new UnprocessableEntityException("AN ERROR")) + .when(refreshJobConfigService).processJobDetail(jobDetail, allowUpdate); + + // WHEN + jobConfigurationRunner.run("input.txt"); + + // THEN + verify(refreshJobConfigService, atLeastOnce()).processJobDetail(jobDetail, allowUpdate); + + assertEquals(JobConfiguration.ERROR_ABORTED_JOB_IMPORT, logsList.get(0).getMessage()); + } + + private void setUpFeatureFlag(boolean enabled) { + when(featureConditionEvaluator.isFlagEnabled("am_org_role_mapping_service", "orm-refresh-job-enable")) + .thenReturn(enabled); + } + +} diff --git a/src/test/java/uk/gov/hmcts/reform/orgrolemapping/domain/service/RefreshJobConfigServiceTest.java b/src/test/java/uk/gov/hmcts/reform/orgrolemapping/domain/service/RefreshJobConfigServiceTest.java new file mode 100644 index 000000000..fecc722ba --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/orgrolemapping/domain/service/RefreshJobConfigServiceTest.java @@ -0,0 +1,303 @@ +package uk.gov.hmcts.reform.orgrolemapping.domain.service; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.hmcts.reform.orgrolemapping.controller.advice.exception.UnprocessableEntityException; +import uk.gov.hmcts.reform.orgrolemapping.data.RefreshJobEntity; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SuppressWarnings("UnnecessaryLocalVariable") +@ExtendWith(MockitoExtension.class) +class RefreshJobConfigServiceTest { + + @Mock + private PersistenceService persistenceService; + + @InjectMocks + private RefreshJobConfigService sut; + + private static final String JOB_CONFIG_11 = "LEGAL_OPERATIONS-PUBLICLAW-NEW-0-11"; + private static final String JOB_CONFIG_12 = "JUDICIAL-CIVIL-ABORTED-4-12"; + private static final String JOB_CONFIG_BAD = "BAD_JOB-INCORRECT_FORMAT-NOT_ENOUGH_PARTS"; + + @ParameterizedTest + @NullAndEmptySource + void testProcessJobDetail_jobDetailEmptyOrNull(String jobDetail) { + + // WHEN + sut.processJobDetail(jobDetail, false); + + // THEN + verify(persistenceService, never()).persistRefreshJob(any()); + } + + @Test + void testProcessJobDetail_singleJob_noJobId() { + + // GIVEN + setUpSaveMock(5L); // i.e. any new job ID + + String jobDetail = "LEGAL_OPERATIONS-PUBLICLAW-NEW-0"; + + // WHEN + sut.processJobDetail(jobDetail, false); + + // THEN + verify(persistenceService, never()).fetchRefreshJobById(anyLong()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RefreshJobEntity.class); + verify(persistenceService, times(1)).persistRefreshJob(captor.capture()); + RefreshJobEntity job = captor.getValue(); + assertEquals("LEGAL_OPERATIONS", job.getRoleCategory()); + assertEquals("PUBLICLAW", job.getJurisdiction()); + assertEquals("NEW", job.getStatus()); + assertEquals(0, job.getLinkedJobId()); + assertNull(job.getJobId()); + } + + @Test + void testProcessJobDetail_singleJob_withJobId_new() { + + // GIVEN + when(persistenceService.fetchRefreshJobById(11L)).thenReturn(Optional.empty()); + setUpSaveMock(11L); // i.e. matching expected job ID + + String jobDetail = JOB_CONFIG_11; + + // WHEN + sut.processJobDetail(jobDetail, false); + + // THEN + ArgumentCaptor captor = ArgumentCaptor.forClass(RefreshJobEntity.class); + verify(persistenceService, times(1)).persistRefreshJob(captor.capture()); + assertJobEqualsJob11(captor.getValue(), true); + } + + @Test + void testProcessJobDetail_singleJob_withJobId_new_mismatched() { + + // GIVEN + when(persistenceService.fetchRefreshJobById(11L)).thenReturn(Optional.empty()); + setUpSaveMock(6L); // i.e. any new job ID + + String jobDetail = JOB_CONFIG_11; + + // WHEN / THEN + UnprocessableEntityException exception = assertThrows(UnprocessableEntityException.class, () -> + sut.processJobDetail(jobDetail, false) + ); + + // THEN + assertTrue(exception.getMessage().startsWith(RefreshJobConfigService.ERROR_REJECTED_JOB_ID_MISMATCH)); + } + + @Test + void testProcessJobDetail_multipleJobs_withJobId_updateAllowed() { + + // GIVEN + RefreshJobEntity job11 = RefreshJobEntity.builder().jobId(11L).build(); + RefreshJobEntity job12 = RefreshJobEntity.builder().jobId(12L).build(); + when(persistenceService.fetchRefreshJobById(11L)).thenReturn(Optional.ofNullable(job11)); + when(persistenceService.fetchRefreshJobById(12L)).thenReturn(Optional.ofNullable(job12)); + setUpSaveMock(job11); + setUpSaveMock(job12); + + String jobDetail = createMultipleJobDetailsList(JOB_CONFIG_11, JOB_CONFIG_12); + + // WHEN + sut.processJobDetail(jobDetail, true); // NB: allows update + + // THEN + assert job11 != null; + verify(persistenceService, times(1)).persistRefreshJob(job11); + assertJobEqualsJob11(job11, false); // i.e. check updated values have been saved + + assert job12 != null; + verify(persistenceService, times(1)).persistRefreshJob(job12); + assertJobEqualsJob12(job12, false); // i.e. check updated values have been saved + } + + @Test + void testProcessJobDetail_multipleJobs_withJobId_noUpdateAllowed_includingSkipped() { + + // GIVEN + RefreshJobEntity job11 = RefreshJobEntity.builder().jobId(11L).build(); + when(persistenceService.fetchRefreshJobById(11L)).thenReturn(Optional.ofNullable(job11)); + when(persistenceService.fetchRefreshJobById(12L)).thenReturn(Optional.empty()); // NB: i.e. not found + setUpSaveMock(12L); // i.e. next JOB ID is as expected + + String jobDetail = createMultipleJobDetailsList(JOB_CONFIG_11, JOB_CONFIG_12); + + // WHEN + sut.processJobDetail(jobDetail, false); + + // THEN + // verify both job ID lookups ran + verify(persistenceService, times(1)).fetchRefreshJobById(11L); + verify(persistenceService, times(1)).fetchRefreshJobById(12L); + + // verify only 1 save + ArgumentCaptor captor = ArgumentCaptor.forClass(RefreshJobEntity.class); + // NB: save will only be called once (i.e. job 12) as job 11 already exists and NO UPDATE ALLOWED specified + verify(persistenceService, times(1)).persistRefreshJob(captor.capture()); + assertJobEqualsJob12(captor.getValue(), true); + + assert job11 != null; + verify(persistenceService, never()).persistRefreshJob(job11); // never called as NO UPDATE ALLOWED specified + } + + @Test + void testProcessJobDetail_badJob_singleJob_tooFewParts() { + + // GIVEN + String jobDetail = JOB_CONFIG_BAD; + + + // WHEN / THEN + UnprocessableEntityException exception = assertThrows(UnprocessableEntityException.class, () -> + sut.processJobDetail(jobDetail, false) + ); + + // THEN + assertTrue(exception.getMessage().startsWith(RefreshJobConfigService.ERROR_JOBS_DETAILS_TOO_FEW_PARTS)); + verify(persistenceService, never()).fetchRefreshJobById(anyLong()); + verify(persistenceService, never()).persistRefreshJob(any()); + } + + @Test + void testProcessJobDetail_badJob_singleJob_tooManyParts() { + + // GIVEN + String jobDetail = JOB_CONFIG_11 + RefreshJobConfigService.REFRESH_JOBS_CONFIG_SPLITTER + "extraArgument"; + + // WHEN / THEN + UnprocessableEntityException exception = assertThrows(UnprocessableEntityException.class, () -> + sut.processJobDetail(jobDetail, false) + ); + + // THEN + assertTrue(exception.getMessage().startsWith(RefreshJobConfigService.ERROR_JOBS_DETAILS_TOO_MANY_PARTS)); + verify(persistenceService, never()).fetchRefreshJobById(anyLong()); + verify(persistenceService, never()).persistRefreshJob(any()); + } + + @Test + void testProcessJobDetail_badJob_singleJob_badLinkedJobId() { + + // GIVEN + String jobDetail = "LEGAL_OPERATIONS-PUBLICLAW-NEW-nonNumeric-11"; + + // WHEN / THEN + UnprocessableEntityException exception = assertThrows(UnprocessableEntityException.class, () -> + sut.processJobDetail(jobDetail, false) + ); + + // THEN + assertTrue(exception.getMessage().startsWith(RefreshJobConfigService.ERROR_LINKED_JOB_ID_NON_NUMERIC)); + verify(persistenceService, never()).fetchRefreshJobById(anyLong()); + verify(persistenceService, never()).persistRefreshJob(any()); + } + + @Test + void testProcessJobDetail_badJob_singleJob_badJobId() { + + // GIVEN + String jobDetail = "LEGAL_OPERATIONS-PUBLICLAW-NEW-0-nonNumeric"; + + // WHEN / THEN + UnprocessableEntityException exception = assertThrows(UnprocessableEntityException.class, () -> + sut.processJobDetail(jobDetail, false) + ); + + // THEN + assertTrue(exception.getMessage().startsWith(RefreshJobConfigService.ERROR_JOB_ID_NON_NUMERIC)); + verify(persistenceService, never()).fetchRefreshJobById(anyLong()); + verify(persistenceService, never()).persistRefreshJob(any()); + } + + @Test + void testProcessJobDetail_badJob_multipleJob_1BadSoProcessStops() { + + // GIVEN + String jobDetail = createMultipleJobDetailsList(JOB_CONFIG_11, JOB_CONFIG_BAD, JOB_CONFIG_12); + when(persistenceService.fetchRefreshJobById(11L)).thenReturn(Optional.empty()); // NB: i.e. not found + setUpSaveMock(11L); // i.e. next JOB ID is as expected + + // WHEN / THEN + assertThrows(UnprocessableEntityException.class, () -> + sut.processJobDetail(jobDetail, false) + ); + + // THEN + // NB: verify process has stopped: i.e. last job not processed as aborted before + verify(persistenceService, never()).fetchRefreshJobById(12L); + } + + private void assertJobEqualsJob11(RefreshJobEntity job, boolean newJob) { + // assert entries match: JOB_CONFIG_11 + assertEquals("LEGAL_OPERATIONS", job.getRoleCategory()); + assertEquals("PUBLICLAW", job.getJurisdiction()); + assertEquals("NEW", job.getStatus()); + assertEquals(0, job.getLinkedJobId()); + if (!newJob) { + assertEquals(11L, job.getJobId()); + } else { + assertNull(job.getJobId()); + } + } + + private void assertJobEqualsJob12(RefreshJobEntity job, boolean newJob) { + // assert entries match: JOB_CONFIG_12 + assertEquals("JUDICIAL", job.getRoleCategory()); + assertEquals("CIVIL", job.getJurisdiction()); + assertEquals("ABORTED", job.getStatus()); + assertEquals(4, job.getLinkedJobId()); + if (!newJob) { + assertEquals(12L, job.getJobId()); + } else { + assertNull(job.getJobId()); + } + } + + private String createMultipleJobDetailsList(String... jobConfigs) { + return String.join(RefreshJobConfigService.REFRESH_JOBS_DETAILS_SPLITTER, jobConfigs); + } + + private void setUpSaveMock(RefreshJobEntity job) { + // save existing job: so just echo it back + setUpSaveMock(job, false, job.getJobId()); + } + + private void setUpSaveMock(Long nextJobId) { + // NB: new job test + setUpSaveMock(any(RefreshJobEntity.class), true, nextJobId); + } + + private void setUpSaveMock(RefreshJobEntity job, boolean newJob, Long nextJobId) { + RefreshJobEntity savedJob = job; + if (newJob) { + // return a new job with new ID + savedJob = RefreshJobEntity.builder().jobId(nextJobId).build(); + } + when(persistenceService.persistRefreshJob(job)).thenReturn(savedJob); + } + +}