Просмотр исходного кода

Merge pull request #7377 from arafubeatbox/fix/114143-114144-fix-questionnaire-cron-test

Fix/114143 114144 fix questionnaire cron test
Ryoji Shimizu 3 лет назад
Родитель
Сommit
c4a8698679

+ 37 - 20
packages/app/src/server/service/questionnaire-cron.ts

@@ -16,6 +16,12 @@ const nodeCron = require('node-cron');
 
 axiosRetry(axios, { retries: 3 });
 
+/**
+ * manage cronjob which
+ *  1. fetches QuestionnaireOrders from questionnaire server
+ *  2. updates QuestionnaireOrder collection to contain only the ones that exist in the fetched list and is not finished (doesn't have to be started)
+ *  3. changes QuestionnaireAnswerStatuses which are 'skipped' to 'not_answered'
+ */
 class QuestionnaireCronService {
 
   crowi: any;
@@ -32,7 +38,9 @@ class QuestionnaireCronService {
     const maxHoursUntilRequest = this.crowi.configManager?.getConfig('crowi', 'app:questionnaireCronMaxHoursUntilRequest');
 
     const maxSecondsUntilRequest = maxHoursUntilRequest * 60 * 60;
-    this.cronJob = this.questionnaireOrderGetCron(cronSchedule, maxSecondsUntilRequest);
+
+    this.cronJob?.stop();
+    this.cronJob = this.generateCronJob(cronSchedule, maxSecondsUntilRequest);
     this.cronJob.start();
   }
 
@@ -40,34 +48,43 @@ class QuestionnaireCronService {
     this.cronJob.stop();
   }
 
-  private questionnaireOrderGetCron(cronSchedule: string, maxSecondsUntilRequest: number) {
-    const growiQuestionnaireServerOrigin = this.crowi.configManager?.getConfig('crowi', 'app:growiQuestionnaireServerOrigin');
-    const saveOrders = async(questionnaireOrders: IQuestionnaireOrder[]) => {
+  async executeJob(): Promise<void> {
+    const fetchQuestionnaireOrders = async(): Promise<IQuestionnaireOrder[]> => {
+      const growiQuestionnaireServerOrigin = this.crowi.configManager?.getConfig('crowi', 'app:growiQuestionnaireServerOrigin');
+      const response = await axios.get(`${growiQuestionnaireServerOrigin}/questionnaire-order/index`);
+      return response.data.questionnaireOrders;
+    };
+
+    const saveUnfinishedOrders = async(questionnaireOrders: IQuestionnaireOrder[]) => {
       const currentDate = new Date(Date.now());
-      // save questionnaires that are not finished (doesn't have to be started)
-      const nonFinishedOrders = questionnaireOrders.filter(order => new Date(order.showUntil) > currentDate);
-      await QuestionnaireOrder.insertMany(nonFinishedOrders);
+      const unfinishedOrders = questionnaireOrders.filter(order => new Date(order.showUntil) > currentDate);
+      await QuestionnaireOrder.insertMany(unfinishedOrders);
     };
 
+    const changeSkippedAnswerStatusToNotAnswered = async() => {
+      await QuestionnaireAnswerStatus.updateMany(
+        { status: StatusType.skipped },
+        { status: StatusType.not_answered },
+      );
+    };
+
+    const questionnaireOrders: IQuestionnaireOrder[] = await fetchQuestionnaireOrders();
+
+    // reset QuestionnaireOrder collection and save unfinished ones that exist on questionnaire server
+    await QuestionnaireOrder.deleteMany();
+    await saveUnfinishedOrders(questionnaireOrders);
+
+    await changeSkippedAnswerStatusToNotAnswered();
+  }
+
+  private generateCronJob(cronSchedule: string, maxSecondsUntilRequest: number) {
     return nodeCron.schedule(cronSchedule, async() => {
       // sleep for a random amount to scatter request time from GROWI apps to questionnaire server
       const secToSleep = getRandomIntInRange(0, maxSecondsUntilRequest);
       await sleep(secToSleep * 1000);
 
       try {
-        const response = await axios.get(`${growiQuestionnaireServerOrigin}/questionnaire-order/index`);
-        const questionnaireOrders: IQuestionnaireOrder[] = response.data.questionnaireOrders;
-
-        // Reset status (denied => not_answered)
-        await QuestionnaireAnswerStatus.updateMany(
-          { status: StatusType.denied },
-          { status: StatusType.not_answered },
-        );
-
-        // Cleanup
-        await QuestionnaireOrder.deleteMany();
-
-        await saveOrders(questionnaireOrders);
+        this.executeJob();
       }
       catch (e) {
         logger.error(e);

+ 27 - 31
packages/app/test/integration/service/questionnaire-cron.test.ts

@@ -1,26 +1,20 @@
+import mongoose from 'mongoose';
+
+import { StatusType } from '../../../src/interfaces/questionnaire/questionnaire-answer-status';
+import QuestionnaireAnswerStatus from '../../../src/server/models/questionnaire/questionnaire-answer-status';
 import QuestionnaireOrder from '../../../src/server/models/questionnaire/questionnaire-order';
 import { getInstance } from '../setup-crowi';
 
 const axios = require('axios').default;
 
-const rand = require('../../../src/utils/rand');
-
 const spyAxiosGet = jest.spyOn<typeof axios, 'get'>(
   axios,
   'get',
 );
 
-const spyGetRandomIntInRange = jest.spyOn<typeof rand, 'getRandomIntInRange'>(
-  rand,
-  'getRandomIntInRange',
-);
-
 describe('QuestionnaireCronService', () => {
   let crowi;
 
-  const maxSecondsUntilRequest = 4 * 60 * 60 * 1000;
-  const secondsUntilRequest = rand.getRandomIntInRange(0, maxSecondsUntilRequest);
-
   const mockResponse = {
     data: {
       questionnaireOrders: [
@@ -134,9 +128,6 @@ describe('QuestionnaireCronService', () => {
   };
 
   beforeAll(async() => {
-    process.env.QUESTIONNAIRE_CRON_SCHEDULE = '0 22 * * *';
-    process.env.QUESTIONNAIRE_CRON_MAX_HOURS_UNTIL_REQUEST = '4';
-
     crowi = await getInstance();
     // reload
     await crowi.setupConfigManager();
@@ -242,33 +233,36 @@ describe('QuestionnaireCronService', () => {
       },
     ]);
 
-    // mock the date to 5 seconds before cronjob execution
-    const mockDate = new Date(2022, 0, 1, 21, 59, 55);
-    jest.useFakeTimers();
-    jest.setSystemTime(mockDate);
+    await QuestionnaireAnswerStatus.insertMany([
+      {
+        user: new mongoose.Types.ObjectId(),
+        questionnaireOrderId: '63a8354837e7aa378e16f0b1',
+        status: StatusType.skipped,
+      },
+      {
+        user: new mongoose.Types.ObjectId(),
+        questionnaireOrderId: '63a8354837e7aa378e16f0b1',
+        status: StatusType.answered,
+      },
+      {
+        user: new mongoose.Types.ObjectId(),
+        questionnaireOrderId: '63a8354837e7aa378e16f0b1',
+        status: StatusType.not_answered,
+      },
+    ]);
 
-    // must be after useFakeTimers for mockDate to be in effect
     crowi.setupCron();
 
     spyAxiosGet.mockResolvedValue(mockResponse);
-    spyGetRandomIntInRange.mockReturnValue(secondsUntilRequest); // static sleep time until request
   });
 
   afterAll(() => {
-    jest.useRealTimers();
-    crowi.questionnaireCronService.stopCron();
+    crowi.questionnaireCronService.stopCron(); // jest will not finish until cronjob stops
   });
 
-  test('Should save quesionnaire orders and delete outdated ones', async() => {
-    jest.advanceTimersByTime(5 * 1000); // advance unitl cronjob execution
-    jest.advanceTimersByTime(secondsUntilRequest); // advance until request execution
-    jest.useRealTimers(); // after cronjob starts, undo timer mocks so mongoose can work properly
-
-    await new Promise((resolve) => {
-      // wait until cronjob execution finishes
-      // refs: https://github.com/node-cron/node-cron/blob/a0be3f4a7a5419af109cecf4a41071ea559b9b3d/src/task.js#L24
-      crowi.questionnaireCronService.cronJob._task.once('task-finished', resolve);
-    });
+  test('Job execution should save(update) quesionnaire orders, delete outdated ones, and update skipped answer statuses', async() => {
+    // testing the cronjob from schedule has untrivial overhead, so test job execution in place
+    await crowi.questionnaireCronService.executeJob();
 
     const savedOrders = await QuestionnaireOrder.find()
       .select('-condition._id -questions._id')
@@ -343,5 +337,7 @@ describe('QuestionnaireCronService', () => {
         __v: 0,
       },
     ]);
+
+    expect((await QuestionnaireAnswerStatus.find({ status: StatusType.not_answered })).length).toEqual(2);
   });
 });