Explorar o código

refs 126149: add ExternalUserGroupSyncService tests

Futa Arai %!s(int64=2) %!d(string=hai) anos
pai
achega
903fd20ad9

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

@@ -6,7 +6,7 @@ export const ExternalGroupProviderType = { ldap: 'ldap' } as const;
 export type ExternalGroupProviderType = typeof ExternalGroupProviderType[keyof typeof ExternalGroupProviderType];
 
 export interface IExternalUserGroup extends Omit<IUserGroup, 'parent'> {
-  parent: Ref<IExternalUserGroup> | null
+  parent?: Ref<IExternalUserGroup>
   externalId: string // identifier used in external app/server
   provider: ExternalGroupProviderType
 }

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

@@ -28,4 +28,36 @@ schema.statics.createRelations = async function(userGroupIds, user) {
   return this.insertMany(documentsToInsert);
 };
 
+/**
+ * find all user and group relation
+ *
+ * @static
+ * @returns {Promise<UserGroupRelation[]>}
+ * @memberof UserGroupRelation
+ */
+schema.statics.findAllRelation = async function() {
+  return this
+    .find()
+    .populate('relatedUser')
+    .populate('relatedGroup')
+    .exec();
+};
+
+/**
+ * remove all invalid relations that has reference to unlinked document
+ */
+schema.statics.removeAllInvalidRelations = async function() {
+  return this.findAllRelation()
+    .then((relations) => {
+      // filter invalid documents
+      return relations.filter((relation) => {
+        return relation.relatedUser == null || relation.relatedGroup == null;
+      });
+    })
+    .then((invalidRelations) => {
+      const ids = invalidRelations.map((relation) => { return relation._id });
+      return this.deleteMany({ _id: { $in: ids } });
+    });
+};
+
 export default getOrCreateModel<ExternalUserGroupRelationDocument, ExternalUserGroupRelationModel>('ExternalUserGroupRelation', schema);

+ 0 - 105
apps/app/src/features/external-user-group/server/service/external-user-group-sync-service.integ.ts

@@ -1,105 +0,0 @@
-import { mock } from 'vitest-mock-extended';
-
-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 { configManager } from '~/server/service/config-manager';
-import { instanciate } from '~/server/service/external-account';
-import PassportService from '~/server/service/passport';
-
-import { ExternalGroupProviderType, ExternalUserGroupTreeNode } from '../../interfaces/external-user-group';
-
-import ExternalUserGroupSyncService from './external-user-group-sync';
-
-
-// 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 rootNode: ExternalUserGroupTreeNode = {
-      id: 'cn=rootGroup,ou=groups,dc=example,dc=org',
-      userInfos: [{
-        id: 'rootGroupUser',
-        username: 'rootGroupUser',
-        name: 'Root Group User',
-        email: 'user@rootgroup.com',
-      }],
-      childGroupNodes: [],
-      name: 'rootGroup',
-      description: 'this is a root group',
-    };
-
-    return [grandParentNode, rootNode];
-  }
-
-}
-
-const testService = new TestExternalUserGroupSyncService();
-
-describe('ExternalUserGroupSyncService.syncExternalUserGroups', () => {
-  describe('When autoGenerateUserOnGroupSync is true', () => {
-    const configParams = {
-      'external-user-group:ldap:autoGenerateUserOnGroupSync': true,
-      'external-user-group:ldap:preserveDeletedGroups': false,
-    };
-
-    beforeAll(async() => {
-      const passportServiceMock = mock<PassportService>();
-      passportServiceMock.isSameUsernameTreatedAsIdenticalUser.mockReturnValue(true);
-      passportServiceMock.isSameEmailTreatedAsIdenticalUser.mockReturnValue(true);
-      instanciate(passportServiceMock);
-
-      await configManager.updateConfigsInTheSameNamespace('crowi', configParams, true);
-    });
-
-    it('syncs groups with new users', async() => {
-      await testService.syncExternalUserGroups();
-
-      const externalUserGroups = await ExternalUserGroup.find().sort('name');
-      console.log(await ExternalUserGroupRelation.find());
-      const relation = await ExternalUserGroupRelation.find({ relatedGroup: externalUserGroups[0]._id }).populate('relatedUser');
-      console.log(relation);
-      expect(externalUserGroups.length).toBe(4);
-    });
-  });
-});

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

@@ -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();
     }
   }
 

+ 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,
+} from '../../../src/features/external-user-group/interfaces/external-user-group';
+import ExternalUserGroup, { ExternalUserGroupDocument } 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: ExternalUserGroupDocument, 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: undefined,
+  });
+  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: undefined,
+  });
+  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);
+    });
+  });
+});