Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

N21 1212 remove user from group #4454

Merged
merged 10 commits into from
Oct 6, 2023
138 changes: 138 additions & 0 deletions apps/server/src/modules/group/domain/group.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { groupFactory, roleFactory, userDoFactory } from '@shared/testing';

import { ObjectId } from 'bson';
import { RoleReference, UserDO } from '@shared/domain';
import { Group } from './group';
import { GroupUser } from './group-user';

describe('Group (Domain Object)', () => {
describe('removeUser', () => {
describe('when the user is in the group', () => {
const setup = () => {
const user: UserDO = userDoFactory.buildWithId();
const groupUser1 = new GroupUser({
userId: user.id as string,
roleId: new ObjectId().toHexString(),
});
const groupUser2 = new GroupUser({
userId: new ObjectId().toHexString(),
roleId: new ObjectId().toHexString(),
});
const group: Group = groupFactory.build({
users: [groupUser1, groupUser2],
});

return {
user,
groupUser1,
groupUser2,
group,
};
};

it('should remove the user', () => {
const { user, group, groupUser1 } = setup();

group.removeUser(user);

expect(group.users).not.toContain(groupUser1);
});

it('should keep all other users', () => {
const { user, group, groupUser2 } = setup();

group.removeUser(user);

expect(group.users).toContain(groupUser2);
});
});

describe('when the user is not in the group', () => {
const setup = () => {
const user: UserDO = userDoFactory.buildWithId();
const groupUser2 = new GroupUser({
userId: new ObjectId().toHexString(),
roleId: new ObjectId().toHexString(),
});
const group: Group = groupFactory.build({
users: [groupUser2],
});

return {
user,
groupUser2,
group,
};
};

it('should do nothing', () => {
const { user, group, groupUser2 } = setup();

group.removeUser(user);

expect(group.users).toEqual([groupUser2]);
});
});

describe('when the group is empty', () => {
const setup = () => {
const user: UserDO = userDoFactory.buildWithId();
const group: Group = groupFactory.build({ users: [] });

return {
user,
group,
};
};

it('should stay empty', () => {
const { user, group } = setup();

group.removeUser(user);

expect(group.users).toEqual([]);
});
});
});

describe('isEmpty', () => {
describe('when no users in group exist', () => {
const setup = () => {
const group: Group = groupFactory.build({ users: [] });

return {
group,
};
};

it('should return true', () => {
const { group } = setup();

const isEmpty = group.isEmpty();

expect(isEmpty).toEqual(true);
});
});

describe('when users in group exist', () => {
const setup = () => {
const externalUserId = 'externalUserId';
const role: RoleReference = roleFactory.buildWithId();
const user: UserDO = userDoFactory.buildWithId({ roles: [role], externalId: externalUserId });
const group: Group = groupFactory.build({ users: [{ userId: user.id as string, roleId: role.id }] });

return {
group,
};
};

it('should return false', () => {
const { group } = setup();

const isEmpty = group.isEmpty();

expect(isEmpty).toEqual(false);
});
});
});
});
10 changes: 9 additions & 1 deletion apps/server/src/modules/group/domain/group.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EntityId, ExternalSource } from '@shared/domain';
import { EntityId, ExternalSource, type UserDO } from '@shared/domain';
import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object';
import { GroupTypes } from './group-types';
import { GroupUser } from './group-user';
Expand Down Expand Up @@ -37,4 +37,12 @@ export class Group extends DomainObject<GroupProps> {
get organizationId(): string | undefined {
return this.props.organizationId;
}

removeUser(user: UserDO): void {
this.props.users = this.props.users.filter((groupUser: GroupUser): boolean => groupUser.userId !== user.id);
IgorCapCoder marked this conversation as resolved.
Show resolved Hide resolved
}

isEmpty(): boolean {
return this.props.users.length === 0;
}
}
68 changes: 66 additions & 2 deletions apps/server/src/modules/group/repo/group.repo.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { EntityManager, ObjectId } from '@mikro-orm/mongodb';
import { Test, TestingModule } from '@nestjs/testing';
import { ExternalSource, SchoolEntity } from '@shared/domain';
import { ExternalSource, SchoolEntity, UserDO, User } from '@shared/domain';
import { MongoMemoryDatabaseModule } from '@shared/infra/database';
import { cleanupCollections, groupEntityFactory, groupFactory, schoolFactory } from '@shared/testing';
import {
cleanupCollections,
groupEntityFactory,
groupFactory,
roleFactory,
schoolFactory,
userDoFactory,
userFactory,
} from '@shared/testing';
import { Group, GroupProps, GroupTypes, GroupUser } from '../domain';
import { GroupEntity, GroupEntityTypes } from '../entity';
import { GroupRepo } from './group.repo';
Expand Down Expand Up @@ -82,6 +90,62 @@ describe('GroupRepo', () => {
});
});

describe('findByUser', () => {
describe('when the user has groups', () => {
const setup = async () => {
const userEntity: User = userFactory.buildWithId();
const user: UserDO = userDoFactory.build({ id: userEntity.id });
const groups: GroupEntity[] = groupEntityFactory.buildListWithId(3, {
users: [{ user: userEntity, role: roleFactory.buildWithId() }],
});

const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2);

await em.persistAndFlush([userEntity, ...groups, ...otherGroups]);
em.clear();

return {
user,
groups,
};
};

it('should return the groups', async () => {
const { user, groups } = await setup();

const result: Group[] = await repo.findByUser(user);

expect(result.map((group) => group.id).sort((a, b) => a.localeCompare(b))).toEqual(
groups.map((group) => group.id).sort((a, b) => a.localeCompare(b))
);
});
});

describe('when the user has no groups exists', () => {
const setup = async () => {
const userEntity: User = userFactory.buildWithId();
const user: UserDO = userDoFactory.build({ id: userEntity.id });

const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2);

await em.persistAndFlush([userEntity, ...otherGroups]);
em.clear();

return {
user,
};
};

it('should return an empty array', async () => {
const { user } = await setup();

const result: Group[] = await repo.findByUser(user);

expect(result).toHaveLength(0);
});
});
});

