package com.autovoucher.job; import com.autovoucher.config.AppProperties; import com.autovoucher.domain.PushTask; import com.autovoucher.repository.PushTaskRepository; import com.autovoucher.service.PushService; import org.junit.jupiter.api.Test; import org.mockito.InOrder; import java.time.LocalDateTime; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; class NccPushRunnerTest { private final AppProperties properties = new AppProperties(); private final PushTaskRepository pushTaskRepository = mock(PushTaskRepository.class); private final PushService pushService = mock(PushService.class); private final NccPushRunner runner = new NccPushRunner(properties, pushTaskRepository, pushService); @Test void disabledPushSkipsAllRepositoryWork() { properties.getPush().setEnabled(false); NccPushRunner.Result result = runner.runDue(1L, 20); assertThat(result.enabled()).isFalse(); assertThat(result.failedTaskIds()).isEmpty(); verifyNoInteractions(pushTaskRepository, pushService); } @Test void reconcilesUnknownTasksBeforePushingPendingOnes() { PushTask unknown = task(1L); PushTask due = task(2L); when(pushTaskRepository.findDueTasks(eq(1L), eq("NCC"), eq(List.of("UNKNOWN")), any())) .thenReturn(List.of(unknown)); when(pushTaskRepository.findDueTasks(eq(1L), eq("NCC"), eq(List.of("PENDING", "RETRY_WAIT")), any())) .thenReturn(List.of(due)); NccPushRunner.Result result = runner.runDue(1L, 20); assertThat(result.enabled()).isTrue(); assertThat(result.reconciled()).isEqualTo(1); assertThat(result.pushed()).isEqualTo(1); assertThat(result.failed()).isZero(); assertThat(result.partialFailure()).isFalse(); assertThat(result.allAttemptsFailed()).isFalse(); InOrder order = inOrder(pushService); order.verify(pushService).reconcile(1L); order.verify(pushService).push(2L, null); } @Test void oneFailingTaskDoesNotStopTheRest() { PushTask badReconcile = task(1L); PushTask badPush = task(2L); PushTask goodPush = task(3L); when(pushTaskRepository.findDueTasks(eq(1L), eq("NCC"), eq(List.of("UNKNOWN")), any())) .thenReturn(List.of(badReconcile)); when(pushTaskRepository.findDueTasks(eq(1L), eq("NCC"), eq(List.of("PENDING", "RETRY_WAIT")), any())) .thenReturn(List.of(badPush, goodPush)); when(pushService.reconcile(1L)).thenThrow(new IllegalStateException("reconcile boom")); doThrow(new IllegalStateException("push boom")).when(pushService).push(2L, null); NccPushRunner.Result result = runner.runDue(1L, 20); assertThat(result.reconciled()).isZero(); assertThat(result.pushed()).isEqualTo(1); assertThat(result.failed()).isEqualTo(2); assertThat(result.failedTaskIds()).containsExactly(1L, 2L); assertThat(result.partialFailure()).isTrue(); assertThat(result.allAttemptsFailed()).isFalse(); verify(pushService).push(3L, null); } @Test void allAttemptsFailingIsReportedAsTotalFailure() { when(pushTaskRepository.findDueTasks(eq(1L), eq("NCC"), eq(List.of("UNKNOWN")), any())) .thenReturn(List.of()); when(pushTaskRepository.findDueTasks(eq(1L), eq("NCC"), eq(List.of("PENDING", "RETRY_WAIT")), any())) .thenReturn(List.of(task(2L))); doThrow(new IllegalStateException("push boom")).when(pushService).push(2L, null); NccPushRunner.Result result = runner.runDue(1L, 20); assertThat(result.failed()).isEqualTo(1); assertThat(result.allAttemptsFailed()).isTrue(); assertThat(result.partialFailure()).isFalse(); } @Test void batchSizeLimitsBothReconcileAndPushRounds() { when(pushTaskRepository.findDueTasks(eq(1L), eq("NCC"), eq(List.of("UNKNOWN")), any())) .thenReturn(List.of(task(1L), task(2L), task(3L))); when(pushTaskRepository.findDueTasks(eq(1L), eq("NCC"), eq(List.of("PENDING", "RETRY_WAIT")), any())) .thenReturn(List.of(task(4L), task(5L), task(6L))); NccPushRunner.Result result = runner.runDue(1L, 2); assertThat(result.reconciled()).isEqualTo(2); assertThat(result.pushed()).isEqualTo(2); verify(pushService).reconcile(1L); verify(pushService).reconcile(2L); verify(pushService, never()).reconcile(3L); verify(pushService).push(4L, null); verify(pushService).push(5L, null); verify(pushService, never()).push(6L, null); } @Test void nonPositiveBatchSizeStillProcessesOneTask() { when(pushTaskRepository.findDueTasks(eq(1L), eq("NCC"), eq(List.of("UNKNOWN")), any())) .thenReturn(List.of()); when(pushTaskRepository.findDueTasks(eq(1L), eq("NCC"), eq(List.of("PENDING", "RETRY_WAIT")), any())) .thenReturn(List.of(task(4L), task(5L))); NccPushRunner.Result result = runner.runDue(1L, 0); assertThat(result.pushed()).isEqualTo(1); verify(pushService, times(1)).push(4L, null); verify(pushService, never()).push(5L, null); } private PushTask task(Long id) { PushTask task = new PushTask(); task.setId(id); task.setTenantId(1L); task.setTargetSystem("NCC"); task.setStatus("PENDING"); task.setNextRetryAt(LocalDateTime.now().minusMinutes(1)); return task; } }