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

add test data for ExternalUserGroups and grantedGroups

Futa Arai 2 лет назад
Родитель
Сommit
2f215fd10f

+ 21 - 11
apps/app/src/server/models/page.ts

@@ -101,17 +101,27 @@ const schema = new Schema<PageDocument, PageModel>({
   status: { type: String, default: STATUS_PUBLISHED, index: true },
   status: { type: String, default: STATUS_PUBLISHED, index: true },
   grant: { type: Number, default: GRANT_PUBLIC, index: true },
   grant: { type: Number, default: GRANT_PUBLIC, index: true },
   grantedUsers: [{ type: ObjectId, ref: 'User' }],
   grantedUsers: [{ type: ObjectId, ref: 'User' }],
-  grantedGroups: [{
-    type: {
-      type: String,
-      enum: Object.values(GroupType),
-      required: true,
-      default: 'UserGroup',
-    },
-    item: {
-      type: ObjectId, refPath: 'grantedGroups.type', required: true, index: true,
-    },
-  }],
+  grantedGroups: {
+    type: [{
+      type: {
+        type: String,
+        enum: Object.values(GroupType),
+        required: true,
+        default: 'UserGroup',
+      },
+      item: {
+        type: ObjectId,
+        refPath: 'grantedGroups.type',
+        required: true,
+        index: true,
+      },
+    }],
+    validate: [function(arr) {
+      if (arr == null) return true;
+      const uniqueItemValues = new Set(arr.map(e => e.item));
+      return arr.length === uniqueItemValues.size;
+    }, 'grantedGroups contains non unique item'],
+  },
   creator: { type: ObjectId, ref: 'User', index: true },
   creator: { type: ObjectId, ref: 'User', index: true },
   lastUpdateUser: { type: ObjectId, ref: 'User' },
   lastUpdateUser: { type: ObjectId, ref: 'User' },
   liker: [{ type: ObjectId, ref: 'User' }],
   liker: [{ type: ObjectId, ref: 'User' }],

+ 1 - 1
apps/app/src/server/service/page-grant.ts

@@ -627,7 +627,7 @@ class PageGrantService {
       const childrenOrItselfUserGroups = (await Promise.all(grantedUserGroupIds.map((groupId) => {
       const childrenOrItselfUserGroups = (await Promise.all(grantedUserGroupIds.map((groupId) => {
         return UserGroup.findGroupsWithDescendantsById(groupId);
         return UserGroup.findGroupsWithDescendantsById(groupId);
       }))).flat();
       }))).flat();
-      const childrenOrItselfExternalUserGroups = (await Promise.all(externalUserGroupUserIds.map((groupId) => {
+      const childrenOrItselfExternalUserGroups = (await Promise.all(grantedExternalUserGroupIds.map((groupId) => {
         return ExternalUserGroup.findGroupsWithDescendantsById(groupId);
         return ExternalUserGroup.findGroupsWithDescendantsById(groupId);
       }))).flat();
       }))).flat();
       const childrenOrItselfGroups = [...childrenOrItselfUserGroups, ...childrenOrItselfExternalUserGroups];
       const childrenOrItselfGroups = [...childrenOrItselfUserGroups, ...childrenOrItselfExternalUserGroups];

+ 274 - 37
apps/app/test/integration/models/v5.page.test.js

@@ -1,6 +1,11 @@
-import { PageGrant } from '@growi/core';
+import { PageGrant, GroupType } from '@growi/core';
 import mongoose from 'mongoose';
 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 UserGroup from '../../../src/server/models/user-group';
+import UserGroupRelation from '../../../src/server/models/user-group-relation';
 import { getInstance } from '../setup-crowi';
 import { getInstance } from '../setup-crowi';
 
 
 describe('Page', () => {
 describe('Page', () => {
@@ -16,8 +21,6 @@ describe('Page', () => {
   let Comment;
   let Comment;
   let ShareLink;
   let ShareLink;
   let PageRedirect;
   let PageRedirect;
-  let UserGroup;
-  let UserGroupRelation;
   let xssSpy;
   let xssSpy;
 
 
   let rootPage;
   let rootPage;
@@ -29,6 +32,10 @@ describe('Page', () => {
   let userGroupIdPModelA;
   let userGroupIdPModelA;
   let userGroupIdPModelB;
   let userGroupIdPModelB;
   let userGroupIdPModelC;
   let userGroupIdPModelC;
+  let externalUserGroupIdPModelIsolate;
+  let externalUserGroupIdPModelA;
+  let externalUserGroupIdPModelB;
+  let externalUserGroupIdPModelC;
 
 
   // To test updatePage overwriting descendants (prefix `upod`)
   // To test updatePage overwriting descendants (prefix `upod`)
   let upodUserA;
   let upodUserA;
@@ -44,6 +51,11 @@ describe('Page', () => {
   const upodUserGroupIdB = new mongoose.Types.ObjectId();
   const upodUserGroupIdB = new mongoose.Types.ObjectId();
   const upodUserGroupIdC = new mongoose.Types.ObjectId();
   const upodUserGroupIdC = new mongoose.Types.ObjectId();
   const upodUserGroupIdAB = new mongoose.Types.ObjectId();
   const upodUserGroupIdAB = new mongoose.Types.ObjectId();
+  const upodExternalUserGroupIdA = new mongoose.Types.ObjectId();
+  const upodExternalUserGroupIdAIsolated = new mongoose.Types.ObjectId();
+  const upodExternalUserGroupIdB = new mongoose.Types.ObjectId();
+  const upodExternalUserGroupIdC = new mongoose.Types.ObjectId();
+  const upodExternalUserGroupIdAB = new mongoose.Types.ObjectId();
   const upodPageIdgAB1 = new mongoose.Types.ObjectId();
   const upodPageIdgAB1 = new mongoose.Types.ObjectId();
   const upodPageIdPublic2 = new mongoose.Types.ObjectId();
   const upodPageIdPublic2 = new mongoose.Types.ObjectId();
   const upodPageIdPublic3 = new mongoose.Types.ObjectId();
   const upodPageIdPublic3 = new mongoose.Types.ObjectId();
@@ -139,6 +151,73 @@ describe('Page', () => {
       },
       },
     ]);
     ]);
 
 
+    // ExternalUserGroups
+    await ExternalUserGroup.insertMany([
+      {
+        _id: upodExternalUserGroupIdAB,
+        name: 'upodExternalGroupAB',
+        parent: null,
+        externalId: 'upodExternalGroupAB',
+        provider: ExternalGroupProviderType.ldap,
+      },
+      {
+        _id: upodExternalUserGroupIdA,
+        name: 'upodExternalGroupA',
+        parent: upodExternalUserGroupIdAB,
+        externalId: 'upodExternalGroupA',
+        provider: ExternalGroupProviderType.ldap,
+      },
+      {
+        _id: upodExternalUserGroupIdAIsolated,
+        name: 'upodExternalGroupAIsolated',
+        parent: null,
+        externalId: 'upodExternalGroupAIsolated',
+        provider: ExternalGroupProviderType.ldap,
+      },
+      {
+        _id: upodExternalUserGroupIdB,
+        name: 'upodExternalGroupB',
+        parent: upodExternalUserGroupIdAB,
+        externalId: 'upodExternalGroupB',
+        provider: ExternalGroupProviderType.ldap,
+      },
+      {
+        _id: upodExternalUserGroupIdC,
+        name: 'upodExternalGroupC',
+        parent: null,
+        externalId: 'upodExternalGroupC',
+        provider: ExternalGroupProviderType.ldap,
+      },
+    ]);
+
+    // ExternalUserGroupRelations
+    await ExternalUserGroupRelation.insertMany([
+      {
+        relatedGroup: upodExternalUserGroupIdAB,
+        relatedUser: upodUserA._id,
+      },
+      {
+        relatedGroup: upodExternalUserGroupIdAB,
+        relatedUser: upodUserB._id,
+      },
+      {
+        relatedGroup: upodExternalUserGroupIdA,
+        relatedUser: upodUserA._id,
+      },
+      {
+        relatedGroup: upodExternalUserGroupIdAIsolated,
+        relatedUser: upodUserA._id,
+      },
+      {
+        relatedGroup: upodExternalUserGroupIdB,
+        relatedUser: upodUserB._id,
+      },
+      {
+        relatedGroup: upodExternalUserGroupIdC,
+        relatedUser: upodUserC._id,
+      },
+    ]);
+
     // Pages
     // Pages
     await Page.insertMany([
     await Page.insertMany([
       // case 1
       // case 1
@@ -149,7 +228,10 @@ describe('Page', () => {
         creator: upodUserA,
         creator: upodUserA,
         lastUpdateUser: upodUserA,
         lastUpdateUser: upodUserA,
         grantedUsers: null,
         grantedUsers: null,
-        grantedGroups: [{ item: upodUserGroupIdAB, type: 'UserGroup' }],
+        grantedGroups: [
+          { item: upodUserGroupIdAB, type: GroupType.userGroup },
+          { item: upodExternalUserGroupIdAB, type: GroupType.externalUserGroup },
+        ],
         parent: rootPage._id,
         parent: rootPage._id,
       },
       },
       {
       {
@@ -158,7 +240,10 @@ describe('Page', () => {
         creator: upodUserB,
         creator: upodUserB,
         lastUpdateUser: upodUserB,
         lastUpdateUser: upodUserB,
         grantedUsers: null,
         grantedUsers: null,
-        grantedGroups: [{ item: upodUserGroupIdB, type: 'UserGroup' }],
+        grantedGroups: [
+          { item: upodUserGroupIdB, type: GroupType.userGroup },
+          { item: upodExternalUserGroupIdB, type: GroupType.externalUserGroup },
+        ],
         parent: upodPageIdgAB1,
         parent: upodPageIdgAB1,
       },
       },
       {
       {
@@ -187,7 +272,10 @@ describe('Page', () => {
         creator: upodUserA,
         creator: upodUserA,
         lastUpdateUser: upodUserA,
         lastUpdateUser: upodUserA,
         grantedUsers: null,
         grantedUsers: null,
-        grantedGroups: [{ item: upodUserGroupIdA, type: 'UserGroup' }],
+        grantedGroups: [
+          { item: upodUserGroupIdA, type: GroupType.userGroup },
+          { item: upodExternalUserGroupIdA, type: GroupType.externalUserGroup },
+        ],
         parent: upodPageIdPublic2,
         parent: upodPageIdPublic2,
       },
       },
       {
       {
@@ -196,7 +284,10 @@ describe('Page', () => {
         creator: upodUserA,
         creator: upodUserA,
         lastUpdateUser: upodUserA,
         lastUpdateUser: upodUserA,
         grantedUsers: null,
         grantedUsers: null,
-        grantedGroups: [{ item: upodUserGroupIdAIsolated, type: 'UserGroup' }],
+        grantedGroups: [
+          { item: upodUserGroupIdAIsolated, type: GroupType.userGroup },
+          { item: upodExternalUserGroupIdAIsolated, type: GroupType.externalUserGroup },
+        ],
         parent: upodPageIdPublic2,
         parent: upodPageIdPublic2,
       },
       },
       {
       {
@@ -225,7 +316,10 @@ describe('Page', () => {
         creator: upodUserA,
         creator: upodUserA,
         lastUpdateUser: upodUserA,
         lastUpdateUser: upodUserA,
         grantedUsers: null,
         grantedUsers: null,
-        grantedGroups: [{ item: upodUserGroupIdAB, type: 'UserGroup' }],
+        grantedGroups: [
+          { item: upodUserGroupIdAB, type: GroupType.userGroup },
+          { item: upodExternalUserGroupIdAB, type: GroupType.externalUserGroup },
+        ],
         parent: upodPageIdPublic3,
         parent: upodPageIdPublic3,
       },
       },
       {
       {
@@ -234,7 +328,10 @@ describe('Page', () => {
         creator: upodUserB,
         creator: upodUserB,
         lastUpdateUser: upodUserB,
         lastUpdateUser: upodUserB,
         grantedUsers: null,
         grantedUsers: null,
-        grantedGroups: [{ item: upodUserGroupIdB, type: 'UserGroup' }],
+        grantedGroups: [
+          { item: upodUserGroupIdB, type: GroupType.userGroup },
+          { item: upodExternalUserGroupIdB, type: GroupType.externalUserGroup },
+        ],
         parent: upodPageIdPublic3,
         parent: upodPageIdPublic3,
       },
       },
       {
       {
@@ -263,7 +360,10 @@ describe('Page', () => {
         creator: upodUserA,
         creator: upodUserA,
         lastUpdateUser: upodUserA,
         lastUpdateUser: upodUserA,
         grantedUsers: null,
         grantedUsers: null,
-        grantedGroups: [{ item: upodUserGroupIdA, type: 'UserGroup' }],
+        grantedGroups: [
+          { item: upodUserGroupIdA, type: GroupType.userGroup },
+          { item: upodExternalUserGroupIdA, type: GroupType.externalUserGroup },
+        ],
         parent: upodPageIdPublic4,
         parent: upodPageIdPublic4,
       },
       },
       {
       {
@@ -272,7 +372,10 @@ describe('Page', () => {
         creator: upodUserC,
         creator: upodUserC,
         lastUpdateUser: upodUserC,
         lastUpdateUser: upodUserC,
         grantedUsers: null,
         grantedUsers: null,
-        grantedGroups: [{ item: upodUserGroupIdC, type: 'UserGroup' }],
+        grantedGroups: [
+          { item: upodUserGroupIdC, type: GroupType.userGroup },
+          { item: upodExternalUserGroupIdC, type: GroupType.externalUserGroup },
+        ],
         parent: upodPageIdPublic4,
         parent: upodPageIdPublic4,
       },
       },
       // case 5
       // case 5
@@ -292,7 +395,10 @@ describe('Page', () => {
         creator: upodUserA,
         creator: upodUserA,
         lastUpdateUser: upodUserA,
         lastUpdateUser: upodUserA,
         grantedUsers: null,
         grantedUsers: null,
-        grantedGroups: [{ item: upodUserGroupIdA, type: 'UserGroup' }],
+        grantedGroups: [
+          { item: upodUserGroupIdA, type: GroupType.userGroup },
+          { item: upodExternalUserGroupIdA, type: GroupType.externalUserGroup },
+        ],
         parent: upodPageIdPublic5,
         parent: upodPageIdPublic5,
       },
       },
       {
       {
@@ -343,8 +449,6 @@ describe('Page', () => {
     Comment = mongoose.model('Comment');
     Comment = mongoose.model('Comment');
     ShareLink = mongoose.model('ShareLink');
     ShareLink = mongoose.model('ShareLink');
     PageRedirect = mongoose.model('PageRedirect');
     PageRedirect = mongoose.model('PageRedirect');
-    UserGroup = mongoose.model('UserGroup');
-    UserGroupRelation = mongoose.model('UserGroupRelation');
 
 
     dummyUser1 = await User.findOne({ username: 'v5DummyUser1' });
     dummyUser1 = await User.findOne({ username: 'v5DummyUser1' });
 
 
@@ -446,6 +550,82 @@ describe('Page', () => {
       },
       },
     ]);
     ]);
 
 
+    externalUserGroupIdPModelIsolate = new mongoose.Types.ObjectId();
+    externalUserGroupIdPModelA = new mongoose.Types.ObjectId();
+    externalUserGroupIdPModelB = new mongoose.Types.ObjectId();
+    externalUserGroupIdPModelC = new mongoose.Types.ObjectId();
+    await ExternalUserGroup.insertMany([
+      {
+        _id: externalUserGroupIdPModelIsolate,
+        name: 'pModel_externalGroupIsolate',
+        externalId: 'pModel_externalGroupIsolate',
+        provider: ExternalGroupProviderType.ldap,
+      },
+      {
+        _id: externalUserGroupIdPModelA,
+        name: 'pModel_externalGroupA',
+        externalId: 'pModel_externalGroupA',
+        provider: ExternalGroupProviderType.ldap,
+      },
+      {
+        _id: externalUserGroupIdPModelB,
+        name: 'pModel_externalGroupB',
+        parent: externalUserGroupIdPModelA,
+        externalId: 'pModel_externalGroupB',
+        provider: ExternalGroupProviderType.ldap,
+      },
+      {
+        _id: externalUserGroupIdPModelC,
+        name: 'pModel_externalGroupC',
+        parent: externalUserGroupIdPModelB,
+        externalId: 'pModel_externalGroupC',
+        provider: ExternalGroupProviderType.ldap,
+      },
+    ]);
+
+    await ExternalUserGroupRelation.insertMany([
+      {
+        relatedGroup: externalUserGroupIdPModelIsolate,
+        relatedUser: pModelUserId1,
+        createdAt: new Date(),
+      },
+      {
+        relatedGroup: externalUserGroupIdPModelIsolate,
+        relatedUser: pModelUserId2,
+        createdAt: new Date(),
+      },
+      {
+        relatedGroup: externalUserGroupIdPModelA,
+        relatedUser: pModelUserId1,
+        createdAt: new Date(),
+      },
+      {
+        relatedGroup: externalUserGroupIdPModelA,
+        relatedUser: pModelUserId2,
+        createdAt: new Date(),
+      },
+      {
+        relatedGroup: externalUserGroupIdPModelA,
+        relatedUser: pModelUserId3,
+        createdAt: new Date(),
+      },
+      {
+        relatedGroup: externalUserGroupIdPModelB,
+        relatedUser: pModelUserId2,
+        createdAt: new Date(),
+      },
+      {
+        relatedGroup: externalUserGroupIdPModelB,
+        relatedUser: pModelUserId3,
+        createdAt: new Date(),
+      },
+      {
+        relatedGroup: externalUserGroupIdPModelC,
+        relatedUser: pModelUserId3,
+        createdAt: new Date(),
+      },
+    ]);
+
     /**
     /**
      * update
      * update
      * mup_ => model update
      * mup_ => model update
@@ -610,7 +790,10 @@ describe('Page', () => {
       {
       {
         path: '/mup20',
         path: '/mup20',
         grant: Page.GRANT_USER_GROUP,
         grant: Page.GRANT_USER_GROUP,
-        grantedGroups: [{ item: userGroupIdPModelA, type: 'UserGroup' }],
+        grantedGroups: [
+          { item: userGroupIdPModelA, type: GroupType.userGroup },
+          { item: externalUserGroupIdPModelA, type: GroupType.externalUserGroup },
+        ],
         creator: pModelUserId1,
         creator: pModelUserId1,
         lastUpdateUser: pModelUserId1,
         lastUpdateUser: pModelUserId1,
         isEmpty: false,
         isEmpty: false,
@@ -638,7 +821,10 @@ describe('Page', () => {
       {
       {
         path: '/mup22/mup23',
         path: '/mup22/mup23',
         grant: Page.GRANT_USER_GROUP,
         grant: Page.GRANT_USER_GROUP,
-        grantedGroups: [{ item: userGroupIdPModelA, type: 'UserGroup' }],
+        grantedGroups: [
+          { item: userGroupIdPModelA, type: GroupType.userGroup },
+          { item: externalUserGroupIdPModelA, type: GroupType.externalUserGroup },
+        ],
         creator: pModelUserId1,
         creator: pModelUserId1,
         lastUpdateUser: pModelUserId1,
         lastUpdateUser: pModelUserId1,
         isEmpty: false,
         isEmpty: false,
@@ -696,7 +882,10 @@ describe('Page', () => {
         _id: pageIdUpd16,
         _id: pageIdUpd16,
         path: '/mup29_A',
         path: '/mup29_A',
         grant: Page.GRANT_USER_GROUP,
         grant: Page.GRANT_USER_GROUP,
-        grantedGroups: [{ item: userGroupIdPModelA, type: 'UserGroup' }],
+        grantedGroups: [
+          { item: userGroupIdPModelA, type: GroupType.userGroup },
+          { item: externalUserGroupIdPModelA, type: GroupType.externalUserGroup },
+        ],
         creator: pModelUserId1,
         creator: pModelUserId1,
         lastUpdateUser: pModelUserId1,
         lastUpdateUser: pModelUserId1,
         isEmpty: false,
         isEmpty: false,
@@ -717,7 +906,10 @@ describe('Page', () => {
         _id: pageIdUpd17,
         _id: pageIdUpd17,
         path: '/mup31_A',
         path: '/mup31_A',
         grant: Page.GRANT_USER_GROUP,
         grant: Page.GRANT_USER_GROUP,
-        grantedGroups: [{ item: userGroupIdPModelA, type: 'UserGroup' }],
+        grantedGroups: [
+          { item: userGroupIdPModelA, type: GroupType.userGroup },
+          { item: externalUserGroupIdPModelA, type: GroupType.externalUserGroup },
+        ],
         creator: pModelUserId1,
         creator: pModelUserId1,
         lastUpdateUser: pModelUserId1,
         lastUpdateUser: pModelUserId1,
         isEmpty: false,
         isEmpty: false,
@@ -738,7 +930,10 @@ describe('Page', () => {
         _id: pageIdUpd18,
         _id: pageIdUpd18,
         path: '/mup33_C',
         path: '/mup33_C',
         grant: Page.GRANT_USER_GROUP,
         grant: Page.GRANT_USER_GROUP,
-        grantedGroups: [{ item: userGroupIdPModelC, type: 'UserGroup' }],
+        grantedGroups: [
+          { item: userGroupIdPModelC, type: GroupType.userGroup },
+          { item: externalUserGroupIdPModelC, type: GroupType.externalUserGroup },
+        ],
         creator: pModelUserId3,
         creator: pModelUserId3,
         lastUpdateUser: pModelUserId3,
         lastUpdateUser: pModelUserId3,
         isEmpty: false,
         isEmpty: false,
@@ -1004,7 +1199,13 @@ describe('Page', () => {
           expect(_page1).toBeTruthy();
           expect(_page1).toBeTruthy();
           expect(_page2).toBeTruthy();
           expect(_page2).toBeTruthy();
 
 
-          const options = { grant: Page.GRANT_USER_GROUP, grantUserGroupIds: [{ item: userGroupIdPModelA, type: 'UserGroup' }] };
+          const options = {
+            grant: Page.GRANT_USER_GROUP,
+            grantUserGroupIds: [
+              { item: userGroupIdPModelA, type: GroupType.userGroup },
+              { item: externalUserGroupIdPModelA, type: GroupType.externalUserGroup },
+            ],
+          };
           const updatedPage = await updatePage(_page2, 'new', 'old', pModelUser1, options); // from GRANT_PUBLIC to GRANT_USER_GROUP(userGroupIdPModelA)
           const updatedPage = await updatePage(_page2, 'new', 'old', pModelUser1, options); // from GRANT_PUBLIC to GRANT_USER_GROUP(userGroupIdPModelA)
 
 
           const page1 = await Page.findById(_page1._id);
           const page1 = await Page.findById(_page1._id);
@@ -1016,7 +1217,7 @@ describe('Page', () => {
 
 
           // check page2 grant and group
           // check page2 grant and group
           expect(page2.grant).toBe(Page.GRANT_USER_GROUP);
           expect(page2.grant).toBe(Page.GRANT_USER_GROUP);
-          expect(page2.grantedGroups.map(g => g.item)).toStrictEqual([userGroupIdPModelA]);
+          expect(page2.grantedGroups.map(g => g.item)).toStrictEqual([userGroupIdPModelA, externalUserGroupIdPModelA]);
         });
         });
 
 
         test('successfully change to GRANT_USER_GROUP from GRANT_RESTRICTED if parent page is GRANT_PUBLIC', async() => {
         test('successfully change to GRANT_USER_GROUP from GRANT_RESTRICTED if parent page is GRANT_PUBLIC', async() => {
@@ -1026,7 +1227,13 @@ describe('Page', () => {
           const _page1 = await Page.findOne({ path: _path1, grant: Page.GRANT_RESTRICTED });
           const _page1 = await Page.findOne({ path: _path1, grant: Page.GRANT_RESTRICTED });
           expect(_page1).toBeTruthy();
           expect(_page1).toBeTruthy();
 
 
-          const options = { grant: Page.GRANT_USER_GROUP, grantUserGroupIds: [{ item: userGroupIdPModelA, type: 'UserGroup' }] };
+          const options = {
+            grant: Page.GRANT_USER_GROUP,
+            grantUserGroupIds: [
+              { item: userGroupIdPModelA, type: GroupType.userGroup },
+              { item: externalUserGroupIdPModelA, type: GroupType.externalUserGroup },
+            ],
+          };
           const updatedPage = await updatePage(_page1, 'new', 'old', pModelUser1, options); // from GRANT_RESTRICTED to GRANT_USER_GROUP(userGroupIdPModelA)
           const updatedPage = await updatePage(_page1, 'new', 'old', pModelUser1, options); // from GRANT_RESTRICTED to GRANT_USER_GROUP(userGroupIdPModelA)
 
 
           const page1 = await Page.findById(_page1._id);
           const page1 = await Page.findById(_page1._id);
@@ -1036,7 +1243,7 @@ describe('Page', () => {
 
 
           // updated page
           // updated page
           expect(page1.grant).toBe(Page.GRANT_USER_GROUP);
           expect(page1.grant).toBe(Page.GRANT_USER_GROUP);
-          expect(page1.grantedGroups.map(g => g.item)).toStrictEqual([userGroupIdPModelA]);
+          expect(page1.grantedGroups.map(g => g.item)).toStrictEqual([userGroupIdPModelA, externalUserGroupIdPModelA]);
 
 
           // parent's grant check
           // parent's grant check
           const parent = await Page.findById(page1.parent);
           const parent = await Page.findById(page1.parent);
@@ -1056,7 +1263,13 @@ describe('Page', () => {
           expect(_page1).toBeTruthy();
           expect(_page1).toBeTruthy();
           expect(_page2).toBeTruthy();
           expect(_page2).toBeTruthy();
 
 
-          const options = { grant: Page.GRANT_USER_GROUP, grantUserGroupIds: [{ item: userGroupIdPModelA, type: 'UserGroup' }] };
+          const options = {
+            grant: Page.GRANT_USER_GROUP,
+            grantUserGroupIds: [
+              { item: userGroupIdPModelA, type: GroupType.userGroup },
+              { item: externalUserGroupIdPModelA, type: GroupType.externalUserGroup },
+            ],
+          };
           const updatedPage = await updatePage(_page2, 'new', 'old', pModelUser1, options); // from GRANT_OWNER to GRANT_USER_GROUP(userGroupIdPModelA)
           const updatedPage = await updatePage(_page2, 'new', 'old', pModelUser1, options); // from GRANT_OWNER to GRANT_USER_GROUP(userGroupIdPModelA)
 
 
           const page1 = await Page.findById(_page1._id);
           const page1 = await Page.findById(_page1._id);
@@ -1068,7 +1281,7 @@ describe('Page', () => {
 
 
           // grant check
           // grant check
           expect(page2.grant).toBe(Page.GRANT_USER_GROUP);
           expect(page2.grant).toBe(Page.GRANT_USER_GROUP);
-          expect(page2.grantedGroups.map(g => g.item)).toStrictEqual([userGroupIdPModelA]);
+          expect(page2.grantedGroups.map(g => g.item)).toStrictEqual([userGroupIdPModelA, externalUserGroupIdPModelA]);
           expect(page2.grantedUsers.length).toBe(0);
           expect(page2.grantedUsers.length).toBe(0);
         });
         });
       });
       });
@@ -1085,7 +1298,7 @@ describe('Page', () => {
           expect(_page1).toBeTruthy();
           expect(_page1).toBeTruthy();
           expect(_page2).toBeTruthy();
           expect(_page2).toBeTruthy();
 
 
-          const options = { grant: Page.GRANT_USER_GROUP, grantUserGroupIds: [{ item: userGroupIdPModelB, type: 'UserGroup' }] };
+          const options = { grant: Page.GRANT_USER_GROUP, grantUserGroupIds: [{ item: userGroupIdPModelB, type: GroupType.userGroup }] };
 
 
           // First round
           // First round
           // Group relation(parent -> child): userGroupIdPModelA -> userGroupIdPModelB -> userGroupIdPModelC
           // Group relation(parent -> child): userGroupIdPModelA -> userGroupIdPModelB -> userGroupIdPModelC
@@ -1104,7 +1317,7 @@ describe('Page', () => {
 
 
           // Second round
           // Second round
           // Update group to groupC which is a grandchild from pageA's point of view
           // Update group to groupC which is a grandchild from pageA's point of view
-          const secondRoundOptions = { grant: Page.GRANT_USER_GROUP, grantUserGroupIds: [{ item: userGroupIdPModelC, type: 'UserGroup' }] }; // from GRANT_USER_GROUP(userGroupIdPModelB) to GRANT_USER_GROUP(userGroupIdPModelC)
+          const secondRoundOptions = { grant: Page.GRANT_USER_GROUP, grantUserGroupIds: [{ item: userGroupIdPModelC, type: GroupType.userGroup }] }; // from GRANT_USER_GROUP(userGroupIdPModelB) to GRANT_USER_GROUP(userGroupIdPModelC)
           // undo grantedGroups populate to prevent Page.hydrate error
           // undo grantedGroups populate to prevent Page.hydrate error
           _page2.grantedGroups.forEach((group) => {
           _page2.grantedGroups.forEach((group) => {
             group.item = group.item._id;
             group.item = group.item._id;
@@ -1133,7 +1346,13 @@ describe('Page', () => {
           // group parent check
           // group parent check
           expect(_groupIsolated.parent).toBeUndefined(); // should have no parent
           expect(_groupIsolated.parent).toBeUndefined(); // should have no parent
 
 
-          const options = { grant: Page.GRANT_USER_GROUP, grantUserGroupIds: [{ item: userGroupIdPModelIsolate, type: 'UserGroup' }] };
+          const options = {
+            grant: Page.GRANT_USER_GROUP,
+            grantUserGroupIds: [
+              { item: userGroupIdPModelIsolate, type: GroupType.userGroup },
+              { item: externalUserGroupIdPModelIsolate, type: GroupType.externalUserGroup },
+            ],
+          };
           await expect(updatePage(_page2, 'new', 'old', pModelUser1, options)) // from GRANT_OWNER to GRANT_USER_GROUP(userGroupIdPModelIsolate)
           await expect(updatePage(_page2, 'new', 'old', pModelUser1, options)) // from GRANT_OWNER to GRANT_USER_GROUP(userGroupIdPModelIsolate)
             .rejects.toThrow(new Error('The selected grant or grantedGroup is not assignable to this page.'));
             .rejects.toThrow(new Error('The selected grant or grantedGroup is not assignable to this page.'));
 
 
@@ -1158,7 +1377,13 @@ describe('Page', () => {
           expect(_page1).toBeTruthy();
           expect(_page1).toBeTruthy();
           expect(_page2).toBeTruthy();
           expect(_page2).toBeTruthy();
 
 
-          const options = { grant: Page.GRANT_USER_GROUP, grantUserGroupIds: [{ item: userGroupIdPModelA, type: 'UserGroup' }] };
+          const options = {
+            grant: Page.GRANT_USER_GROUP,
+            grantUserGroupIds: [
+              { item: userGroupIdPModelA, type: GroupType.userGroup },
+              { item: externalUserGroupIdPModelA, type: GroupType.externalUserGroup },
+            ],
+          };
 
 
           // Group relation(parent -> child): userGroupIdPModelA -> userGroupIdPModelB -> userGroupIdPModelC
           // Group relation(parent -> child): userGroupIdPModelA -> userGroupIdPModelB -> userGroupIdPModelC
           // this should fail because the groupC is a descendant of groupA
           // this should fail because the groupC is a descendant of groupA
@@ -1188,7 +1413,7 @@ describe('Page', () => {
           expect(_page1).toBeTruthy();
           expect(_page1).toBeTruthy();
           expect(_page2).toBeTruthy();
           expect(_page2).toBeTruthy();
 
 
-          const options = { grant: Page.GRANT_USER_GROUP, grantUserGroupIds: [{ item: userGroupIdPModelA, type: 'UserGroup' }] };
+          const options = { grant: Page.GRANT_USER_GROUP, grantUserGroupIds: [{ item: userGroupIdPModelA, type: GroupType.userGroup }] };
           await expect(updatePage(_page2, 'new', 'old', pModelUser1, options)) // from GRANT_OWNER to GRANT_USER_GROUP(userGroupIdPModelA)
           await expect(updatePage(_page2, 'new', 'old', pModelUser1, options)) // from GRANT_OWNER to GRANT_USER_GROUP(userGroupIdPModelA)
             .rejects.toThrow(new Error('The selected grant or grantedGroup is not assignable to this page.'));
             .rejects.toThrow(new Error('The selected grant or grantedGroup is not assignable to this page.'));
 
 
@@ -1301,7 +1526,10 @@ describe('Page', () => {
       // Update
       // Update
       const options = {
       const options = {
         grant: PageGrant.GRANT_USER_GROUP,
         grant: PageGrant.GRANT_USER_GROUP,
-        grantUserGroupIds: [{ item: upodUserGroupIdAB, type: 'UserGroup' }],
+        grantUserGroupIds: [
+          { item: upodUserGroupIdAB, type: GroupType.userGroup },
+          { item: upodExternalUserGroupIdAB, type: GroupType.externalUserGroup },
+        ],
         overwriteScopesOfDescendants: true,
         overwriteScopesOfDescendants: true,
       };
       };
       const updatedPage = await updatePage(upodPagePublic, 'newRevisionBody', 'oldRevisionBody', upodUserA, options);
       const updatedPage = await updatePage(upodPagePublic, 'newRevisionBody', 'oldRevisionBody', upodUserA, options);
@@ -1312,11 +1540,11 @@ describe('Page', () => {
 
 
       // Changed
       // Changed
       const newGrant = PageGrant.GRANT_USER_GROUP;
       const newGrant = PageGrant.GRANT_USER_GROUP;
-      const newGrantedGroup = upodUserGroupIdAB;
+      const newGrantedGroups = [upodUserGroupIdAB, upodExternalUserGroupIdAB];
       expect(updatedPage.grant).toBe(newGrant);
       expect(updatedPage.grant).toBe(newGrant);
-      expect(updatedPage.grantedGroups.map(g => g.item._id)).toStrictEqual([newGrantedGroup]);
+      expect(updatedPage.grantedGroups.map(g => g.item._id)).toStrictEqual(newGrantedGroups);
       expect(upodPagegABUpdated.grant).toBe(newGrant);
       expect(upodPagegABUpdated.grant).toBe(newGrant);
-      expect(upodPagegABUpdated.grantedGroups.map(g => g.item)).toStrictEqual([newGrantedGroup]);
+      expect(upodPagegABUpdated.grantedGroups.map(g => g.item)).toStrictEqual(newGrantedGroups);
       // Not changed
       // Not changed
       expect(upodPagegBUpdated.grant).toBe(PageGrant.GRANT_USER_GROUP);
       expect(upodPagegBUpdated.grant).toBe(PageGrant.GRANT_USER_GROUP);
       expect(upodPagegBUpdated.grantedGroups).toStrictEqual(upodPagegB.grantedGroups);
       expect(upodPagegBUpdated.grantedGroups).toStrictEqual(upodPagegB.grantedGroups);
@@ -1341,7 +1569,10 @@ describe('Page', () => {
       // Update
       // Update
       const options = {
       const options = {
         grant: PageGrant.GRANT_USER_GROUP,
         grant: PageGrant.GRANT_USER_GROUP,
-        grantUserGroupIds: [{ item: upodUserGroupIdAB, type: 'UserGroup' }],
+        grantUserGroupIds: [
+          { item: upodUserGroupIdAB, type: GroupType.userGroup },
+          { item: upodExternalUserGroupIdAB, type: GroupType.externalUserGroup },
+        ],
         overwriteScopesOfDescendants: true,
         overwriteScopesOfDescendants: true,
       };
       };
       const updatedPagePromise = updatePage(upodPagePublic, 'newRevisionBody', 'oldRevisionBody', upodUserA, options);
       const updatedPagePromise = updatePage(upodPagePublic, 'newRevisionBody', 'oldRevisionBody', upodUserA, options);
@@ -1366,7 +1597,10 @@ describe('Page', () => {
       // Update
       // Update
       const options = {
       const options = {
         grant: PageGrant.GRANT_USER_GROUP,
         grant: PageGrant.GRANT_USER_GROUP,
-        grantUserGroupIds: [{ item: upodUserGroupIdAB, type: 'UserGroup' }],
+        grantUserGroupIds: [
+          { item: upodUserGroupIdAB, type: GroupType.userGroup },
+          { item: upodExternalUserGroupIdAB, type: GroupType.externalUserGroup },
+        ],
         overwriteScopesOfDescendants: true,
         overwriteScopesOfDescendants: true,
       };
       };
       const updatedPagePromise = updatePage(upodPagePublic, 'newRevisionBody', 'oldRevisionBody', upodUserA, options);
       const updatedPagePromise = updatePage(upodPagePublic, 'newRevisionBody', 'oldRevisionBody', upodUserA, options);
@@ -1386,7 +1620,10 @@ describe('Page', () => {
       // Update
       // Update
       const options = {
       const options = {
         grant: PageGrant.GRANT_USER_GROUP,
         grant: PageGrant.GRANT_USER_GROUP,
-        grantUserGroupIds: [{ item: upodUserGroupIdAB, type: 'UserGroup' }],
+        grantUserGroupIds: [
+          { item: upodUserGroupIdAB, type: GroupType.userGroup },
+          { item: upodExternalUserGroupIdAB, type: GroupType.externalUserGroup },
+        ],
         overwriteScopesOfDescendants: true,
         overwriteScopesOfDescendants: true,
       };
       };
       const updatedPagePromise = updatePage(upodPagePublic, 'newRevisionBody', 'oldRevisionBody', upodUserA, options);
       const updatedPagePromise = updatePage(upodPagePublic, 'newRevisionBody', 'oldRevisionBody', upodUserA, options);