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

Merge pull request #7858 from weseek/feat/120030-126149-ldap-group-sync-service-tests

Feat/120030 126149 ldap group sync service tests
Yuki Takei 2 лет назад
Родитель
Сommit
b3d01f14d0

+ 1 - 1
apps/app/src/features/external-user-group/interfaces/external-user-group.ts

@@ -42,7 +42,7 @@ export type ExternalUserInfo = {
 
 // Data structure to express the tree structure of external groups, before converting to ExternalUserGroup model
 export interface ExternalUserGroupTreeNode {
-  id: string
+  id: string // external group id
   userInfos: ExternalUserInfo[]
   childGroupNodes: ExternalUserGroupTreeNode[]
   name: string

+ 35 - 8
apps/app/src/features/external-user-group/server/models/external-user-group-relation.integ.ts

@@ -15,25 +15,33 @@ const userSchema = new mongoose.Schema({
 const User = mongoose.model('User', userSchema);
 
 describe('ExternalUserGroupRelation model', () => {
+  let user1;
   const userId1 = new mongoose.Types.ObjectId();
-  const groupId1 = new mongoose.Types.ObjectId();
-  const groupId2 = new mongoose.Types.ObjectId();
+
+  beforeAll(async() => {
+    user1 = await User.create({
+      _id: userId1, name: 'user1', username: 'user1', email: 'user1@example.com',
+    });
+  });
+
+  afterEach(async() => {
+    await ExternalUserGroup.deleteMany();
+    await ExternalUserGroupRelation.deleteMany();
+  });
 
   describe('createRelations', () => {
-    let user1;
+    const groupId1 = new mongoose.Types.ObjectId();
+    const groupId2 = new mongoose.Types.ObjectId();
 
     beforeAll(async() => {
-      user1 = await User.create({
-        _id: userId1, name: 'user1', username: 'user1', email: 'user1@example.com',
-      });
-      await ExternalUserGroup.insertMany(
+      await ExternalUserGroup.insertMany([
         {
           _id: groupId1, name: 'test group 1', externalId: 'testExternalId', provider: 'testProvider',
         },
         {
           _id: groupId2, name: 'test group 2', externalId: 'testExternalId', provider: 'testProvider',
         },
-      );
+      ]);
     });
 
     it('creates relation for user', async() => {
@@ -45,4 +53,23 @@ describe('ExternalUserGroupRelation model', () => {
       expect(idCombinations).toStrictEqual([[groupId1, userId1], [groupId2, userId1]]);
     });
   });
+
+  describe('removeAllInvalidRelations', () => {
+    const groupId1 = new mongoose.Types.ObjectId();
+    const groupId2 = new mongoose.Types.ObjectId();
+
+    beforeAll(async() => {
+      await ExternalUserGroupRelation.createRelations([groupId1, groupId2], user1);
+    });
+
+    it('removes invalid relations', async() => {
+      const relationsBeforeRemoval = await ExternalUserGroupRelation.find();
+      expect(relationsBeforeRemoval.length).not.toBe(0);
+
+      await ExternalUserGroupRelation.removeAllInvalidRelations();
+
+      const relationsAfterRemoval = await ExternalUserGroupRelation.find();
+      expect(relationsAfterRemoval.length).toBe(0);
+    });
+  });
 });

+ 4 - 0
apps/app/src/features/external-user-group/server/models/external-user-group-relation.ts

@@ -28,4 +28,8 @@ schema.statics.createRelations = UserGroupRelation.createRelations;
 
 schema.statics.removeAllByUserGroups = UserGroupRelation.removeAllByUserGroups;
 
+schema.statics.findAllRelation = UserGroupRelation.findAllRelation;
+
+schema.statics.removeAllInvalidRelations = UserGroupRelation.removeAllInvalidRelations;
+
 export default getOrCreateModel<ExternalUserGroupRelationDocument, ExternalUserGroupRelationModel>('ExternalUserGroupRelation', schema);

+ 6 - 8
apps/app/src/features/external-user-group/server/models/external-user-group.ts

@@ -36,16 +36,14 @@ schema.plugin(mongoosePaginate);
  * @returns ExternalUserGroupDocument[]
  */
 schema.statics.findAndUpdateOrCreateGroup = async function(name: string, externalId: string, provider: string, description?: string, parentId?: string) {
-  // create without parent
-  if (parentId == null) {
-    return this.findOneAndUpdate({ externalId }, { name, description, provider }, { upsert: true, new: true });
+  let parent: ExternalUserGroupDocument | null = null;
+  if (parentId != null) {
+    parent = await this.findOne({ _id: parentId });
+    if (parent == null) {
+      throw Error('Parent does not exist.');
+    }
   }
 
-  // create with parent
-  const parent = await this.findOne({ _id: parentId });
-  if (parent == null) {
-    throw Error('Parent does not exist.');
-  }
   return this.findOneAndUpdate({ externalId }, {
     name, description, provider, parent,
   }, { upsert: true, new: true });

+ 2 - 1
apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group-relation.ts

@@ -1,6 +1,7 @@
 import { ErrorV3 } from '@growi/core';
 import { Router, Request } from 'express';
 
+import { IExternalUserGroupRelationHasId } from '~/features/external-user-group/interfaces/external-user-group';
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
 import Crowi from '~/server/crowi';
 import loggerFactory from '~/utils/logger';
@@ -33,7 +34,7 @@ module.exports = (crowi: Crowi): Router => {
     try {
       const relations = await ExternalUserGroupRelation.find({ relatedGroup: { $in: query.groupIds } }).populate('relatedUser');
 
-      let relationsOfChildGroups = null;
+      let relationsOfChildGroups: IExternalUserGroupRelationHasId[] | null = null;
       if (Array.isArray(query.childGroupIds)) {
         const _relationsOfChildGroups = await ExternalUserGroupRelation.find({ relatedGroup: { $in: query.childGroupIds } }).populate('relatedUser');
         relationsOfChildGroups = _relationsOfChildGroups.map(relation => serializeUserGroupRelationSecurely(relation)); // serialize

+ 4 - 9
apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts

@@ -4,10 +4,8 @@ import {
   body, param, query, validationResult,
 } from 'express-validator';
 
-import ExternalUserGroup, { ExternalUserGroupDocument } from '~/features/external-user-group/server/models/external-user-group';
-import ExternalUserGroupRelation, {
-  ExternalUserGroupRelationDocument,
-} from '~/features/external-user-group/server/models/external-user-group-relation';
+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 { SupportedAction } from '~/interfaces/activity';
 import Crowi from '~/server/crowi';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
@@ -18,7 +16,7 @@ import { configManager } from '~/server/service/config-manager';
 import UserGroupService from '~/server/service/user-group';
 import loggerFactory from '~/utils/logger';
 
-import LdapUserGroupSyncService from '../../service/ldap-user-group-sync-service';
+import LdapUserGroupSyncService from '../../service/ldap-user-group-sync';
 
 const logger = loggerFactory('growi:routes:apiv3:external-user-group');
 
@@ -141,10 +139,7 @@ module.exports = (crowi: Crowi): Router => {
 
       try {
         const userGroups = await (crowi.userGroupService as UserGroupService)
-          .removeCompletelyByRootGroupId<
-            ExternalUserGroupDocument,
-            ExternalUserGroupRelationDocument
-          >(deleteGroupId, actionName, transferToUserGroupId, req.user, ExternalUserGroup, ExternalUserGroupRelation);
+          .removeCompletelyByRootGroupId(deleteGroupId, actionName, transferToUserGroupId, req.user, ExternalUserGroup, ExternalUserGroupRelation);
 
         const parameters = { action: SupportedAction.ACTION_ADMIN_USER_GROUP_DELETE };
         activityEvent.emit('update', res.locals.activity._id, parameters);

+ 6 - 6
apps/app/src/features/external-user-group/server/service/external-user-group-sync-service.ts → apps/app/src/features/external-user-group/server/service/external-user-group-sync.ts

@@ -35,9 +35,9 @@ abstract class ExternalUserGroupSyncService {
     const syncNode = async(node: ExternalUserGroupTreeNode, parentId?: string) => {
       const externalUserGroup = await this.createUpdateExternalUserGroup(node, parentId);
       existingExternalUserGroupIds.push(externalUserGroup._id);
-      node.childGroupNodes.forEach((childNode) => {
-        syncNode(childNode, externalUserGroup._id);
-      });
+      await Promise.all(node.childGroupNodes.map((childNode) => {
+        return syncNode(childNode, externalUserGroup._id);
+      }));
     };
 
     await Promise.all(trees.map((root) => {
@@ -47,6 +47,7 @@ abstract class ExternalUserGroupSyncService {
     const preserveDeletedLdapGroups: boolean = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:preserveDeletedGroups`);
     if (!preserveDeletedLdapGroups) {
       await ExternalUserGroup.deleteMany({ _id: { $nin: existingExternalUserGroupIds }, groupProviderType: this.groupProviderType });
+      await ExternalUserGroupRelation.removeAllInvalidRelations();
     }
   }
 
@@ -72,7 +73,7 @@ abstract class ExternalUserGroupSyncService {
 
           // remove existing relations from list to create
           const existingRelations = await ExternalUserGroupRelation.find({ relatedGroup: { $in: userGroupIds }, relatedUser: user._id });
-          const existingGroupIds = existingRelations.map(r => r.relatedGroup);
+          const existingGroupIds = existingRelations.map(r => r.relatedGroup.toString());
           const groupIdsToCreateRelation = excludeTestIdsFromTargetIds(userGroupIds, existingGroupIds);
 
           await ExternalUserGroupRelation.createRelations(groupIdsToCreateRelation, user);
@@ -104,8 +105,7 @@ abstract class ExternalUserGroupSyncService {
     const externalAccount = await getExternalAccount();
 
     if (externalAccount != null) {
-      await externalAccount.populate('user');
-      return externalAccount.user;
+      return (await externalAccount.populate<{user: IUserHasId | null}>('user')).user;
     }
     return null;
   }

+ 1 - 1
apps/app/src/features/external-user-group/server/service/ldap-user-group-sync-service.ts → apps/app/src/features/external-user-group/server/service/ldap-user-group-sync.ts

@@ -6,7 +6,7 @@ import {
   ExternalGroupProviderType, ExternalUserGroupTreeNode, ExternalUserInfo, LdapGroupMembershipAttributeType,
 } from '../../interfaces/external-user-group';
 
-import ExternalUserGroupSyncService from './external-user-group-sync-service';
+import ExternalUserGroupSyncService from './external-user-group-sync';
 
 class LdapUserGroupSyncService extends ExternalUserGroupSyncService {
 

+ 1 - 2
apps/app/src/server/models/external-account.ts

@@ -15,7 +15,7 @@ const uniqueValidator = require('mongoose-unique-validator');
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
 
-interface ExternalAccountDocument extends IExternalAccount, Document {}
+export interface ExternalAccountDocument extends IExternalAccount, Document {}
 
 export interface ExternalAccountModel extends Model<ExternalAccountDocument> {
   [x:string]: any, // for old methods
@@ -72,7 +72,6 @@ schema.statics.findOrRegister = function(
     nameToBeRegistered?: string,
     mailToBeRegistered?: string,
 ): Promise<IExternalAccountHasId> {
-//
   return this.findOne({ providerType, accountId })
     .then((account) => {
     // ExternalAccount is found

+ 6 - 8
apps/app/src/server/models/user-group.ts

@@ -71,16 +71,14 @@ schema.statics.countUserGroups = function() {
 };
 
 schema.statics.createGroup = async function(name, description, parentId) {
-  // create without parent
-  if (parentId == null) {
-    return this.create({ name, description });
+  let parent: UserGroupDocument | null = null;
+  if (parentId != null) {
+    parent = await this.findOne({ _id: parentId });
+    if (parent == null) {
+      throw Error('Parent does not exist.');
+    }
   }
 
-  // create with parent
-  const parent = await this.findOne({ _id: parentId });
-  if (parent == null) {
-    throw Error('Parent does not exist.');
-  }
   return this.create({ name, description, parent });
 };
 

+ 2 - 3
apps/app/src/server/service/external-account.ts

@@ -1,11 +1,10 @@
 import { ErrorV3 } from '^/../../packages/core/dist';
 
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
-import { IExternalAccountHasId } from '~/interfaces/external-account';
 import loggerFactory from '~/utils/logger';
 
 import { NullUsernameToBeRegisteredError } from '../models/errors';
-import ExternalAccount from '../models/external-account';
+import ExternalAccount, { ExternalAccountDocument } from '../models/external-account';
 
 import PassportService from './passport';
 
@@ -23,7 +22,7 @@ class ExternalAccountService {
   async getOrCreateUser(
       userInfo: {id: string, username: string, name?: string, email?: string},
       providerId: string,
-  ): Promise<IExternalAccountHasId | undefined> {
+  ): Promise<ExternalAccountDocument | undefined> {
     // get option
     const isSameUsernameTreatedAsIdenticalUser = this.passportService.isSameUsernameTreatedAsIdenticalUser(providerId);
     const isSameEmailTreatedAsIdenticalUser = this.passportService.isSameEmailTreatedAsIdenticalUser(providerId);

+ 3 - 6
apps/app/src/server/service/user-group.ts

@@ -113,13 +113,10 @@ class UserGroupService {
     return userGroup.save();
   }
 
-  async removeCompletelyByRootGroupId<
-    D extends UserGroupDocument,
-    RD extends UserGroupRelationDocument,
-  >(
+  async removeCompletelyByRootGroupId(
       deleteRootGroupId, action, transferToUserGroupId, user,
-      userGroupModel: Model<D> & UserGroupModel = UserGroup,
-      userGroupRelationModel: Model<RD> & UserGroupRelationModel = UserGroupRelation,
+      userGroupModel: Model<UserGroupDocument> & UserGroupModel = UserGroup,
+      userGroupRelationModel: Model<UserGroupRelationDocument> & UserGroupRelationModel = UserGroupRelation,
   ) {
     const rootGroup = await userGroupModel.findById(deleteRootGroupId);
     if (rootGroup == null) {

+ 266 - 0
apps/app/test/integration/service/external-user-group-sync.test.ts

@@ -0,0 +1,266 @@
+import { IUserHasId } from '@growi/core';
+import mongoose, { Types } from 'mongoose';
+
+import {
+  ExternalGroupProviderType, ExternalUserGroupTreeNode, IExternalUserGroup, IExternalUserGroupHasId,
+} 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 ExternalUserGroupSyncService from '../../../src/features/external-user-group/server/service/external-user-group-sync';
+import ExternalAccount from '../../../src/server/models/external-account';
+import { configManager } from '../../../src/server/service/config-manager';
+import { instanciate } from '../../../src/server/service/external-account';
+import PassportService from '../../../src/server/service/passport';
+import { getInstance } from '../setup-crowi';
+
+
+// dummy class to implement generateExternalUserGroupTrees which returns test data
+class TestExternalUserGroupSyncService extends ExternalUserGroupSyncService {
+
+  constructor() {
+    super(ExternalGroupProviderType.ldap, 'ldap');
+  }
+
+  async generateExternalUserGroupTrees(): Promise<ExternalUserGroupTreeNode[]> {
+    const childNode: ExternalUserGroupTreeNode = {
+      id: 'cn=childGroup,ou=groups,dc=example,dc=org',
+      userInfos: [{
+        id: 'childGroupUser',
+        username: 'childGroupUser',
+        name: 'Child Group User',
+        email: 'user@childgroup.com',
+      }],
+      childGroupNodes: [],
+      name: 'childGroup',
+      description: 'this is a child group',
+    };
+    const parentNode: ExternalUserGroupTreeNode = {
+      id: 'cn=parentGroup,ou=groups,dc=example,dc=org',
+      // name is undefined
+      userInfos: [{
+        id: 'parentGroupUser',
+        username: 'parentGroupUser',
+        email: 'user@parentgroup.com',
+      }],
+      childGroupNodes: [childNode],
+      name: 'parentGroup',
+      description: 'this is a parent group',
+    };
+    const grandParentNode: ExternalUserGroupTreeNode = {
+      id: 'cn=grandParentGroup,ou=groups,dc=example,dc=org',
+      // email is undefined
+      userInfos: [{
+        id: 'grandParentGroupUser',
+        username: 'grandParentGroupUser',
+        name: 'Grand Parent Group User',
+      }],
+      childGroupNodes: [parentNode],
+      name: 'grandParentGroup',
+      description: 'this is a grand parent group',
+    };
+
+    const previouslySyncedNode: ExternalUserGroupTreeNode = {
+      id: 'cn=previouslySyncedGroup,ou=groups,dc=example,dc=org',
+      userInfos: [{
+        id: 'previouslySyncedGroupUser',
+        username: 'previouslySyncedGroupUser',
+        name: 'Root Group User',
+        email: 'user@previouslySyncedgroup.com',
+      }],
+      childGroupNodes: [],
+      name: 'previouslySyncedGroup',
+      description: 'this is a previouslySynced group',
+    };
+
+    return [grandParentNode, previouslySyncedNode];
+  }
+
+}
+
+const testService = new TestExternalUserGroupSyncService();
+
+const checkGroup = (group: IExternalUserGroupHasId, expected: Omit<IExternalUserGroup, 'createdAt'>) => {
+  const actual = {
+    name: group.name,
+    parent: group.parent,
+    description: group.description,
+    externalId: group.externalId,
+    provider: group.provider,
+  };
+  expect(actual).toStrictEqual(expected);
+};
+
+const checkSync = async(autoGenerateUserOnGroupSync = true) => {
+  const grandParentGroup = await ExternalUserGroup.findOne({ name: 'grandParentGroup' });
+  checkGroup(grandParentGroup, {
+    externalId: 'cn=grandParentGroup,ou=groups,dc=example,dc=org',
+    name: 'grandParentGroup',
+    description: 'this is a grand parent group',
+    provider: 'ldap',
+    parent: null,
+  });
+  const grandParentGroupRelations = await ExternalUserGroupRelation
+    .find({ relatedGroup: grandParentGroup._id });
+  if (autoGenerateUserOnGroupSync) {
+    expect(grandParentGroupRelations.length).toBe(3);
+    const grandParentGroupUser = (await grandParentGroupRelations[0].populate<{relatedUser: IUserHasId}>('relatedUser'))?.relatedUser;
+    expect(grandParentGroupUser?.username).toBe('grandParentGroupUser');
+    const parentGroupUser = (await grandParentGroupRelations[1].populate<{relatedUser: IUserHasId}>('relatedUser'))?.relatedUser;
+    expect(parentGroupUser?.username).toBe('parentGroupUser');
+    const childGroupUser = (await grandParentGroupRelations[2].populate<{relatedUser: IUserHasId}>('relatedUser'))?.relatedUser;
+    expect(childGroupUser?.username).toBe('childGroupUser');
+  }
+  else {
+    expect(grandParentGroupRelations.length).toBe(0);
+  }
+
+  const parentGroup = await ExternalUserGroup.findOne({ name: 'parentGroup' });
+  checkGroup(parentGroup, {
+    externalId: 'cn=parentGroup,ou=groups,dc=example,dc=org',
+    name: 'parentGroup',
+    description: 'this is a parent group',
+    provider: 'ldap',
+    parent: grandParentGroup._id,
+  });
+  const parentGroupRelations = await ExternalUserGroupRelation
+    .find({ relatedGroup: parentGroup._id });
+  if (autoGenerateUserOnGroupSync) {
+    expect(parentGroupRelations.length).toBe(2);
+    const parentGroupUser = (await parentGroupRelations[0].populate<{relatedUser: IUserHasId}>('relatedUser'))?.relatedUser;
+    expect(parentGroupUser?.username).toBe('parentGroupUser');
+    const childGroupUser = (await parentGroupRelations[1].populate<{relatedUser: IUserHasId}>('relatedUser'))?.relatedUser;
+    expect(childGroupUser?.username).toBe('childGroupUser');
+  }
+  else {
+    expect(parentGroupRelations.length).toBe(0);
+  }
+
+  const childGroup = await ExternalUserGroup.findOne({ name: 'childGroup' });
+  checkGroup(childGroup, {
+    externalId: 'cn=childGroup,ou=groups,dc=example,dc=org',
+    name: 'childGroup',
+    description: 'this is a child group',
+    provider: 'ldap',
+    parent: parentGroup._id,
+  });
+  const childGroupRelations = await ExternalUserGroupRelation
+    .find({ relatedGroup: childGroup._id });
+  if (autoGenerateUserOnGroupSync) {
+    expect(childGroupRelations.length).toBe(1);
+    const childGroupUser = (await childGroupRelations[0].populate<{relatedUser: IUserHasId}>('relatedUser'))?.relatedUser;
+    expect(childGroupUser?.username).toBe('childGroupUser');
+  }
+  else {
+    expect(childGroupRelations.length).toBe(0);
+  }
+
+  const previouslySyncedGroup = await ExternalUserGroup.findOne({ name: 'previouslySyncedGroup' });
+  checkGroup(previouslySyncedGroup, {
+    externalId: 'cn=previouslySyncedGroup,ou=groups,dc=example,dc=org',
+    name: 'previouslySyncedGroup',
+    description: 'this is a previouslySynced group',
+    provider: 'ldap',
+    parent: null,
+  });
+  const previouslySyncedGroupRelations = await ExternalUserGroupRelation
+    .find({ relatedGroup: previouslySyncedGroup._id });
+  if (autoGenerateUserOnGroupSync) {
+    expect(previouslySyncedGroupRelations.length).toBe(1);
+    const previouslySyncedGroupUser = (await previouslySyncedGroupRelations[0].populate<{relatedUser: IUserHasId}>('relatedUser'))?.relatedUser;
+    expect(previouslySyncedGroupUser?.username).toBe('previouslySyncedGroupUser');
+  }
+  else {
+    expect(previouslySyncedGroupRelations.length).toBe(0);
+  }
+};
+
+describe('ExternalUserGroupSyncService.syncExternalUserGroups', () => {
+  let crowi;
+
+  beforeAll(async() => {
+    crowi = await getInstance();
+    const passportService = new PassportService(crowi);
+    instanciate(passportService);
+  });
+
+  beforeEach(async() => {
+    await ExternalUserGroup.create({
+      name: 'nameBeforeEdit',
+      description: 'this is a description before edit',
+      externalId: 'cn=previouslySyncedGroup,ou=groups,dc=example,dc=org',
+      provider: 'ldap',
+    });
+  });
+
+  afterEach(async() => {
+    await ExternalUserGroup.deleteMany();
+    await ExternalUserGroupRelation.deleteMany();
+    await mongoose.model('User')
+      .deleteMany({ username: { $in: ['childGroupUser', 'parentGroupUser', 'grandParentGroupUser', 'previouslySyncedGroupUser'] } });
+    await ExternalAccount.deleteMany({ accountId: { $in: ['childGroupUser', 'parentGroupUser', 'grandParentGroupUser', 'previouslySyncedGroupUser'] } });
+  });
+
+  describe('When autoGenerateUserOnGroupSync is true', () => {
+    const configParams = {
+      'external-user-group:ldap:autoGenerateUserOnGroupSync': true,
+      'external-user-group:ldap:preserveDeletedGroups': false,
+    };
+
+    beforeAll(async() => {
+      await configManager.updateConfigsInTheSameNamespace('crowi', configParams, true);
+    });
+
+    // eslint-disable-next-line jest/expect-expect
+    it('syncs groups with new users', async() => {
+      await testService.syncExternalUserGroups();
+      await checkSync();
+    });
+  });
+
+  describe('When autoGenerateUserOnGroupSync is false', () => {
+    const configParams = {
+      'external-user-group:ldap:autoGenerateUserOnGroupSync': false,
+      'external-user-group:ldap:preserveDeletedGroups': true,
+    };
+
+    beforeAll(async() => {
+      await configManager.updateConfigsInTheSameNamespace('crowi', configParams, true);
+    });
+
+    // eslint-disable-next-line jest/expect-expect
+    it('syncs groups without new users', async() => {
+      await testService.syncExternalUserGroups();
+      await checkSync(false);
+    });
+  });
+
+  describe('When preserveDeletedGroups is false', () => {
+    const configParams = {
+      'external-user-group:ldap:autoGenerateUserOnGroupSync': true,
+      'external-user-group:ldap:preserveDeletedGroups': false,
+    };
+
+    beforeAll(async() => {
+      await configManager.updateConfigsInTheSameNamespace('crowi', configParams, true);
+
+      const groupId = new Types.ObjectId();
+      const userId = new Types.ObjectId();
+
+      await ExternalUserGroup.create({
+        _id: groupId,
+        name: 'non existent group',
+        externalId: 'cn=nonExistentGroup,ou=groups,dc=example,dc=org',
+        provider: 'ldap',
+      });
+      await mongoose.model('User').create({ _id: userId, username: 'nonExistentGroupUser' });
+      await ExternalUserGroupRelation.create({ relatedUser: userId, relatedGroup: groupId });
+    });
+
+    it('syncs groups and deletes groups that do not exist externally', async() => {
+      await testService.syncExternalUserGroups();
+      await checkSync();
+      expect(await ExternalUserGroup.countDocuments()).toBe(4);
+      expect(await ExternalUserGroupRelation.countDocuments()).toBe(7);
+    });
+  });
+});