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

Merge pull request #5115 from weseek/imprv/add-test-code-create-update

imprv: Add test code for PageGrantService
Haku Mizuki 4 лет назад
Родитель
Сommit
d007799d87

+ 4 - 2
packages/app/src/server/models/page.ts

@@ -57,6 +57,7 @@ export interface PageModel extends Model<PageDocument> {
   STATUS_DELETED
 }
 
+type IObjectId = mongoose.Types.ObjectId;
 const ObjectId = mongoose.Schema.Types.ObjectId;
 
 const schema = new Schema<PageDocument, PageModel>({
@@ -377,7 +378,7 @@ export default (crowi: Crowi): any => {
     const Page = this;
     const Revision = crowi.model('Revision');
     const {
-      format = 'markdown', redirectTo, grantedUserIds = [user._id], grantUserGroupId,
+      format = 'markdown', redirectTo, grantUserGroupId,
     } = options;
     let grant = options.grant;
 
@@ -404,8 +405,9 @@ export default (crowi: Crowi): any => {
       try {
         // It must check descendants as well if emptyTarget is not null
         const shouldCheckDescendants = emptyPage != null;
+        const newGrantedUserIds = grant === GRANT_OWNER ? [user._id] as IObjectId[] : undefined;
 
-        isGrantNormalized = await crowi.pageGrantService.isGrantNormalized(path, grant, grantedUserIds, grantUserGroupId, shouldCheckDescendants);
+        isGrantNormalized = await crowi.pageGrantService.isGrantNormalized(path, grant, newGrantedUserIds, grantUserGroupId, shouldCheckDescendants);
       }
       catch (err) {
         logger.error(`Failed to validate grant of page at "${path}" of grant ${grant}:`, err);

+ 106 - 40
packages/app/src/server/service/page-grant.ts

@@ -1,19 +1,22 @@
 import mongoose from 'mongoose';
-import { pagePathUtils } from '@growi/core';
+import { pagePathUtils, pathUtils } from '@growi/core';
+import escapeStringRegexp from 'escape-string-regexp';
 
 import UserGroup from '~/server/models/user-group';
 import { PageModel } from '~/server/models/page';
 import { PageQueryBuilder } from '../models/obsolete-page';
 import { isIncludesObjectId, removeDuplicates, excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
 
+const { addTrailingSlash } = pathUtils;
 const { isTopPage } = pagePathUtils;
 
 type ObjectId = mongoose.Types.ObjectId;
 
 type ComparableTarget = {
   grant: number,
-  grantedUserIds: ObjectId[],
+  grantedUserIds?: ObjectId[],
   grantedGroupId: ObjectId,
+  applicableUserIds?: ObjectId[],
   applicableGroupIds?: ObjectId[],
 };
 
@@ -25,8 +28,9 @@ type ComparableAncestor = {
 };
 
 type ComparableDescendants = {
+  isPublicExist: boolean,
   grantedUserIds: ObjectId[],
-  descendantGroupIds: ObjectId[],
+  grantedGroupIds: ObjectId[],
 };
 
 class PageGrantService {
@@ -63,16 +67,20 @@ class PageGrantService {
      * ancestor side
      */
     // GRANT_PUBLIC
-    if (ancestor.grant === Page.GRANT_PUBLIC) {
+    if (ancestor.grant === Page.GRANT_PUBLIC) { // any page can exist under public page
       // do nothing
     }
     // GRANT_OWNER
     else if (ancestor.grant === Page.GRANT_OWNER) {
-      if (target.grant !== Page.GRANT_OWNER) {
+      if (target.grantedUserIds?.length !== 1) {
+        throw Error('grantedUserIds must have one user');
+      }
+
+      if (target.grant !== Page.GRANT_OWNER) { // only GRANT_OWNER page can exist under GRANT_OWNER page
         return false;
       }
 
-      if (!ancestor.grantedUserIds[0].equals(target.grantedUserIds[0])) {
+      if (!ancestor.grantedUserIds[0].equals(target.grantedUserIds[0])) { // the grantedUser must be the same as parent's under the GRANT_OWNER page
         return false;
       }
     }
@@ -82,18 +90,22 @@ class PageGrantService {
         throw Error('applicableGroupIds and applicableUserIds are not specified');
       }
 
-      if (target.grant === Page.GRANT_PUBLIC) {
+      if (target.grant === Page.GRANT_PUBLIC) { // public page must not exist under GRANT_USER_GROUP page
         return false;
       }
 
       if (target.grant === Page.GRANT_OWNER) {
-        if (!isIncludesObjectId(ancestor.applicableUserIds, target.grantedUserIds[0])) {
+        if (target.grantedUserIds?.length !== 1) {
+          throw Error('grantedUserIds must have one user');
+        }
+
+        if (!isIncludesObjectId(ancestor.applicableUserIds, target.grantedUserIds[0])) { // GRANT_OWNER pages under GRAND_USER_GROUP page must be owned by the member of the grantedGroup of the GRAND_USER_GROUP page
           return false;
         }
       }
 
       if (target.grant === Page.GRANT_USER_GROUP) {
-        if (!isIncludesObjectId(ancestor.applicableGroupIds, target.grantedGroupId)) {
+        if (!isIncludesObjectId(ancestor.applicableGroupIds, target.grantedGroupId)) { // only child groups or the same group can exist under GRANT_USER_GROUP page
           return false;
         }
       }
@@ -107,27 +119,40 @@ class PageGrantService {
      */
 
     // GRANT_PUBLIC
-    if (target.grant === Page.GRANT_PUBLIC) {
+    if (target.grant === Page.GRANT_PUBLIC) { // any page can exist under public page
       // do nothing
     }
     // GRANT_OWNER
     else if (target.grant === Page.GRANT_OWNER) {
-      if (descendants.descendantGroupIds.length !== 0 || descendants.grantedUserIds.length > 1) {
+      if (target.grantedUserIds?.length !== 1) {
+        throw Error('grantedUserIds must have one user');
+      }
+
+      if (descendants.isPublicExist) { // public page must not exist under GRANT_OWNER page
+        return false;
+      }
+
+      if (descendants.grantedGroupIds.length !== 0 || descendants.grantedUserIds.length > 1) { // groups or more than 2 grantedUsers must not be in descendants
         return false;
       }
 
-      if (descendants.grantedUserIds.length === 1 && descendants.grantedUserIds[0].equals(target.grantedGroupId)) {
+      if (descendants.grantedUserIds.length === 1 && !descendants.grantedUserIds[0].equals(target.grantedUserIds[0])) { // if Only me page exists, then all of them must be owned by the same user as the target page
         return false;
       }
     }
     // GRANT_USER_GROUP
     else if (target.grant === Page.GRANT_USER_GROUP) {
-      if (target.applicableGroupIds == null) {
-        throw Error('applicableGroupIds must not be null');
+      if (target.applicableGroupIds == null || target.applicableUserIds == null) {
+        throw Error('applicableGroupIds and applicableUserIds must not be null');
       }
 
-      const shouldNotExistIds = excludeTestIdsFromTargetIds(descendants.descendantGroupIds, target.applicableGroupIds);
-      if (shouldNotExistIds.length !== 0) {
+      if (descendants.isPublicExist) { // public page must not exist under GRANT_USER_GROUP page
+        return false;
+      }
+
+      const shouldNotExistGroupIds = excludeTestIdsFromTargetIds(descendants.grantedGroupIds, target.applicableGroupIds);
+      const shouldNotExistUserIds = excludeTestIdsFromTargetIds(descendants.grantedUserIds, target.applicableUserIds);
+      if (shouldNotExistGroupIds.length !== 0 || shouldNotExistUserIds.length !== 0) {
         return false;
       }
     }
@@ -140,17 +165,33 @@ class PageGrantService {
    * @returns Promise<ComparableAncestor>
    */
   private async generateComparableTarget(
-      grant, grantedUserIds: ObjectId[], grantedGroupId: ObjectId, includeApplicable: boolean,
+      grant, grantedUserIds: ObjectId[] | undefined, grantedGroupId: ObjectId, includeApplicable: boolean,
   ): Promise<ComparableTarget> {
     if (includeApplicable) {
-      const applicableGroups = grantedGroupId != null ? await UserGroup.findGroupsWithDescendantsById(grantedGroupId) : null;
-      const applicableGroupIds = applicableGroups?.map(g => g._id) || null;
+      const Page = mongoose.model('Page') as PageModel;
+      const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO: Typescriptize model
+
+      let applicableUserIds: ObjectId[] | undefined;
+      let applicableGroupIds: ObjectId[] | undefined;
+
+      if (grant === Page.GRANT_USER_GROUP) {
+        const targetUserGroup = await UserGroup.findOne({ _id: grantedGroupId });
+        if (targetUserGroup == null) {
+          throw Error('Target user group does not exist');
+        }
+
+        const relatedUsers = await UserGroupRelation.find({ relatedGroup: targetUserGroup._id });
+        applicableUserIds = relatedUsers.map(u => u.relatedUser);
 
+        const applicableGroups = grantedGroupId != null ? await UserGroup.findGroupsWithDescendantsById(grantedGroupId) : null;
+        applicableGroupIds = applicableGroups?.map(g => g._id) || null;
+      }
 
       return {
         grant,
         grantedUserIds,
         grantedGroupId,
+        applicableUserIds,
         applicableGroupIds,
       };
     }
@@ -215,28 +256,51 @@ class PageGrantService {
     /*
      * make granted users list of descendant's
      */
-    // find all descendants excluding empty pages
-    const builderForDescendants = new PageQueryBuilder(Page.find({}, { _id: 0, grantedUsers: 1, grantedGroup: 1 }), false);
-    const descendants = await builderForDescendants
-      .addConditionToListOnlyDescendants(targetPath)
-      .query
-      .exec();
-
-    let grantedUsersOfGrantOwner: ObjectId[] = []; // users of GRANT_OWNER
-    const grantedGroups: ObjectId[] = [];
-    descendants.forEach((d) => {
-      if (d.grantedUsers != null) {
-        grantedUsersOfGrantOwner = grantedUsersOfGrantOwner.concat(d.grantedUsers);
-      }
-      if (d.grantedGroup != null) {
-        grantedGroups.push(d.grantedGroup);
-      }
-    });
+    const pathWithTrailingSlash = addTrailingSlash(targetPath);
+    const startsPattern = escapeStringRegexp(pathWithTrailingSlash);
+
+    const result = await Page.aggregate([
+      { // match to descendants excluding empty pages
+        $match: {
+          path: new RegExp(`^${startsPattern}`),
+          isEmpty: { $ne: true },
+        },
+      },
+      {
+        $project: {
+          _id: 0,
+          grant: 1,
+          grantedUsers: 1,
+          grantedGroup: 1,
+        },
+      },
+      { // remove duplicates from pipeline
+        $group: {
+          _id: '$grant',
+          grantedGroupSet: { $addToSet: '$grantedGroup' },
+          grantedUsersSet: { $addToSet: '$grantedUsers' },
+        },
+      },
+      { // flatten granted user set
+        $unwind: {
+          path: '$grantedUsersSet',
+        },
+      },
+    ]);
+
+    // GRANT_PUBLIC group
+    const isPublicExist = result.some(r => r._id === Page.GRANT_PUBLIC);
+    // GRANT_OWNER group
+    const grantOwnerResult = result.filter(r => r._id === Page.GRANT_OWNER)[0]; // users of GRANT_OWNER
+    const grantedUserIds: ObjectId[] = grantOwnerResult?.grantedUsersSet ?? [];
+    // GRANT_USER_GROUP group
+    const grantUserGroupResult = result.filter(r => r._id === Page.GRANT_USER_GROUP)[0]; // users of GRANT_OWNER
+    const grantedGroupIds = grantUserGroupResult?.grantedGroupSet ?? [];
 
-    const descendantGroupIds = removeDuplicates(grantedGroups);
     return {
-      grantedUserIds: grantedUsersOfGrantOwner,
-      descendantGroupIds,
+      isPublicExist,
+      grantedUserIds,
+      grantedGroupIds,
     };
   }
 
@@ -244,7 +308,9 @@ class PageGrantService {
    * About the rule of validation, see: https://dev.growi.org/61b2cdabaa330ce7d8152844
    * @returns Promise<boolean>
    */
-  async isGrantNormalized(targetPath: string, grant, grantedUserIds: ObjectId[], grantedGroupId: ObjectId, shouldCheckDescendants = false): Promise<boolean> {
+  async isGrantNormalized(
+      targetPath: string, grant, grantedUserIds: ObjectId[] | undefined, grantedGroupId: ObjectId, shouldCheckDescendants = false,
+  ): Promise<boolean> {
     if (isTopPage(targetPath)) {
       return true;
     }

+ 0 - 1
packages/app/src/server/util/compare-objectId.ts

@@ -2,7 +2,6 @@ import mongoose from 'mongoose';
 
 type IObjectId = mongoose.Types.ObjectId;
 const ObjectId = mongoose.Types.ObjectId;
-type ObjectIdLike = IObjectId | string;
 
 export const isIncludesObjectId = (arr: (IObjectId | string)[], id: IObjectId | string): boolean => {
   const _arr = arr.map(i => i.toString());

+ 369 - 0
packages/app/src/test/integration/service/page-grant.test.js

@@ -0,0 +1,369 @@
+import mongoose from 'mongoose';
+
+import { getInstance } from '../setup-crowi';
+import UserGroup from '~/server/models/user-group';
+
+/*
+ * There are 3 grant types to test.
+ * GRANT_PUBLIC, GRANT_OWNER, GRANT_USER_GROUP
+ */
+describe('PageGrantService', () => {
+  /*
+   * models
+   */
+  let User;
+  let Page;
+  let UserGroupRelation;
+
+  /*
+   * global instances
+   */
+  let crowi;
+  let pageGrantService;
+  let xssSpy;
+
+  let user1;
+  let user2;
+
+  let groupParent;
+  let groupChild;
+
+  let rootPage;
+
+  let emptyPage1;
+  let emptyPage2;
+  let emptyPage3;
+  const emptyPagePath1 = '/E1';
+  const emptyPagePath2 = '/E2';
+  const emptyPagePath3 = '/E3';
+
+  let pageRootPublic;
+  let pageRootGroupParent;
+  const pageRootPublicPath = '/Public';
+  const pageRootGroupParentPath = '/GroupParent';
+
+  let pageE1Public;
+  let pageE2User1;
+  let pageE3GroupParent;
+  let pageE3GroupChild;
+  let pageE3User1;
+  const pageE1PublicPath = '/E1/Public';
+  const pageE2User1Path = '/E2/User1';
+  const pageE3GroupParentPath = '/E3/GroupParent';
+  const pageE3GroupChildPath = '/E3/GroupChild';
+  const pageE3User1Path = '/E3/User1';
+
+  /*
+   * prepare before all tests
+   */
+  beforeAll(async() => {
+    crowi = await getInstance();
+
+    pageGrantService = crowi.pageGrantService;
+
+    User = mongoose.model('User');
+    Page = mongoose.model('Page');
+    UserGroupRelation = mongoose.model('UserGroupRelation');
+
+    // Users
+    await User.insertMany([
+      { name: 'User1', username: 'User1', email: 'user1@example.com' },
+      { name: 'User2', username: 'User2', email: 'user2@example.com' },
+    ]);
+
+    user1 = await User.findOne({ username: 'User1' });
+    user2 = await User.findOne({ username: 'User2' });
+
+    // Parent user groups
+    await UserGroup.insertMany([
+      {
+        name: 'GroupParent',
+        parent: null,
+      },
+    ]);
+    groupParent = await UserGroup.findOne({ name: 'GroupParent' });
+
+    // Child user groups
+    await UserGroup.insertMany([
+      {
+        name: 'GroupChild',
+        parent: groupParent._id,
+      },
+    ]);
+    groupChild = await UserGroup.findOne({ name: 'GroupChild' });
+
+    // UserGroupRelations
+    await UserGroupRelation.insertMany([
+      {
+        relatedGroup: groupParent._id,
+        relatedUser: user1._id,
+      },
+      {
+        relatedGroup: groupParent._id,
+        relatedUser: user2._id,
+      },
+      {
+        relatedGroup: groupChild._id,
+        relatedUser: user1._id,
+      },
+    ]);
+
+    // Root page (Depth: 0)
+    await Page.insertMany([
+      {
+        path: '/',
+        grant: Page.GRANT_PUBLIC,
+      },
+    ]);
+    rootPage = await Page.findOne({ path: '/' });
+
+    // Empty pages (Depth: 1)
+    await Page.insertMany([
+      {
+        path: emptyPagePath1,
+        grant: Page.GRANT_PUBLIC,
+        isEmpty: true,
+        parent: rootPage._id,
+      },
+      {
+        path: emptyPagePath2,
+        grant: Page.GRANT_PUBLIC,
+        isEmpty: true,
+        parent: rootPage._id,
+      },
+      {
+        path: emptyPagePath3,
+        grant: Page.GRANT_PUBLIC,
+        isEmpty: true,
+        parent: rootPage._id,
+      },
+      {
+        path: pageRootPublicPath,
+        grant: Page.GRANT_PUBLIC,
+        creator: user1,
+        lastUpdateUser: user1,
+        grantedUsers: null,
+        grantedGroup: null,
+        parent: rootPage._id,
+      },
+      {
+        path: pageRootGroupParentPath,
+        grant: Page.GRANT_USER_GROUP,
+        creator: user1,
+        lastUpdateUser: user1,
+        grantedUsers: null,
+        grantedGroup: groupParent._id,
+        parent: rootPage._id,
+      },
+    ]);
+
+    emptyPage1 = await Page.findOne({ path: emptyPagePath1 });
+    emptyPage2 = await Page.findOne({ path: emptyPagePath2 });
+    emptyPage3 = await Page.findOne({ path: emptyPagePath3 });
+
+    // Leaf pages (Depth: 2)
+    await Page.insertMany([
+      {
+        path: pageE1PublicPath,
+        grant: Page.GRANT_PUBLIC,
+        creator: user1,
+        lastUpdateUser: user1,
+        grantedUsers: null,
+        grantedGroup: null,
+        parent: emptyPage1._id,
+      },
+      {
+        path: pageE2User1Path,
+        grant: Page.GRANT_OWNER,
+        creator: user1,
+        lastUpdateUser: user1,
+        grantedUsers: [user1._id],
+        grantedGroup: null,
+        parent: emptyPage2._id,
+      },
+      {
+        path: pageE3GroupParentPath,
+        grant: Page.GRANT_USER_GROUP,
+        creator: user1,
+        lastUpdateUser: user1,
+        grantedUsers: null,
+        grantedGroup: groupParent._id,
+        parent: emptyPage3._id,
+      },
+      {
+        path: pageE3GroupChildPath,
+        grant: Page.GRANT_USER_GROUP,
+        creator: user1,
+        lastUpdateUser: user1,
+        grantedUsers: null,
+        grantedGroup: groupChild._id,
+        parent: emptyPage3._id,
+      },
+      {
+        path: pageE3User1Path,
+        grant: Page.GRANT_OWNER,
+        creator: user1,
+        lastUpdateUser: user1,
+        grantedUsers: [user1._id],
+        grantedGroup: null,
+        parent: emptyPage3._id,
+      },
+    ]);
+    pageE1Public = await Page.findOne({ path: pageE1PublicPath });
+    pageE2User1 = await Page.findOne({ path: pageE2User1Path });
+    pageE3GroupParent = await Page.findOne({ path: pageE3GroupParentPath });
+    pageE3GroupChild = await Page.findOne({ path: pageE3GroupChildPath });
+    pageE3User1 = await Page.findOne({ path: pageE3User1Path });
+
+    xssSpy = jest.spyOn(crowi.xss, 'process').mockImplementation(path => path);
+  });
+
+  describe('Test isGrantNormalized method with shouldCheckDescendants false', () => {
+    test('Should return true when Ancestor: root, Target: public', async() => {
+      const targetPath = '/NEW';
+      const grant = Page.GRANT_PUBLIC;
+      const grantedUserIds = null;
+      const grantedGroupId = null;
+      const shouldCheckDescendants = false;
+
+      const result = await pageGrantService.isGrantNormalized(targetPath, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
+
+      expect(result).toBe(true);
+    });
+
+    test('Should return true when Ancestor: root, Target: GroupParent', async() => {
+      const targetPath = '/NEW_GroupParent';
+      const grant = Page.GRANT_USER_GROUP;
+      const grantedUserIds = null;
+      const grantedGroupId = groupParent._id;
+      const shouldCheckDescendants = false;
+
+      const result = await pageGrantService.isGrantNormalized(targetPath, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
+
+      expect(result).toBe(true);
+    });
+
+    test('Should return true when Ancestor: under-root public, Target: public', async() => {
+      const targetPath = `${pageRootPublicPath}/NEW`;
+      const grant = Page.GRANT_PUBLIC;
+      const grantedUserIds = null;
+      const grantedGroupId = null;
+      const shouldCheckDescendants = false;
+
+      const result = await pageGrantService.isGrantNormalized(targetPath, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
+
+      expect(result).toBe(true);
+    });
+
+    test('Should return true when Ancestor: under-root GroupParent, Target: GroupParent', async() => {
+      const targetPath = `${pageRootGroupParentPath}/NEW`;
+      const grant = Page.GRANT_USER_GROUP;
+      const grantedUserIds = null;
+      const grantedGroupId = groupParent._id;
+      const shouldCheckDescendants = false;
+
+      const result = await pageGrantService.isGrantNormalized(targetPath, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
+
+      expect(result).toBe(true);
+    });
+
+    test('Should return true when Ancestor: public, Target: public', async() => {
+      const targetPath = `${pageE1PublicPath}/NEW`;
+      const grant = Page.GRANT_PUBLIC;
+      const grantedUserIds = null;
+      const grantedGroupId = null;
+      const shouldCheckDescendants = false;
+
+      const result = await pageGrantService.isGrantNormalized(targetPath, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
+
+      expect(result).toBe(true);
+    });
+
+    test('Should return true when Ancestor: owned by User1, Target: owned by User1', async() => {
+      const targetPath = `${pageE2User1Path}/NEW`;
+      const grant = Page.GRANT_OWNER;
+      const grantedUserIds = [user1._id];
+      const grantedGroupId = null;
+      const shouldCheckDescendants = false;
+
+      const result = await pageGrantService.isGrantNormalized(targetPath, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
+
+      expect(result).toBe(true);
+    });
+
+    test('Should return false when Ancestor: owned by GroupParent, Target: public', async() => {
+      const targetPath = `${pageE3GroupParentPath}/NEW`;
+      const grant = Page.GRANT_PUBLIC;
+      const grantedUserIds = null;
+      const grantedGroupId = null;
+      const shouldCheckDescendants = false;
+
+      const result = await pageGrantService.isGrantNormalized(targetPath, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
+
+      expect(result).toBe(false);
+    });
+
+    test('Should return false when Ancestor: owned by GroupChild, Target: GroupParent', async() => {
+      const targetPath = `${pageE3GroupChildPath}/NEW`;
+      const grant = Page.GRANT_USER_GROUP;
+      const grantedUserIds = null;
+      const grantedGroupId = groupParent._id;
+      const shouldCheckDescendants = false;
+
+      const result = await pageGrantService.isGrantNormalized(targetPath, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
+
+      expect(result).toBe(false);
+    });
+  });
+
+  describe('Test isGrantNormalized method with shouldCheckDescendants true', () => {
+    test('Should return true when Target: public, Descendant: public', async() => {
+      const targetPath = emptyPagePath1;
+      const grant = Page.GRANT_PUBLIC;
+      const grantedUserIds = null;
+      const grantedGroupId = null;
+      const shouldCheckDescendants = true;
+
+      const result = await pageGrantService.isGrantNormalized(targetPath, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
+
+      expect(result).toBe(true);
+    });
+
+    test('Should return true when Target: owned by User1, Descendant: User1 only', async() => {
+      const targetPath = emptyPagePath2;
+      const grant = Page.GRANT_OWNER;
+      const grantedUserIds = [user1._id];
+      const grantedGroupId = null;
+      const shouldCheckDescendants = true;
+
+      const result = await pageGrantService.isGrantNormalized(targetPath, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
+
+      expect(result).toBe(true);
+    });
+
+    test('Should return true when Target: owned by GroupParent, Descendant: GroupParent, GroupChild and User1', async() => {
+      const targetPath = emptyPagePath3;
+      const grant = Page.GRANT_USER_GROUP;
+      const grantedUserIds = null;
+      const grantedGroupId = groupParent._id;
+      const shouldCheckDescendants = true;
+
+      const result = await pageGrantService.isGrantNormalized(targetPath, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
+
+      expect(result).toBe(true);
+    });
+
+    test('Should return false when Target: owned by UserA, Descendant: public', async() => {
+      const targetPath = emptyPagePath1;
+      const grant = Page.GRANT_OWNER;
+      const grantedUserIds = [user1._id];
+      const grantedGroupId = null;
+      const shouldCheckDescendants = true;
+
+      const result = await pageGrantService.isGrantNormalized(targetPath, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
+
+      expect(result).toBe(false);
+    });
+  });
+
+});