WorkHoursTimerScheduleTest.java 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. package com.malk.timer;
  2. import com.malk.server.aliwork.YDConf;
  3. import com.malk.server.aliwork.YDParam;
  4. import com.malk.server.dingtalk.DDR_New;
  5. import com.malk.server.workhours.ApprovalWriteBackResult;
  6. import com.malk.service.workhours.WorkHoursCalcService;
  7. import com.malk.server.workhours.WHConf;
  8. import com.malk.service.aliwork.YDClient;
  9. import com.malk.service.workhours.ApprovalWriteBackService;
  10. import com.malk.service.workhours.BackendExceptionLogService;
  11. import org.junit.Test;
  12. import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  13. import org.springframework.scheduling.annotation.Scheduled;
  14. import org.springframework.test.util.ReflectionTestUtils;
  15. import java.lang.reflect.Method;
  16. import java.util.ArrayList;
  17. import java.util.Arrays;
  18. import java.util.Collections;
  19. import java.util.HashMap;
  20. import java.util.HashSet;
  21. import java.util.List;
  22. import java.util.Map;
  23. import java.util.Set;
  24. import java.util.concurrent.atomic.AtomicInteger;
  25. import static org.junit.Assert.assertEquals;
  26. import static org.junit.Assert.assertFalse;
  27. import static org.junit.Assert.assertTrue;
  28. import static org.mockito.Mockito.mock;
  29. import static org.mockito.Mockito.times;
  30. import static org.mockito.Mockito.verify;
  31. import static org.mockito.Mockito.when;
  32. import static org.mockito.ArgumentMatchers.any;
  33. import static org.mockito.ArgumentMatchers.eq;
  34. public class WorkHoursTimerScheduleTest {
  35. @Test
  36. public void workHoursIncrementalTimerShouldRunAtThreeThirtyAndTwelveFortyFive() throws Exception {
  37. Method method = WorkHoursCalcTimer.class.getMethod("calcDailyIncrementalSync");
  38. assertEquals(setOf("0 30 3 * * ?", "0 45 12 * * ?"), scheduledCrons(method));
  39. }
  40. @Test
  41. public void workHoursIncrementalTimerShouldUseSevenDayRecoveryWindow() {
  42. WorkHoursCalcService service = mock(WorkHoursCalcService.class);
  43. WorkHoursCalcTimer timer = new WorkHoursCalcTimer();
  44. ReflectionTestUtils.setField(timer, "workHoursCalcService", service);
  45. timer.calcDailyIncrementalSync();
  46. verify(service).incrementalSync(7);
  47. }
  48. @Test
  49. public void workHoursTimerShouldRespectGlobalSchedulingSwitch() {
  50. ConditionalOnProperty condition = WorkHoursCalcTimer.class.getAnnotation(ConditionalOnProperty.class);
  51. assertEquals("enable", condition.prefix());
  52. assertEquals(Arrays.asList("scheduling"), Arrays.asList(condition.name()));
  53. assertEquals("true", condition.havingValue());
  54. assertEquals(false, condition.matchIfMissing());
  55. }
  56. @Test
  57. public void projectChangeTimerShouldRunFifteenMinutesBeforeWorkHoursTimer() throws Exception {
  58. Method method = ProjectChangeSyncTimer.class.getMethod("syncProjectChanges");
  59. assertEquals(setOf("0 15 3 * * ?", "0 30 12 * * ?"), scheduledCrons(method));
  60. }
  61. @Test
  62. public void projectChangeTimerShouldRequireExplicitEnablement() {
  63. ConditionalOnProperty condition = ProjectChangeSyncTimer.class.getAnnotation(ConditionalOnProperty.class);
  64. assertEquals("workhours", condition.prefix());
  65. assertEquals(Arrays.asList("projectChangeSyncEnabled"), Arrays.asList(condition.name()));
  66. assertEquals("true", condition.havingValue());
  67. assertEquals(false, condition.matchIfMissing());
  68. }
  69. @Test
  70. public void approvalWriteBackShouldRunBeforeApprovalResubmit() throws Exception {
  71. Method writeBack = ApprovalWriteBackTimer.class.getMethod("scanAndWriteBack");
  72. Method resubmit = ReSubmitApprovalTimer.class.getMethod("scheduledMorningResubmit");
  73. Method noonResubmit = ReSubmitApprovalTimer.class.getMethod("scheduledAfternoonResubmit");
  74. assertEquals(setOf("0 20 4 * * ?", "0 20 12 * * ?"), scheduledCrons(writeBack));
  75. assertEquals(setOf("0 0 7 * * ?"), scheduledCrons(resubmit));
  76. assertEquals(setOf("0 0 16 * * ?"), scheduledCrons(noonResubmit));
  77. }
  78. @Test
  79. public void approvalResubmitShouldDefaultToSevenDayRecoveryWindow() {
  80. assertEquals(7, new WHConf().getApprovalResubmitModifiedLookbackDays());
  81. }
  82. @Test
  83. public void approvalFallbackTimersShouldRespectGlobalSchedulingSwitch() {
  84. assertSchedulingSwitch(ApprovalWriteBackTimer.class);
  85. assertSchedulingSwitch(ReSubmitApprovalTimer.class);
  86. assertSchedulingSwitch(PersonnelSyncTimer.class);
  87. assertSchedulingSwitch(BeisenSyncTimer.class);
  88. }
  89. @Test
  90. public void personnelTimerShouldRetryWriteAndManagerLookupFailures() {
  91. assertTrue(PersonnelSyncTimer.hasSyncFailures(null));
  92. Map<String, Object> stats = new HashMap<>();
  93. stats.put("failed", 0);
  94. stats.put("managerLookupFailed", 1);
  95. assertTrue(PersonnelSyncTimer.hasSyncFailures(stats));
  96. stats.put("managerLookupFailed", 0);
  97. stats.put("failed", 1);
  98. assertTrue(PersonnelSyncTimer.hasSyncFailures(stats));
  99. stats.put("failed", 0);
  100. assertFalse(PersonnelSyncTimer.hasSyncFailures(stats));
  101. }
  102. @Test
  103. public void approvalWriteBackShouldReadCurrentProcessDataShape() {
  104. Map<String, Object> formData = new HashMap<>();
  105. formData.put("selectField_mq58cd5p", "全部成功");
  106. Map<String, Object> process = new HashMap<>();
  107. process.put("data", formData);
  108. assertEquals(formData, ApprovalWriteBackTimer.extractFormData(process));
  109. }
  110. @Test
  111. public void approvalWriteBackShouldRemainCompatibleWithLegacyFormDataShape() {
  112. Map<String, Object> formData = new HashMap<>();
  113. formData.put("selectField_mq58cd5p", "全部成功");
  114. Map<String, Object> process = new HashMap<>();
  115. process.put("formData", formData);
  116. assertEquals(formData, ApprovalWriteBackTimer.extractFormData(process));
  117. }
  118. @Test
  119. public void approvalWriteBackShouldContinueAfterLegacyTwentyPageLimit() {
  120. assertTrue(ApprovalWriteBackTimer.hasNextPage(20, 100, 2500, 100));
  121. assertFalse(ApprovalWriteBackTimer.hasNextPage(25, 100, 2500, 100));
  122. assertFalse(ApprovalWriteBackTimer.hasNextPage(20, 100, 2500, 99));
  123. }
  124. @Test
  125. public void approvalWriteBackShouldRetryEveryStatusExceptAllSuccess() {
  126. assertTrue(ApprovalWriteBackTimer.shouldRetrySyncStatus(""));
  127. assertTrue(ApprovalWriteBackTimer.shouldRetrySyncStatus("部分失败"));
  128. assertTrue(ApprovalWriteBackTimer.shouldRetrySyncStatus("全部失败"));
  129. assertTrue(ApprovalWriteBackTimer.shouldRetrySyncStatus("未知状态"));
  130. assertFalse(ApprovalWriteBackTimer.shouldRetrySyncStatus("全部成功"));
  131. }
  132. @Test
  133. public void approvalWriteBackShouldUseStatusFieldForEachApprovalType() {
  134. WHConf conf = new WHConf();
  135. conf.setApprovalSyncStatusField("bill-status-field");
  136. conf.setOtherApprovalSyncStatusField("other-status-field");
  137. ApprovalWriteBackTimer timer = new ApprovalWriteBackTimer();
  138. ReflectionTestUtils.setField(timer, "whConf", conf);
  139. assertEquals("bill-status-field", timer.resolveSyncStatusField("bill"));
  140. assertEquals("other-status-field", timer.resolveSyncStatusField("other"));
  141. }
  142. @Test
  143. public void approvalWriteBackShouldFailClosedWhenApprovalResultIsMissing() {
  144. assertEquals(Integer.valueOf(1), ApprovalWriteBackTimer.resolveResultCode("agree"));
  145. assertEquals(Integer.valueOf(0), ApprovalWriteBackTimer.resolveResultCode("refuse"));
  146. assertEquals(null, ApprovalWriteBackTimer.resolveResultCode(null));
  147. assertEquals(null, ApprovalWriteBackTimer.resolveResultCode("unknown"));
  148. }
  149. @Test
  150. public void approvalWriteBackShouldRetryFailedItemsAfterBothFormsFinish() {
  151. YDClient client = mock(YDClient.class);
  152. ApprovalWriteBackService service = mock(ApprovalWriteBackService.class);
  153. ApprovalWriteBackTimer timer = approvalTimer(client, service);
  154. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process)))
  155. .thenReturn(approvalResponse("bill-id"), approvalResponse("other-id"));
  156. ApprovalWriteBackResult success = ApprovalWriteBackResult.builder().syncStatus("全部成功").build();
  157. List<String> calls = new ArrayList<>();
  158. AtomicInteger billCalls = new AtomicInteger();
  159. when(service.writeBack("bill-id", 1)).thenAnswer(invocation -> {
  160. calls.add("bill");
  161. return billCalls.incrementAndGet() == 1
  162. ? ApprovalWriteBackResult.builder().syncStatus("部分失败").build() : success;
  163. });
  164. when(service.writeBack("other-id", 1)).thenAnswer(invocation -> {
  165. calls.add("other");
  166. return success;
  167. });
  168. timer.scanAndWriteBack();
  169. assertEquals(Arrays.asList("bill", "other", "bill"), calls);
  170. verify(service, times(2)).writeBack("bill-id", 1);
  171. }
  172. @Test
  173. public void approvalWriteBackShouldStopAfterOneDeferredRetry() {
  174. YDClient client = mock(YDClient.class);
  175. ApprovalWriteBackService service = mock(ApprovalWriteBackService.class);
  176. ApprovalWriteBackTimer timer = approvalTimer(client, service);
  177. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process)))
  178. .thenReturn(approvalResponse("bill-id"), approvalResponse("other-id"));
  179. when(service.writeBack("bill-id", 1)).thenThrow(new IllegalStateException("temporary failure"));
  180. when(service.writeBack("other-id", 1))
  181. .thenReturn(ApprovalWriteBackResult.builder().syncStatus("全部成功").build());
  182. timer.scanAndWriteBack();
  183. verify(service, times(2)).writeBack("bill-id", 1);
  184. verify(service).writeBack("other-id", 1);
  185. }
  186. @Test
  187. public void approvalWriteBackShouldRetryFailedScanAfterBothForms() {
  188. YDClient client = mock(YDClient.class);
  189. ApprovalWriteBackService service = mock(ApprovalWriteBackService.class);
  190. ApprovalWriteBackTimer timer = approvalTimer(client, service);
  191. List<String> calls = new ArrayList<>();
  192. AtomicInteger billScans = new AtomicInteger();
  193. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process)))
  194. .thenAnswer(invocation -> {
  195. YDParam param = invocation.getArgument(0);
  196. String formUuid = param.getFormUuid();
  197. calls.add("scan:" + formUuid);
  198. if ("bill-form".equals(formUuid) && billScans.incrementAndGet() == 1) {
  199. throw new IllegalStateException("temporary query failure");
  200. }
  201. return approvalResponse("bill-form".equals(formUuid) ? "bill-id" : "other-id");
  202. });
  203. ApprovalWriteBackResult success = ApprovalWriteBackResult.builder().syncStatus("全部成功").build();
  204. when(service.writeBack("bill-id", 1)).thenAnswer(invocation -> {
  205. calls.add("write:bill");
  206. return success;
  207. });
  208. when(service.writeBack("other-id", 1)).thenAnswer(invocation -> {
  209. calls.add("write:other");
  210. return success;
  211. });
  212. timer.scanAndWriteBack();
  213. assertEquals(Arrays.asList("scan:bill-form", "scan:other-form", "write:other",
  214. "scan:bill-form", "write:bill"), calls);
  215. verify(service).writeBack("bill-id", 1);
  216. verify(service).writeBack("other-id", 1);
  217. }
  218. @Test
  219. public void approvalWriteBackShouldRescanMissingFormDataAfterBothForms() {
  220. YDClient client = mock(YDClient.class);
  221. ApprovalWriteBackService service = mock(ApprovalWriteBackService.class);
  222. ApprovalWriteBackTimer timer = approvalTimer(client, service);
  223. DDR_New<Object> missing = approvalResponse("bill-id");
  224. ((Map) ((List) missing.getData()).get(0)).remove("data");
  225. List<String> calls = new ArrayList<>();
  226. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process)))
  227. .thenAnswer(invocation -> {
  228. YDParam param = invocation.getArgument(0);
  229. calls.add("scan:" + param.getFormUuid());
  230. if (calls.size() == 1) return missing;
  231. return approvalResponse("bill-form".equals(param.getFormUuid()) ? "bill-id" : "other-id");
  232. });
  233. ApprovalWriteBackResult success = ApprovalWriteBackResult.builder().syncStatus("全部成功").build();
  234. when(service.writeBack("bill-id", 1)).thenAnswer(invocation -> {
  235. calls.add("write:bill");
  236. return success;
  237. });
  238. when(service.writeBack("other-id", 1)).thenAnswer(invocation -> {
  239. calls.add("write:other");
  240. return success;
  241. });
  242. timer.scanAndWriteBack();
  243. assertEquals(Arrays.asList("scan:bill-form", "scan:other-form", "write:other",
  244. "scan:bill-form", "write:bill"), calls);
  245. }
  246. @Test
  247. public void approvalWriteBackShouldRescanUnknownResultWithoutGuessing() {
  248. YDClient client = mock(YDClient.class);
  249. ApprovalWriteBackService service = mock(ApprovalWriteBackService.class);
  250. ApprovalWriteBackTimer timer = approvalTimer(client, service);
  251. DDR_New<Object> unknown = approvalResponse("bill-id");
  252. ((Map) ((List) unknown.getData()).get(0)).remove("approvedResult");
  253. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process)))
  254. .thenReturn(unknown, approvalResponse("other-id"), approvalResponse("bill-id"));
  255. when(service.writeBack(any(String.class), eq(1)))
  256. .thenReturn(ApprovalWriteBackResult.builder().syncStatus("全部成功").build());
  257. timer.scanAndWriteBack();
  258. verify(service, times(1)).writeBack("bill-id", 1);
  259. verify(service, times(1)).writeBack("other-id", 1);
  260. }
  261. @Test
  262. public void approvalWriteBackShouldSkipRetryWhenStatusAlreadySucceeded() {
  263. YDClient client = mock(YDClient.class);
  264. ApprovalWriteBackService service = mock(ApprovalWriteBackService.class);
  265. ApprovalWriteBackTimer timer = approvalTimer(client, service);
  266. ((WHConf) ReflectionTestUtils.getField(timer, "whConf"))
  267. .setApprovalSyncStatusField("selectField_mq58cd5p");
  268. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_process)))
  269. .thenReturn(approvalResponse("bill-id"), approvalResponse("other-id"));
  270. DDR_New<Object> current = new DDR_New<>();
  271. current.setFormData(Collections.singletonMap("selectField_mq58cd5p", "全部成功"));
  272. when(client.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_id)))
  273. .thenReturn(current);
  274. when(service.writeBack("bill-id", 1))
  275. .thenReturn(ApprovalWriteBackResult.builder().syncStatus("部分失败").build());
  276. when(service.writeBack("other-id", 1))
  277. .thenReturn(ApprovalWriteBackResult.builder().syncStatus("全部成功").build());
  278. timer.scanAndWriteBack();
  279. verify(service, times(1)).writeBack("bill-id", 1);
  280. }
  281. private static ApprovalWriteBackTimer approvalTimer(YDClient client, ApprovalWriteBackService service) {
  282. WHConf conf = new WHConf();
  283. conf.setFormUuidApproval("bill-form");
  284. conf.setFormUuidOtherApproval("other-form");
  285. ApprovalWriteBackTimer timer = new ApprovalWriteBackTimer();
  286. ReflectionTestUtils.setField(timer, "ydClient", client);
  287. ReflectionTestUtils.setField(timer, "whConf", conf);
  288. ReflectionTestUtils.setField(timer, "approvalWriteBackService", service);
  289. ReflectionTestUtils.setField(timer, "backendExceptionLogService", mock(BackendExceptionLogService.class));
  290. return timer;
  291. }
  292. private static DDR_New<Object> approvalResponse(String instanceId) {
  293. Map<String, Object> formData = new HashMap<>();
  294. formData.put("selectField_mq58cd5p", "部分失败");
  295. Map<String, Object> process = new HashMap<>();
  296. process.put("formInstanceId", instanceId);
  297. process.put("approvedResult", "agree");
  298. process.put("data", formData);
  299. DDR_New<Object> response = new DDR_New<>();
  300. response.setData(Collections.singletonList(process));
  301. response.setTotalCount(1);
  302. return response;
  303. }
  304. private static void assertSchedulingSwitch(Class<?> timerClass) {
  305. ConditionalOnProperty condition = timerClass.getAnnotation(ConditionalOnProperty.class);
  306. assertEquals("enable", condition.prefix());
  307. assertEquals(Arrays.asList("scheduling"), Arrays.asList(condition.name()));
  308. assertEquals("true", condition.havingValue());
  309. assertEquals(false, condition.matchIfMissing());
  310. }
  311. private static Set<String> scheduledCrons(Method method) {
  312. Set<String> crons = new HashSet<>();
  313. for (Scheduled scheduled : method.getAnnotationsByType(Scheduled.class)) {
  314. crons.add(scheduled.cron());
  315. }
  316. return crons;
  317. }
  318. private static Set<String> setOf(String... values) {
  319. return new HashSet<>(Arrays.asList(values));
  320. }
  321. }