package com.malk.timer; import com.malk.server.aliwork.YDConf; import com.malk.server.aliwork.YDParam; import com.malk.server.dingtalk.DDR_New; import com.malk.server.workhours.ApprovalWriteBackResult; import com.malk.service.workhours.WorkHoursCalcService; import com.malk.server.workhours.WHConf; import com.malk.service.aliwork.YDClient; import com.malk.service.workhours.ApprovalWriteBackService; import com.malk.service.workhours.BackendExceptionLogService; import org.junit.Test; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.test.util.ReflectionTestUtils; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; public class WorkHoursTimerScheduleTest { @Test public void workHoursIncrementalTimerShouldRunAtThreeThirtyAndTwelveFortyFive() throws Exception { Method method = WorkHoursCalcTimer.class.getMethod("calcDailyIncrementalSync"); assertEquals(setOf("0 30 3 * * ?", "0 45 12 * * ?"), scheduledCrons(method)); } @Test public void workHoursIncrementalTimerShouldUseSevenDayRecoveryWindow() { WorkHoursCalcService service = mock(WorkHoursCalcService.class); WorkHoursCalcTimer timer = new WorkHoursCalcTimer(); ReflectionTestUtils.setField(timer, "workHoursCalcService", service); timer.calcDailyIncrementalSync(); verify(service).incrementalSync(7); } @Test public void workHoursTimerShouldRespectGlobalSchedulingSwitch() { ConditionalOnProperty condition = WorkHoursCalcTimer.class.getAnnotation(ConditionalOnProperty.class); assertEquals("enable", condition.prefix()); assertEquals(Arrays.asList("scheduling"), Arrays.asList(condition.name())); assertEquals("true", condition.havingValue()); assertEquals(false, condition.matchIfMissing()); } @Test public void projectChangeTimerShouldRunFifteenMinutesBeforeWorkHoursTimer() throws Exception { Method method = ProjectChangeSyncTimer.class.getMethod("syncProjectChanges"); assertEquals(setOf("0 15 3 * * ?", "0 30 12 * * ?"), scheduledCrons(method)); } @Test public void projectChangeTimerShouldRequireExplicitEnablement() { ConditionalOnProperty condition = ProjectChangeSyncTimer.class.getAnnotation(ConditionalOnProperty.class); assertEquals("workhours", condition.prefix()); assertEquals(Arrays.asList("projectChangeSyncEnabled"), Arrays.asList(condition.name())); assertEquals("true", condition.havingValue()); assertEquals(false, condition.matchIfMissing()); } @Test public void approvalWriteBackShouldRunBeforeApprovalResubmit() throws Exception { Method writeBack = ApprovalWriteBackTimer.class.getMethod("scanAndWriteBack"); Method resubmit = ReSubmitApprovalTimer.class.getMethod("scheduledMorningResubmit"); Method noonResubmit = ReSubmitApprovalTimer.class.getMethod("scheduledAfternoonResubmit"); assertEquals(setOf("0 20 4 * * ?", "0 20 12 * * ?"), scheduledCrons(writeBack)); assertEquals(setOf("0 0 7 * * ?"), scheduledCrons(resubmit)); assertEquals(setOf("0 0 16 * * ?"), scheduledCrons(noonResubmit)); } @Test public void approvalResubmitShouldDefaultToSevenDayRecoveryWindow() { assertEquals(7, new WHConf().getApprovalResubmitModifiedLookbackDays()); } @Test public void approvalFallbackTimersShouldRespectGlobalSchedulingSwitch() { assertSchedulingSwitch(ApprovalWriteBackTimer.class); assertSchedulingSwitch(ReSubmitApprovalTimer.class); assertSchedulingSwitch(PersonnelSyncTimer.class); assertSchedulingSwitch(BeisenSyncTimer.class); } @Test public void personnelTimerShouldRetryWriteAndManagerLookupFailures() { assertTrue(PersonnelSyncTimer.hasSyncFailures(null)); Map stats = new HashMap<>(); stats.put("failed", 0); stats.put("managerLookupFailed", 1); assertTrue(PersonnelSyncTimer.hasSyncFailures(stats)); stats.put("managerLookupFailed", 0); stats.put("failed", 1); assertTrue(PersonnelSyncTimer.hasSyncFailures(stats)); stats.put("failed", 0); assertFalse(PersonnelSyncTimer.hasSyncFailures(stats)); } @Test public void approvalWriteBackShouldReadCurrentProcessDataShape() { Map formData = new HashMap<>(); formData.put("selectField_mq58cd5p", "全部成功"); Map process = new HashMap<>(); process.put("data", formData); assertEquals(formData, ApprovalWriteBackTimer.extractFormData(process)); } @Test public void approvalWriteBackShouldRemainCompatibleWithLegacyFormDataShape() { Map formData = new HashMap<>(); formData.put("selectField_mq58cd5p", "全部成功"); Map process = new HashMap<>(); process.put("formData", formData); assertEquals(formData, ApprovalWriteBackTimer.extractFormData(process)); } @Test public void approvalWriteBackShouldContinueAfterLegacyTwentyPageLimit() { assertTrue(ApprovalWriteBackTimer.hasNextPage(20, 100, 2500, 100)); assertFalse(ApprovalWriteBackTimer.hasNextPage(25, 100, 2500, 100)); assertFalse(ApprovalWriteBackTimer.hasNextPage(20, 100, 2500, 99)); } @Test public void approvalWriteBackShouldRetryEveryStatusExceptAllSuccess() { assertTrue(ApprovalWriteBackTimer.shouldRetrySyncStatus("")); assertTrue(ApprovalWriteBackTimer.shouldRetrySyncStatus("部分失败")); assertTrue(ApprovalWriteBackTimer.shouldRetrySyncStatus("全部失败")); assertTrue(ApprovalWriteBackTimer.shouldRetrySyncStatus("未知状态")); assertFalse(ApprovalWriteBackTimer.shouldRetrySyncStatus("全部成功")); } @Test public void approvalWriteBackShouldUseStatusFieldForEachApprovalType() { WHConf conf = new WHConf(); conf.setApprovalSyncStatusField("bill-status-field"); conf.setOtherApprovalSyncStatusField("other-status-field"); ApprovalWriteBackTimer timer = new ApprovalWriteBackTimer(); ReflectionTestUtils.setField(timer, "whConf", conf); assertEquals("bill-status-field", timer.resolveSyncStatusField("bill")); assertEquals("other-status-field", timer.resolveSyncStatusField("other")); } @Test public void approvalWriteBackShouldFailClosedWhenApprovalResultIsMissing() { assertEquals(Integer.valueOf(1), ApprovalWriteBackTimer.resolveResultCode("agree")); assertEquals(Integer.valueOf(0), ApprovalWriteBackTimer.resolveResultCode("refuse")); assertEquals(null, ApprovalWriteBackTimer.resolveResultCode(null)); assertEquals(null, ApprovalWriteBackTimer.resolveResultCode("unknown")); } @Test public void approvalWriteBackShouldRetryFailedItemsAfterBothFormsFinish() { YDClient client = mock(YDClient.class); ApprovalWriteBackService service = mock(ApprovalWriteBackService.class); ApprovalWriteBackTimer timer = approvalTimer(client, service); when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process))) .thenReturn(approvalResponse("bill-id"), approvalResponse("other-id")); ApprovalWriteBackResult success = ApprovalWriteBackResult.builder().syncStatus("全部成功").build(); List calls = new ArrayList<>(); AtomicInteger billCalls = new AtomicInteger(); when(service.writeBack("bill-id", 1)).thenAnswer(invocation -> { calls.add("bill"); return billCalls.incrementAndGet() == 1 ? ApprovalWriteBackResult.builder().syncStatus("部分失败").build() : success; }); when(service.writeBack("other-id", 1)).thenAnswer(invocation -> { calls.add("other"); return success; }); timer.scanAndWriteBack(); assertEquals(Arrays.asList("bill", "other", "bill"), calls); verify(service, times(2)).writeBack("bill-id", 1); } @Test public void approvalWriteBackShouldStopAfterOneDeferredRetry() { YDClient client = mock(YDClient.class); ApprovalWriteBackService service = mock(ApprovalWriteBackService.class); ApprovalWriteBackTimer timer = approvalTimer(client, service); when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process))) .thenReturn(approvalResponse("bill-id"), approvalResponse("other-id")); when(service.writeBack("bill-id", 1)).thenThrow(new IllegalStateException("temporary failure")); when(service.writeBack("other-id", 1)) .thenReturn(ApprovalWriteBackResult.builder().syncStatus("全部成功").build()); timer.scanAndWriteBack(); verify(service, times(2)).writeBack("bill-id", 1); verify(service).writeBack("other-id", 1); } @Test public void approvalWriteBackShouldRetryFailedScanAfterBothForms() { YDClient client = mock(YDClient.class); ApprovalWriteBackService service = mock(ApprovalWriteBackService.class); ApprovalWriteBackTimer timer = approvalTimer(client, service); List calls = new ArrayList<>(); AtomicInteger billScans = new AtomicInteger(); when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process))) .thenAnswer(invocation -> { YDParam param = invocation.getArgument(0); String formUuid = param.getFormUuid(); calls.add("scan:" + formUuid); if ("bill-form".equals(formUuid) && billScans.incrementAndGet() == 1) { throw new IllegalStateException("temporary query failure"); } return approvalResponse("bill-form".equals(formUuid) ? "bill-id" : "other-id"); }); ApprovalWriteBackResult success = ApprovalWriteBackResult.builder().syncStatus("全部成功").build(); when(service.writeBack("bill-id", 1)).thenAnswer(invocation -> { calls.add("write:bill"); return success; }); when(service.writeBack("other-id", 1)).thenAnswer(invocation -> { calls.add("write:other"); return success; }); timer.scanAndWriteBack(); assertEquals(Arrays.asList("scan:bill-form", "scan:other-form", "write:other", "scan:bill-form", "write:bill"), calls); verify(service).writeBack("bill-id", 1); verify(service).writeBack("other-id", 1); } @Test public void approvalWriteBackShouldRescanMissingFormDataAfterBothForms() { YDClient client = mock(YDClient.class); ApprovalWriteBackService service = mock(ApprovalWriteBackService.class); ApprovalWriteBackTimer timer = approvalTimer(client, service); DDR_New missing = approvalResponse("bill-id"); ((Map) ((List) missing.getData()).get(0)).remove("data"); List calls = new ArrayList<>(); when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process))) .thenAnswer(invocation -> { YDParam param = invocation.getArgument(0); calls.add("scan:" + param.getFormUuid()); if (calls.size() == 1) return missing; return approvalResponse("bill-form".equals(param.getFormUuid()) ? "bill-id" : "other-id"); }); ApprovalWriteBackResult success = ApprovalWriteBackResult.builder().syncStatus("全部成功").build(); when(service.writeBack("bill-id", 1)).thenAnswer(invocation -> { calls.add("write:bill"); return success; }); when(service.writeBack("other-id", 1)).thenAnswer(invocation -> { calls.add("write:other"); return success; }); timer.scanAndWriteBack(); assertEquals(Arrays.asList("scan:bill-form", "scan:other-form", "write:other", "scan:bill-form", "write:bill"), calls); } @Test public void approvalWriteBackShouldRescanUnknownResultWithoutGuessing() { YDClient client = mock(YDClient.class); ApprovalWriteBackService service = mock(ApprovalWriteBackService.class); ApprovalWriteBackTimer timer = approvalTimer(client, service); DDR_New unknown = approvalResponse("bill-id"); ((Map) ((List) unknown.getData()).get(0)).remove("approvedResult"); when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process))) .thenReturn(unknown, approvalResponse("other-id"), approvalResponse("bill-id")); when(service.writeBack(any(String.class), eq(1))) .thenReturn(ApprovalWriteBackResult.builder().syncStatus("全部成功").build()); timer.scanAndWriteBack(); verify(service, times(1)).writeBack("bill-id", 1); verify(service, times(1)).writeBack("other-id", 1); } @Test public void approvalWriteBackShouldSkipRetryWhenStatusAlreadySucceeded() { YDClient client = mock(YDClient.class); ApprovalWriteBackService service = mock(ApprovalWriteBackService.class); ApprovalWriteBackTimer timer = approvalTimer(client, service); ((WHConf) ReflectionTestUtils.getField(timer, "whConf")) .setApprovalSyncStatusField("selectField_mq58cd5p"); when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process))) .thenReturn(approvalResponse("bill-id"), approvalResponse("other-id")); DDR_New current = new DDR_New<>(); current.setFormData(Collections.singletonMap("selectField_mq58cd5p", "全部成功")); when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_id))) .thenReturn(current); when(service.writeBack("bill-id", 1)) .thenReturn(ApprovalWriteBackResult.builder().syncStatus("部分失败").build()); when(service.writeBack("other-id", 1)) .thenReturn(ApprovalWriteBackResult.builder().syncStatus("全部成功").build()); timer.scanAndWriteBack(); verify(service, times(1)).writeBack("bill-id", 1); } private static ApprovalWriteBackTimer approvalTimer(YDClient client, ApprovalWriteBackService service) { WHConf conf = new WHConf(); conf.setFormUuidApproval("bill-form"); conf.setFormUuidOtherApproval("other-form"); ApprovalWriteBackTimer timer = new ApprovalWriteBackTimer(); ReflectionTestUtils.setField(timer, "ydClient", client); ReflectionTestUtils.setField(timer, "whConf", conf); ReflectionTestUtils.setField(timer, "approvalWriteBackService", service); ReflectionTestUtils.setField(timer, "backendExceptionLogService", mock(BackendExceptionLogService.class)); return timer; } private static DDR_New approvalResponse(String instanceId) { Map formData = new HashMap<>(); formData.put("selectField_mq58cd5p", "部分失败"); Map process = new HashMap<>(); process.put("formInstanceId", instanceId); process.put("approvedResult", "agree"); process.put("data", formData); DDR_New response = new DDR_New<>(); response.setData(Collections.singletonList(process)); response.setTotalCount(1); return response; } private static void assertSchedulingSwitch(Class timerClass) { ConditionalOnProperty condition = timerClass.getAnnotation(ConditionalOnProperty.class); assertEquals("enable", condition.prefix()); assertEquals(Arrays.asList("scheduling"), Arrays.asList(condition.name())); assertEquals("true", condition.havingValue()); assertEquals(false, condition.matchIfMissing()); } private static Set scheduledCrons(Method method) { Set crons = new HashSet<>(); for (Scheduled scheduled : method.getAnnotationsByType(Scheduled.class)) { crons.add(scheduled.cron()); } return crons; } private static Set setOf(String... values) { return new HashSet<>(Arrays.asList(values)); } }