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-1207 show group members #4479

Merged
merged 28 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
479d969
add boolean to classInfo WIP (todo: unit/api test)
IgorCapCoder Oct 13, 2023
44e671c
tests
IgorCapCoder Oct 16, 2023
46a9dc3
Merge branch 'main' into N21-1264-new-class-page-extension
IgorCapCoder Oct 16, 2023
1f25827
N21-1207 adds get /groups/:id
arnegns Oct 17, 2023
a71b361
add group to seed data
IgorCapCoder Oct 17, 2023
7d59cc0
N21-1207 fix tests
arnegns Oct 17, 2023
73294c1
N21-1207 adds script for adding permission GROUP_LIST and GROUP_VIEW
arnegns Oct 17, 2023
2110657
N21-1207 adds GROUP_LIST permission to superhero
arnegns Oct 18, 2023
a8c2d1d
N21-1207 revert factory changes
arnegns Oct 18, 2023
13f0b61
N21-1207 adjust group rule and tests
arnegns Oct 18, 2023
d44defe
N21-1207 review fixes
arnegns Oct 18, 2023
71724bc
N21-1207 changes enum api property
arnegns Oct 18, 2023
a715750
Merge branch 'main' into N21-1207-show-group-members
arnegns Oct 18, 2023
a54c382
Merge branch 'main' into N21-1207-show-group-members
arnegns Oct 19, 2023
8da3fc0
Merge branch 'main' into N21-1207-show-group-members
arnegns Oct 20, 2023
4b389db
Merge remote-tracking branch 'origin/N21-1207-show-group-members' int…
arnegns Oct 20, 2023
714abe6
N21-1207 fix import
arnegns Oct 20, 2023
385ad0e
Merge branch 'main' into N21-1207-show-group-members
arnegns Oct 20, 2023
1974338
Merge branch 'main' into N21-1264-new-class-page-extension
arnegns Oct 20, 2023
fd9c188
Merge branch 'N21-1264-new-class-page-extension' into N21-1207-show-g…
arnegns Oct 20, 2023
0042a07
Merge branch 'main' into N21-1264-new-class-page-extension
arnegns Oct 20, 2023
e43a703
Merge branch 'N21-1264-new-class-page-extension' into N21-1207-show-g…
arnegns Oct 20, 2023
5306fa1
N21-1264 fix seed data
arnegns Oct 20, 2023
9df1077
N21-1264 fixes seed data
arnegns Oct 20, 2023
7f928f3
Merge branch 'N21-1264-new-class-page-extension' into N21-1207-show-g…
arnegns Oct 20, 2023
42a0879
Merge branch 'main' into N21-1207-show-group-members
arnegns Oct 20, 2023
9b79248
N21-1207 fixes test
arnegns Oct 20, 2023
e0847a4
N21-1207 fixes test 2
arnegns Oct 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/server/src/modules/authorization/rule-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
TaskRule,
TeamRule,
UserRule,
GroupRule,
} from '@shared/domain/rules';
import { UserLoginMigrationRule } from '@shared/domain/rules/user-login-migration.rule';
import { courseFactory, setupEntities, userFactory } from '@shared/testing';
Expand All @@ -33,6 +34,7 @@ describe('RuleManager', () => {
let boardDoRule: DeepMocked<BoardDoRule>;
let contextExternalToolRule: DeepMocked<ContextExternalToolRule>;
let userLoginMigrationRule: DeepMocked<UserLoginMigrationRule>;
let groupRule: DeepMocked<GroupRule>;

beforeAll(async () => {
await setupEntities();
Expand All @@ -52,6 +54,7 @@ describe('RuleManager', () => {
{ provide: BoardDoRule, useValue: createMock<BoardDoRule>() },
{ provide: ContextExternalToolRule, useValue: createMock<ContextExternalToolRule>() },
{ provide: UserLoginMigrationRule, useValue: createMock<UserLoginMigrationRule>() },
{ provide: GroupRule, useValue: createMock<GroupRule>() },
],
}).compile();

Expand All @@ -68,6 +71,7 @@ describe('RuleManager', () => {
boardDoRule = await module.get(BoardDoRule);
contextExternalToolRule = await module.get(ContextExternalToolRule);
userLoginMigrationRule = await module.get(UserLoginMigrationRule);
groupRule = await module.get(GroupRule);
});

afterEach(() => {
Expand Down Expand Up @@ -98,6 +102,7 @@ describe('RuleManager', () => {
boardDoRule.isApplicable.mockReturnValueOnce(false);
contextExternalToolRule.isApplicable.mockReturnValueOnce(false);
userLoginMigrationRule.isApplicable.mockReturnValueOnce(false);
groupRule.isApplicable.mockReturnValueOnce(false);

return { user, object, context };
};
Expand All @@ -119,6 +124,7 @@ describe('RuleManager', () => {
expect(boardDoRule.isApplicable).toBeCalled();
expect(contextExternalToolRule.isApplicable).toBeCalled();
expect(userLoginMigrationRule.isApplicable).toBeCalled();
expect(groupRule.isApplicable).toBeCalled();
});

it('should return CourseRule', () => {
Expand Down Expand Up @@ -148,6 +154,7 @@ describe('RuleManager', () => {
boardDoRule.isApplicable.mockReturnValueOnce(false);
contextExternalToolRule.isApplicable.mockReturnValueOnce(false);
userLoginMigrationRule.isApplicable.mockReturnValueOnce(false);
groupRule.isApplicable.mockReturnValueOnce(false);

return { user, object, context };
};
Expand Down Expand Up @@ -177,6 +184,7 @@ describe('RuleManager', () => {
boardDoRule.isApplicable.mockReturnValueOnce(false);
contextExternalToolRule.isApplicable.mockReturnValueOnce(false);
userLoginMigrationRule.isApplicable.mockReturnValueOnce(false);
groupRule.isApplicable.mockReturnValueOnce(false);

return { user, object, context };
};
Expand Down
5 changes: 4 additions & 1 deletion apps/server/src/modules/authorization/rule-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
TaskRule,
TeamRule,
UserRule,
GroupRule,
} from '@shared/domain/rules';
import { ContextExternalToolRule } from '@shared/domain/rules/context-external-tool.rule';
import { UserLoginMigrationRule } from '@shared/domain/rules/user-login-migration.rule';
Expand All @@ -33,7 +34,8 @@ export class RuleManager {
private readonly schoolExternalToolRule: SchoolExternalToolRule,
private readonly boardDoRule: BoardDoRule,
private readonly contextExternalToolRule: ContextExternalToolRule,
private readonly userLoginMigrationRule: UserLoginMigrationRule
private readonly userLoginMigrationRule: UserLoginMigrationRule,
private readonly groupRule: GroupRule
) {
this.rules = [
this.courseRule,
Expand All @@ -48,6 +50,7 @@ export class RuleManager {
this.boardDoRule,
this.contextExternalToolRule,
this.userLoginMigrationRule,
this.groupRule,
];
}

Expand Down
118 changes: 117 additions & 1 deletion apps/server/src/modules/group/controller/api-test/group.api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { ClassEntity } from '@src/modules/class/entity';
import { classEntityFactory } from '@src/modules/class/entity/testing/factory/class.entity.factory';
import { ServerTestModule } from '@src/modules/server';
import { ObjectId } from 'bson';
import { GroupEntity, GroupEntityTypes } from '../../entity';
import { ClassRootType } from '../../uc/dto/class-root-type';
import { ClassInfoSearchListResponse, ClassSortBy } from '../dto';
Expand All @@ -41,7 +42,7 @@ describe('Group (API)', () => {
await app.close();
});

describe('findClassesForSchool', () => {
describe('[GET] /groups/class', () => {
describe('when an admin requests a list of classes', () => {
const setup = async () => {
const school: SchoolEntity = schoolFactory.buildWithId();
Expand Down Expand Up @@ -157,4 +158,119 @@ describe('Group (API)', () => {
});
});
});

describe('[GET] /groups/:groupId', () => {
describe('when authorized user requests a group', () => {
describe('when group exists', () => {
const setup = async () => {
const school: SchoolEntity = schoolFactory.buildWithId();
const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school });

const group: GroupEntity = groupEntityFactory.buildWithId({
users: [
{
user: teacherUser,
role: teacherUser.roles[0],
},
],
organization: school,
});

await em.persistAndFlush([teacherAccount, teacherUser, group]);
em.clear();

const loggedInClient = await testApiClient.login(teacherAccount);

return {
loggedInClient,
group,
teacherUser,
};
};

it('should return the group', async () => {
const { loggedInClient, group, teacherUser } = await setup();

const response = await loggedInClient.get(`${group.id}`);

expect(response.status).toEqual(HttpStatus.OK);
expect(response.body).toEqual({
id: group.id,
name: group.name,
type: group.type,
users: [
{
id: teacherUser.id,
firstName: teacherUser.firstName,
lastName: teacherUser.lastName,
role: teacherUser.roles[0].name,
},
],
externalSource: {
externalId: group.externalSource?.externalId,
systemId: group.externalSource?.system.id,
},
});
});
});

describe('when group does not exist', () => {
const setup = async () => {
const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher();

await em.persistAndFlush([teacherAccount, teacherUser]);
em.clear();

const loggedInClient = await testApiClient.login(teacherAccount);

return {
loggedInClient,
};
};

it('should return not found', async () => {
const { loggedInClient } = await setup();

const response = await loggedInClient.get(`${new ObjectId().toHexString()}`);

expect(response.status).toEqual(HttpStatus.NOT_FOUND);
expect(response.body).toEqual({
code: HttpStatus.NOT_FOUND,
message: 'Not Found',
title: 'Not Found',
type: 'NOT_FOUND',
});
});
});
});

describe('when unauthorized user requests a group', () => {
const setup = async () => {
const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent();

const group: GroupEntity = groupEntityFactory.buildWithId();

await em.persistAndFlush([studentAccount, studentUser, group]);
em.clear();

return {
groupId: group.id,
};
};

it('should return unauthorized', async () => {
const { groupId } = await setup();

const response = await testApiClient.get(`${groupId}`);

expect(response.status).toEqual(HttpStatus.UNAUTHORIZED);
expect(response.body).toEqual({
code: HttpStatus.UNAUTHORIZED,
message: 'Unauthorized',
title: 'Unauthorized',
type: 'UNAUTHORIZED',
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsMongoId } from 'class-validator';

export class GroupIdParams {
@IsMongoId()
@ApiProperty({ nullable: false, required: true })
groupId!: string;
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './class-sort-params';
export * from './group-id-params';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';

export class ExternalSourceResponse {
@ApiProperty()
externalId: string;

@ApiProperty()
systemId: string;

constructor(props: ExternalSourceResponse) {
this.externalId = props.externalId;
this.systemId = props.systemId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum GroupTypeResponse {
CLASS = 'class',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';
import { RoleName } from '@shared/domain';

export class GroupUserResponse {
@ApiProperty()
id: string;

@ApiProperty()
firstName: string;

@ApiProperty()
lastName: string;

@ApiProperty()
role: RoleName;

constructor(user: GroupUserResponse) {
this.id = user.id;
this.firstName = user.firstName;
this.lastName = user.lastName;
this.role = user.role;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ExternalSourceResponse } from './external-source.response';
import { GroupTypeResponse } from './group-type.response';
import { GroupUserResponse } from './group-user.response';

export class GroupResponse {
@ApiProperty()
id: string;

@ApiProperty()
name: string;

@ApiProperty({ enum: GroupTypeResponse })
type: GroupTypeResponse;

@ApiProperty({ type: [GroupUserResponse] })
users: GroupUserResponse[];

@ApiPropertyOptional()
externalSource?: ExternalSourceResponse;

@ApiPropertyOptional()
organizationId?: string;

constructor(group: GroupResponse) {
this.id = group.id;
this.name = group.name;
this.type = group.type;
this.users = group.users;
this.externalSource = group.externalSource;
this.organizationId = group.organizationId;
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export * from './class-info.response';
export * from './class-info-search-list.response';
export * from './external-source.response';
export * from './group.response';
export * from './group-type.response';
export * from './group-user.response';
22 changes: 19 additions & 3 deletions apps/server/src/modules/group/controller/group.controller.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Controller, Get, HttpStatus, Query } from '@nestjs/common';
import { Controller, Get, HttpStatus, Param, Query } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { PaginationParams } from '@shared/controller';
import { Page } from '@shared/domain';
import { ErrorResponse } from '@src/core/error/dto';
import { ICurrentUser } from '@src/modules/authentication';
import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator';
import { GroupUc } from '../uc';
import { ClassInfoDto } from '../uc/dto';
import { ClassInfoSearchListResponse, ClassSortParams } from './dto';
import { ClassInfoDto, ResolvedGroupDto } from '../uc/dto';
import { ClassInfoSearchListResponse, ClassSortParams, GroupIdParams, GroupResponse } from './dto';
import { GroupResponseMapper } from './mapper';

@ApiTags('Group')
Expand Down Expand Up @@ -43,4 +43,20 @@ export class GroupController {

return response;
}

@Get('/:groupId')
@ApiOperation({ summary: 'Get a group by id.' })
@ApiResponse({ status: HttpStatus.OK, type: GroupResponse })
@ApiResponse({ status: '4XX', type: ErrorResponse })
@ApiResponse({ status: '5XX', type: ErrorResponse })
public async getGroup(
@CurrentUser() currentUser: ICurrentUser,
@Param() params: GroupIdParams
): Promise<GroupResponse> {
const group: ResolvedGroupDto = await this.groupUc.getGroup(currentUser.userId, params.groupId);

const response: GroupResponse = GroupResponseMapper.mapToGroupResponse(group);

return response;
}
}
Loading