فهرست منبع

migrate test to vitest

Yuki Takei 2 ماه پیش
والد
کامیت
d6c0946300
1فایلهای تغییر یافته به همراه131 افزوده شده و 127 حذف شده
  1. 131 127
      apps/app/src/server/service/page/v5.non-public-page.integ.ts

+ 131 - 127
apps/app/test/integration/service/v5.non-public-page.test.ts → apps/app/src/server/service/page/v5.non-public-page.integ.ts

@@ -6,22 +6,28 @@ import {
 } from '@growi/core';
 import mongoose from 'mongoose';
 
-import { ExternalGroupProviderType } from '../../../src/features/external-user-group/interfaces/external-user-group';
-import ExternalUserGroup from '../../../src/features/external-user-group/server/models/external-user-group';
-import ExternalUserGroupRelation from '../../../src/features/external-user-group/server/models/external-user-group-relation';
-import type { IPageTagRelation } from '../../../src/interfaces/page-tag-relation';
-import type Crowi from '../../../src/server/crowi';
-import type { PageDocument, PageModel } from '../../../src/server/models/page';
-import PageTagRelation from '../../../src/server/models/page-tag-relation';
+import { getInstance } from '^/test-with-vite/setup/crowi';
+
+import { ExternalGroupProviderType } from '~/features/external-user-group/interfaces/external-user-group';
+import ExternalUserGroup from '~/features/external-user-group/server/models/external-user-group';
+import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
+import { PageActionType } from '~/interfaces/page-operation';
+import type { IPageTagRelation } from '~/interfaces/page-tag-relation';
+import type Crowi from '~/server/crowi';
+import type { PageDocument, PageModel } from '~/server/models/page';
+import type {
+  IPageOperation,
+  PageOperationModel,
+} from '~/server/models/page-operation';
+import PageTagRelation from '~/server/models/page-tag-relation';
 import type {
   IRevisionDocument,
   IRevisionModel,
-} from '../../../src/server/models/revision';
-import Tag from '../../../src/server/models/tag';
-import UserGroup from '../../../src/server/models/user-group';
-import UserGroupRelation from '../../../src/server/models/user-group-relation';
-import { generalXssFilter } from '../../../src/services/general-xss-filter';
-import { getInstance } from '../setup-crowi';
+} from '~/server/models/revision';
+import Tag from '~/server/models/tag';
+import UserGroup from '~/server/models/user-group';
+import UserGroupRelation from '~/server/models/user-group-relation';
+import { generalXssFilter } from '~/services/general-xss-filter';
 
 describe('PageService page operations with non-public pages', () => {
   // biome-ignore lint/suspicious/noImplicitAnyLet: ignore
@@ -45,9 +51,9 @@ describe('PageService page operations with non-public pages', () => {
   let crowi: Crowi;
   let Page: PageModel;
   let Revision: IRevisionModel;
-  // biome-ignore lint/suspicious/noImplicitAnyLet: ignore
-  let User;
-  let generalXssFilterProcessSpy: jest.SpyInstance;
+  let User: UserModel;
+  let PageOperation: PageOperationModel;
+  let generalXssFilterProcessSpy: ReturnType<typeof vi.spyOn>;
 
   let rootPage: PageDocument;
 
@@ -110,7 +116,7 @@ describe('PageService page operations with non-public pages', () => {
   const tagIdRevert2 = new mongoose.Types.ObjectId();
 
   const create = async (path, body, user, options = {}) => {
-    const mockedCreateSubOperation = jest
+    const mockedCreateSubOperation = vi
       .spyOn(crowi.pageService, 'createSubOperation')
       .mockReturnValue(null);
 
@@ -145,6 +151,31 @@ describe('PageService page operations with non-public pages', () => {
     });
   };
 
+  // Common helper to wait for PageOperation completion
+  const waitForPageOperationComplete = async (
+    fromPath: string,
+    actionType?: PageActionType,
+    maxWaitMs = 5000,
+  ) => {
+    const startTime = Date.now();
+    while (Date.now() - startTime < maxWaitMs) {
+      const query: { fromPath: string; actionType?: PageActionType } = {
+        fromPath,
+      };
+      if (actionType != null) {
+        query.actionType = actionType;
+      }
+      const op = await PageOperation.findOne(query);
+      if (op == null) {
+        return; // Operation completed
+      }
+      await new Promise((resolve) => setTimeout(resolve, 50));
+    }
+    throw new Error(
+      `PageOperation for ${fromPath} did not complete within ${maxWaitMs}ms`,
+    );
+  };
+
   beforeAll(async () => {
     crowi = await getInstance();
     await crowi.configManager.updateConfig('app:isV5Compatible', true);
@@ -152,6 +183,9 @@ describe('PageService page operations with non-public pages', () => {
     User = mongoose.model('User');
     Page = mongoose.model<IPage, PageModel>('Page');
     Revision = mongoose.model<IRevision, IRevisionModel>('Revision');
+    PageOperation = mongoose.model<IPageOperation, PageOperationModel>(
+      'PageOperation',
+    );
 
     /*
      * Common
@@ -329,22 +363,48 @@ describe('PageService page operations with non-public pages', () => {
       },
     ]);
 
-    generalXssFilterProcessSpy = jest.spyOn(generalXssFilter, 'process');
+    generalXssFilterProcessSpy = vi.spyOn(generalXssFilter, 'process');
+
+    // Ensure root page exists
+    const existingRootPage = await Page.findOne({ path: '/' });
+    if (existingRootPage == null) {
+      const rootPageId = new mongoose.Types.ObjectId();
+      rootPage = await Page.create({
+        _id: rootPageId,
+        path: '/',
+        grant: Page.GRANT_PUBLIC,
+      });
+    } else {
+      rootPage = existingRootPage;
+    }
+
+    // Create dummy users if they do not exist
+    const usersToCreate = [
+      {
+        name: 'v5DummyUser1',
+        username: 'v5DummyUser1',
+        email: 'v5dummyuser1@example.com',
+      },
+      {
+        name: 'v5DummyUser2',
+        username: 'v5DummyUser2',
+        email: 'v5dummyuser2@example.com',
+      },
+    ];
+    for (const userData of usersToCreate) {
+      const existing = await User.findOne({ username: userData.username });
+      if (existing == null) {
+        await User.insertMany([userData]);
+      }
+    }
 
+    // Assign dummy users to variables
     dummyUser1 = await User.findOne({ username: 'v5DummyUser1' });
     dummyUser2 = await User.findOne({ username: 'v5DummyUser2' });
     npDummyUser1 = await User.findOne({ username: 'npUser1' });
     npDummyUser2 = await User.findOne({ username: 'npUser2' });
     npDummyUser3 = await User.findOne({ username: 'npUser3' });
 
-    rootPage = (await Page.findOne({ path: '/' }))!;
-    if (rootPage == null) {
-      const pages = await Page.insertMany([
-        { path: '/', grant: Page.GRANT_PUBLIC },
-      ]);
-      rootPage = pages[0];
-    }
-
     /**
      * create
      * mc_ => model create
@@ -1003,8 +1063,8 @@ describe('PageService page operations with non-public pages', () => {
 
   describe('create', () => {
     describe('Creating a page using existing path', () => {
-      test('with grant RESTRICTED should only create the page and change nothing else', async () => {
-        const isGrantNormalizedSpy = jest.spyOn(
+      it('with grant RESTRICTED should only create the page and change nothing else', async () => {
+        const isGrantNormalizedSpy = vi.spyOn(
           crowi.pageGrantService,
           'isGrantNormalized',
         );
@@ -1051,8 +1111,8 @@ describe('PageService page operations with non-public pages', () => {
       });
     });
     describe('Creating a page under a page with grant RESTRICTED', () => {
-      test('will create a new empty page with the same path as the grant RESTRECTED page and become a parent', async () => {
-        const isGrantNormalizedSpy = jest.spyOn(
+      it('will create a new empty page with the same path as the grant RESTRECTED page and become a parent', async () => {
+        const isGrantNormalizedSpy = vi.spyOn(
           crowi.pageGrantService,
           'isGrantNormalized',
         );
@@ -1102,7 +1162,7 @@ describe('PageService page operations with non-public pages', () => {
     });
     describe('Creating a page under a page with grant USER_GROUP', () => {
       describe('When onlyInheritUserRelatedGrantedGroups is true', () => {
-        test('Only user related groups should be inherited', async () => {
+        it('Only user related groups should be inherited', async () => {
           const pathT = '/mc6_top';
           const pageT = await Page.findOne({ path: pathT });
           expect(pageT).toBeTruthy();
@@ -1129,7 +1189,7 @@ describe('PageService page operations with non-public pages', () => {
       });
 
       describe('When onlyInheritUserRelatedGrantedGroups is false', () => {
-        test('All groups should be inherited', async () => {
+        it('All groups should be inherited', async () => {
           const pathT = '/mc6_top';
           const pageT = await Page.findOne({ path: pathT });
           expect(pageT).toBeTruthy();
@@ -1160,8 +1220,8 @@ describe('PageService page operations with non-public pages', () => {
 
   describe('create by system', () => {
     describe('Creating a page using existing path', () => {
-      test('with grant RESTRICTED should only create the page and change nothing else', async () => {
-        const isGrantNormalizedSpy = jest.spyOn(
+      it('with grant RESTRICTED should only create the page and change nothing else', async () => {
+        const isGrantNormalizedSpy = vi.spyOn(
           crowi.pageGrantService,
           'isGrantNormalized',
         );
@@ -1208,8 +1268,8 @@ describe('PageService page operations with non-public pages', () => {
       });
     });
     describe('Creating a page under a page with grant RESTRICTED', () => {
-      test('will create a new empty page with the same path as the grant RESTRECTED page and become a parent', async () => {
-        const isGrantNormalizedSpy = jest.spyOn(
+      it('will create a new empty page with the same path as the grant RESTRECTED page and become a parent', async () => {
+        const isGrantNormalizedSpy = vi.spyOn(
           crowi.pageGrantService,
           'isGrantNormalized',
         );
@@ -1267,10 +1327,7 @@ describe('PageService page operations with non-public pages', () => {
       options,
       activityParameters?,
     ) => {
-      // mock return value
-      const mockedRenameSubOperation = jest
-        .spyOn(crowi.pageService, 'renameSubOperation')
-        .mockReturnValue(null);
+      const fromPath = page.path;
       const renamedPage = await crowi.pageService.renamePage(
         page,
         newPagePath,
@@ -1279,25 +1336,16 @@ describe('PageService page operations with non-public pages', () => {
         activityParameters,
       );
 
-      // retrieve the arguments passed when calling method renameSubOperation inside renamePage method
-      const argsForRenameSubOperation = mockedRenameSubOperation.mock.calls[0];
-
-      // restores the original implementation
-      mockedRenameSubOperation.mockRestore();
-
-      // rename descendants
+      // Wait for the async renameSubOperation to complete
+      // renameSubOperation is called without await in production, so we need to wait for it
       if (page.grant !== Page.GRANT_RESTRICTED) {
-        await crowi.pageService.renameSubOperation(
-          ...(argsForRenameSubOperation as Parameters<
-            typeof crowi.pageService.renameSubOperation
-          >),
-        );
+        await waitForPageOperationComplete(fromPath);
       }
 
       return renamedPage;
     };
 
-    test('Should rename/move with descendants with grant normalized pages', async () => {
+    it('Should rename/move with descendants with grant normalized pages', async () => {
       const _pathD = '/np_rename1_destination';
       const _path2 = '/np_rename2';
       const _path3 = '/np_rename2/np_rename3';
@@ -1359,7 +1407,7 @@ describe('PageService page operations with non-public pages', () => {
       );
       expect(generalXssFilterProcessSpy).toHaveBeenCalled();
     });
-    test('Should throw with NOT grant normalized pages', async () => {
+    it('Should throw with NOT grant normalized pages', async () => {
       const _pathD = '/np_rename4_destination';
       const _path2 = '/np_rename5';
       const _path3 = '/np_rename5/np_rename6';
@@ -1414,7 +1462,7 @@ describe('PageService page operations with non-public pages', () => {
       expect(page2Renamed).toBeNull();
       expect(page3Renamed).toBeNull();
     });
-    test('Should rename/move multiple pages: child page with GRANT_RESTRICTED should NOT be renamed.', async () => {
+    it('Should rename/move multiple pages: child page with GRANT_RESTRICTED should NOT be renamed.', async () => {
       const _pathD = '/np_rename7_destination';
       const _path2 = '/np_rename8';
       const _path3 = '/np_rename8/np_rename9';
@@ -1469,10 +1517,7 @@ describe('PageService page operations with non-public pages', () => {
       isRecursively: boolean,
       onlyDuplicateUserRelatedResources: boolean,
     ) => {
-      // mock return value
-      const mockedDuplicateRecursivelyMainOperation = jest
-        .spyOn(crowi.pageService, 'duplicateRecursivelyMainOperation')
-        .mockReturnValue(null);
+      const fromPath = page.path;
       const duplicatedPage = await crowi.pageService.duplicate(
         page,
         newPagePath,
@@ -1481,25 +1526,14 @@ describe('PageService page operations with non-public pages', () => {
         onlyDuplicateUserRelatedResources,
       );
 
-      // retrieve the arguments passed when calling method duplicateRecursivelyMainOperation inside duplicate method
-      const argsForDuplicateRecursivelyMainOperation =
-        mockedDuplicateRecursivelyMainOperation.mock.calls[0];
-
-      // restores the original implementation
-      mockedDuplicateRecursivelyMainOperation.mockRestore();
-
-      // duplicate descendants
+      // Wait for the async duplicateRecursivelyMainOperation to complete
       if (page.grant !== Page.GRANT_RESTRICTED && isRecursively) {
-        await crowi.pageService.duplicateRecursivelyMainOperation(
-          ...(argsForDuplicateRecursivelyMainOperation as Parameters<
-            typeof crowi.pageService.duplicateRecursivelyMainOperation
-          >),
-        );
+        await waitForPageOperationComplete(fromPath, PageActionType.Duplicate);
       }
 
       return duplicatedPage;
     };
-    test('Duplicate single page with GRANT_RESTRICTED', async () => {
+    it('Duplicate single page with GRANT_RESTRICTED', async () => {
       const _page = await Page.findOne({
         path: '/np_duplicate1',
         grant: Page.GRANT_RESTRICTED,
@@ -1528,7 +1562,7 @@ describe('PageService page operations with non-public pages', () => {
       expect(duplicatedRevision?.body).toBe(_revision?.body);
     });
 
-    test('Should duplicate multiple pages with GRANT_USER_GROUP', async () => {
+    it('Should duplicate multiple pages with GRANT_USER_GROUP', async () => {
       const _path1 = '/np_duplicate2';
       const _path2 = '/np_duplicate2/np_duplicate3';
       const _page1 = await Page.findOne({
@@ -1595,7 +1629,7 @@ describe('PageService page operations with non-public pages', () => {
       expect(duplicatedRevision1?.pageId).toStrictEqual(duplicatedPage1?._id);
       expect(duplicatedRevision2?.pageId).toStrictEqual(duplicatedPage2?._id);
     });
-    test('Should duplicate multiple pages. Page with GRANT_RESTRICTED should NOT be duplicated', async () => {
+    it('Should duplicate multiple pages. Page with GRANT_RESTRICTED should NOT be duplicated', async () => {
       const _path1 = '/np_duplicate4';
       const _path2 = '/np_duplicate4/np_duplicate5';
       const _path3 = '/np_duplicate4/np_duplicate6';
@@ -1668,7 +1702,7 @@ describe('PageService page operations with non-public pages', () => {
       expect(duplicatedRevision1?.pageId).toStrictEqual(duplicatedPage1?._id);
       expect(duplicatedRevision3?.pageId).toStrictEqual(duplicatedPage3?._id);
     });
-    test('Should duplicate only user related pages and granted groups when onlyDuplicateUserRelatedResources is true', async () => {
+    it('Should duplicate only user related pages and granted groups when onlyDuplicateUserRelatedResources is true', async () => {
       const _path1 = '/np_duplicate7';
       const _path2 = '/np_duplicate7/np_duplicate8';
       const _path3 = '/np_duplicate7/np_duplicate9';
@@ -1724,7 +1758,7 @@ describe('PageService page operations with non-public pages', () => {
       expect(duplicatedRevision1?.body).toBe(_revision1?.body);
       expect(duplicatedRevision1?.pageId).toStrictEqual(duplicatedPage1?._id);
     });
-    test('Should duplicate all pages and granted groups when onlyDuplicateUserRelatedResources is false', async () => {
+    it('Should duplicate all pages and granted groups when onlyDuplicateUserRelatedResources is false', async () => {
       const _path1 = '/np_duplicate7';
       const _path2 = '/np_duplicate7/np_duplicate8';
       const _path3 = '/np_duplicate7/np_duplicate9';
@@ -1824,10 +1858,7 @@ describe('PageService page operations with non-public pages', () => {
       isRecursively,
       activityParameters?,
     ) => {
-      const mockedDeleteRecursivelyMainOperation = jest
-        .spyOn(crowi.pageService, 'deleteRecursivelyMainOperation')
-        .mockReturnValue(null);
-
+      const fromPath = page.path;
       const deletedPage = await crowi.pageService.deletePage(
         page,
         user,
@@ -1836,23 +1867,15 @@ describe('PageService page operations with non-public pages', () => {
         activityParameters,
       );
 
-      const argsForDeleteRecursivelyMainOperation =
-        mockedDeleteRecursivelyMainOperation.mock.calls[0];
-
-      mockedDeleteRecursivelyMainOperation.mockRestore();
-
+      // Wait for the async deleteRecursivelyMainOperation to complete
       if (isRecursively) {
-        await crowi.pageService.deleteRecursivelyMainOperation(
-          ...(argsForDeleteRecursivelyMainOperation as Parameters<
-            typeof crowi.pageService.deleteRecursivelyMainOperation
-          >),
-        );
+        await waitForPageOperationComplete(fromPath, PageActionType.Delete);
       }
 
       return deletedPage;
     };
     describe('Delete single page with grant RESTRICTED', () => {
-      test('should be able to delete', async () => {
+      it('should be able to delete', async () => {
         const _pathT = '/npdel1_awl';
         const _pageT = await Page.findOne({
           path: _pathT,
@@ -1875,7 +1898,7 @@ describe('PageService page operations with non-public pages', () => {
       });
     });
     describe('Delete single page with grant USER_GROUP', () => {
-      test('should be able to delete', async () => {
+      it('should be able to delete', async () => {
         const _path = '/npdel2_ug';
         const _page1 = await Page.findOne({
           path: _path,
@@ -1905,7 +1928,7 @@ describe('PageService page operations with non-public pages', () => {
       });
     });
     describe('Delete multiple pages with grant USER_GROUP', () => {
-      test('should be able to delete all descendants except page with GRANT_RESTRICTED', async () => {
+      it('should be able to delete all descendants except page with GRANT_RESTRICTED', async () => {
         const _pathT = '/npdel3_top';
         const _path1 = '/npdel3_top/npdel4_ug';
         const _path2 = '/npdel3_top/npdel4_ug/npdel5_ug';
@@ -2001,10 +2024,7 @@ describe('PageService page operations with non-public pages', () => {
       preventEmitting = false,
       activityParameters?,
     ) => {
-      const mockedDeleteCompletelyRecursivelyMainOperation = jest
-        .spyOn(crowi.pageService, 'deleteCompletelyRecursivelyMainOperation')
-        .mockReturnValue(null);
-
+      const fromPath = page.path;
       await crowi.pageService.deleteCompletely(
         page,
         user,
@@ -2014,16 +2034,11 @@ describe('PageService page operations with non-public pages', () => {
         activityParameters,
       );
 
-      const argsForDeleteCompletelyRecursivelyMainOperation =
-        mockedDeleteCompletelyRecursivelyMainOperation.mock.calls[0];
-
-      mockedDeleteCompletelyRecursivelyMainOperation.mockRestore();
-
+      // Wait for the async deleteCompletelyRecursivelyMainOperation to complete
       if (isRecursively) {
-        await crowi.pageService.deleteCompletelyRecursivelyMainOperation(
-          ...(argsForDeleteCompletelyRecursivelyMainOperation as Parameters<
-            typeof crowi.pageService.deleteCompletelyRecursivelyMainOperation
-          >),
+        await waitForPageOperationComplete(
+          fromPath,
+          PageActionType.DeleteCompletely,
         );
       }
 
@@ -2031,7 +2046,7 @@ describe('PageService page operations with non-public pages', () => {
     };
 
     describe('Delete single page with grant RESTRICTED', () => {
-      test('should be able to delete completely', async () => {
+      it('should be able to delete completely', async () => {
         const _path = '/npdc1_awl';
         const _page = await Page.findOne({
           path: _path,
@@ -2052,7 +2067,7 @@ describe('PageService page operations with non-public pages', () => {
       });
     });
     describe('Delete single page with grant USER_GROUP', () => {
-      test('should be able to delete completely', async () => {
+      it('should be able to delete completely', async () => {
         const _path = '/npdc2_ug';
         const _page = await Page.findOne({
           path: _path,
@@ -2075,7 +2090,7 @@ describe('PageService page operations with non-public pages', () => {
       });
     });
     describe('Delete multiple pages with grant USER_GROUP', () => {
-      test('should be able to delete all descendants completely except page with GRANT_RESTRICTED', async () => {
+      it('should be able to delete all descendants completely except page with GRANT_RESTRICTED', async () => {
         const _path1 = '/npdc3_ug';
         const _path2 = '/npdc3_ug/npdc4_ug';
         const _path3 = '/npdc3_ug/npdc4_ug/npdc5_ug';
@@ -2143,10 +2158,7 @@ describe('PageService page operations with non-public pages', () => {
       isRecursively = false,
       activityParameters?,
     ) => {
-      // mock return value
-      const mockedRevertRecursivelyMainOperation = jest
-        .spyOn(crowi.pageService, 'revertRecursivelyMainOperation')
-        .mockReturnValue(null);
+      const fromPath = page.path;
       const revertedPage = await crowi.pageService.revertDeletedPage(
         page,
         user,
@@ -2155,22 +2167,14 @@ describe('PageService page operations with non-public pages', () => {
         activityParameters,
       );
 
-      const argsForRecursivelyMainOperation =
-        mockedRevertRecursivelyMainOperation.mock.calls[0];
-
-      // restores the original implementation
-      mockedRevertRecursivelyMainOperation.mockRestore();
+      // Wait for the async revertRecursivelyMainOperation to complete
       if (isRecursively) {
-        await crowi.pageService.revertRecursivelyMainOperation(
-          ...(argsForRecursivelyMainOperation as Parameters<
-            typeof crowi.pageService.revertRecursivelyMainOperation
-          >),
-        );
+        await waitForPageOperationComplete(fromPath, PageActionType.Revert);
       }
 
       return revertedPage;
     };
-    test('should revert single deleted page with GRANT_RESTRICTED', async () => {
+    it('should revert single deleted page with GRANT_RESTRICTED', async () => {
       const trashedPage = await Page.findOne({
         path: '/trash/np_revert1',
         status: Page.STATUS_DELETED,
@@ -2211,7 +2215,7 @@ describe('PageService page operations with non-public pages', () => {
       expect(revertedPage?.grant).toBe(Page.GRANT_RESTRICTED);
       expect(pageTagRelation?.isPageTrashed).toBe(false);
     });
-    test('should revert single deleted page with GRANT_USER_GROUP', async () => {
+    it('should revert single deleted page with GRANT_USER_GROUP', async () => {
       const beforeRevertPath = '/trash/np_revert2';
       const user1 = await User.findOne({ name: 'npUser1' });
       const trashedPage = await Page.findOne({
@@ -2307,7 +2311,7 @@ describe('PageService page operations with non-public pages', () => {
       expect(revertedPage?.status).toBe(Page.STATUS_PUBLISHED);
       expect(revertedPage?.grant).toBe(Page.GRANT_PUBLIC);
     });
-    test('revert multiple pages: target page, initially non-existant page and leaf page with GRANT_USER_GROUP shoud be reverted', async () => {
+    it('revert multiple pages: target page, initially non-existant page and leaf page with GRANT_USER_GROUP shoud be reverted', async () => {
       const user = await User.findOne({ _id: npDummyUser3 });
       const beforeRevertPath1 = '/trash/np_revert5';
       const beforeRevertPath2 = '/trash/np_revert5/middle/np_revert6';