describe('findClassesForSchool', () => {
describe('when groups of type class for the school exist', () => {
const setup = async () => {
Expand Down
17 changes: 16 additions & 1 deletion apps/server/src/modules/group/repo/group.repo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { EntityManager } from '@mikro-orm/mongodb';
import { EntityManager, ObjectId } from '@mikro-orm/mongodb';
import { Injectable } from '@nestjs/common';
import { EntityId } from '@shared/domain/types';
import { type UserDO } from '@shared/domain';
import { Group, GroupProps } from '../domain';
import { GroupEntity, GroupEntityProps, GroupEntityTypes } from '../entity';
import { GroupDomainMapper } from './group-domain.mapper';
Expand Down Expand Up @@ -42,6 +43,20 @@ export class GroupRepo {
return domainObject;
}

public async findByUser(user: UserDO): Promise<Group[]> {
const entities: GroupEntity[] = await this.em.find(GroupEntity, {
users: { user: new ObjectId(user.id) },
});

const domainObjects = entities.map((entity) => {
const props: GroupProps = GroupDomainMapper.mapEntityToDomainObjectProperties(entity);

return new Group(props);
});

return domainObjects;
}

public async findClassesForSchool(schoolId: EntityId): Promise<Group[]> {
const entities: GroupEntity[] = await this.em.find(GroupEntity, {
type: GroupEntityTypes.CLASS,
Expand Down
47 changes: 46 additions & 1 deletion apps/server/src/modules/group/service/group.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { ObjectId } from '@mikro-orm/mongodb';
import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundLoggableException } from '@shared/common/loggable-exception';
import { groupFactory } from '@shared/testing';
import { groupFactory, userDoFactory } from '@shared/testing';
import { UserDO } from '@shared/domain';
import { Group } from '../domain';
import { GroupRepo } from '../repo';
import { GroupService } from './group.service';
Expand Down Expand Up @@ -120,6 +121,50 @@ describe('GroupService', () => {
});
});

describe('findByUser', () => {
describe('when groups with the user exists', () => {
const setup = () => {
const user: UserDO = userDoFactory.buildWithId();
const groups: Group[] = groupFactory.buildList(2);

groupRepo.findByUser.mockResolvedValue(groups);

return {
user,
groups,
};
};

it('should return the groups', async () => {
const { user, groups } = setup();

const result: Group[] = await service.findByUser(user);

expect(result).toEqual(groups);
});
});

describe('when no groups with the user exists', () => {
const setup = () => {
const user: UserDO = userDoFactory.buildWithId();

groupRepo.findByUser.mockResolvedValue([]);

return {
user,
};
};

it('should return empty array', async () => {
const { user } = setup();

const result: Group[] = await service.findByUser(user);

expect(result).toEqual([]);
});
});
});

describe('findClassesForSchool', () => {
describe('when the school has groups of type class', () => {
const setup = () => {
Expand Down
8 changes: 7 additions & 1 deletion apps/server/src/modules/group/service/group.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { NotFoundLoggableException } from '@shared/common/loggable-exception';
import { EntityId } from '@shared/domain';
import { EntityId, type UserDO } from '@shared/domain';
import { AuthorizationLoaderServiceGeneric } from '@src/modules/authorization';
import { Group } from '../domain';
import { GroupRepo } from '../repo';
Expand Down Expand Up @@ -31,6 +31,12 @@ export class GroupService implements AuthorizationLoaderServiceGeneric<Group> {
return group;
}

public async findByUser(user: UserDO): Promise<Group[]> {
const groups: Group[] = await this.groupRepo.findByUser(user);

return groups;
}

public async findClassesForSchool(schoolId: EntityId): Promise<Group[]> {
const group: Group[] = await this.groupRepo.findClassesForSchool(schoolId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,18 @@ describe('OidcStrategy', () => {
};
};

it('should call the OidcProvisioningService.removeExternalGroupsAndAffiliation', async () => {
const { oauthData } = setup();

await strategy.apply(oauthData);

expect(oidcProvisioningService.removeExternalGroupsAndAffiliation).toHaveBeenCalledWith(
oauthData.externalUser.externalId,
oauthData.externalGroups,
oauthData.system.systemId
);
});

it('should call the OidcProvisioningService.provisionExternalGroup for each group', async () => {
const { oauthData } = setup();

Expand Down Expand Up @@ -241,6 +253,14 @@ describe('OidcStrategy', () => {
};
};

it('should not call the OidcProvisioningService.removeExternalGroupsAndAffiliation', async () => {
const { oauthData } = setup();

await strategy.apply(oauthData);

expect(oidcProvisioningService.removeExternalGroupsAndAffiliation).not.toHaveBeenCalled();
});

it('should not call the OidcProvisioningService.provisionExternalGroup', async () => {
const { oauthData } = setup();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ export abstract class OidcProvisioningStrategy extends ProvisioningStrategy {
);

if (Configuration.get('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED') && data.externalGroups) {
// TODO: N21-1212 remove user from groups
await this.oidcProvisioningService.removeExternalGroupsAndAffiliation(
data.externalUser.externalId,
data.externalGroups,
data.system.systemId
);

await Promise.all(
data.externalGroups.map((externalGroup) =>
Expand Down
Loading
Loading