Skip to content

Commit

Permalink
N21-1206 show classes and goups (#4406)
Browse files Browse the repository at this point in the history
* implement endpoint for class list
* add feature
  • Loading branch information
MarvinOehlerkingCap authored Sep 27, 2023
1 parent 7e6a281 commit 5b4fa78
Show file tree
Hide file tree
Showing 36 changed files with 1,143 additions and 60 deletions.
44 changes: 39 additions & 5 deletions apps/server/src/modules/class/repo/classes.repo.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { MongoMemoryDatabaseModule } from '@shared/infra/database';
import { EntityManager, ObjectId } from '@mikro-orm/mongodb';
import { TestingModule } from '@nestjs/testing/testing-module';
import { Test } from '@nestjs/testing';
import { cleanupCollections } from '@shared/testing';
import { TestingModule } from '@nestjs/testing/testing-module';
import { SchoolEntity } from '@shared/domain';
import { MongoMemoryDatabaseModule } from '@shared/infra/database';
import { cleanupCollections, schoolFactory } from '@shared/testing';
import { classEntityFactory } from '@src/modules/class/entity/testing/factory/class.entity.factory';
import { ClassesRepo } from './classes.repo';
import { Class } from '../domain';
import { ClassEntity } from '../entity';
import { ClassesRepo } from './classes.repo';
import { ClassMapper } from './mapper';
import { Class } from '../domain';

describe(ClassesRepo.name, () => {
let module: TestingModule;
Expand All @@ -32,6 +33,38 @@ describe(ClassesRepo.name, () => {
await cleanupCollections(em);
});

describe('findAllBySchoolId', () => {
describe('when school has no class', () => {
it('should return empty array', async () => {
const result = await repo.findAllBySchoolId(new ObjectId().toHexString());

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

describe('when school has classes', () => {
const setup = async () => {
const school: SchoolEntity = schoolFactory.buildWithId();
const classes: ClassEntity[] = classEntityFactory.buildListWithId(3, { schoolId: school.id });

await em.persistAndFlush(classes);

return {
school,
classes,
};
};

it('should find classes with particular userId', async () => {
const { school } = await setup();

const result: Class[] = await repo.findAllBySchoolId(school.id);

expect(result.length).toEqual(3);
});
});
});

describe('findAllByUserId', () => {
describe('when user is not found in classes', () => {
it('should return empty array', async () => {
Expand All @@ -40,6 +73,7 @@ describe(ClassesRepo.name, () => {
expect(result).toEqual([]);
});
});

describe('when user is in classes', () => {
const setup = async () => {
const testUser = new ObjectId();
Expand Down
20 changes: 15 additions & 5 deletions apps/server/src/modules/class/repo/classes.repo.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { Injectable } from '@nestjs/common';

import { EntityManager, ObjectId } from '@mikro-orm/mongodb';
import { Injectable } from '@nestjs/common';
import { EntityId } from '@shared/domain';
import { ClassEntity } from '../entity';
import { Class } from '../domain';
import { ClassEntity } from '../entity';
import { ClassMapper } from './mapper';

@Injectable()
export class ClassesRepo {
constructor(private readonly em: EntityManager, private readonly mapper: ClassMapper) {}
constructor(private readonly em: EntityManager) {}

async findAllBySchoolId(schoolId: EntityId): Promise<Class[]> {
const classes: ClassEntity[] = await this.em.find(ClassEntity, { schoolId: new ObjectId(schoolId) });

const mapped: Class[] = ClassMapper.mapToDOs(classes);

return mapped;
}

async findAllByUserId(userId: EntityId): Promise<Class[]> {
const classes: ClassEntity[] = await this.em.find(ClassEntity, { userIds: new ObjectId(userId) });
return ClassMapper.mapToDOs(classes);

const mapped: Class[] = ClassMapper.mapToDOs(classes);

return mapped;
}

async updateMany(classes: Class[]): Promise<void> {
Expand Down
47 changes: 23 additions & 24 deletions apps/server/src/modules/class/service/class.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { ObjectId } from '@mikro-orm/mongodb';
import { InternalServerErrorException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { DeepMocked, createMock } from '@golevelup/ts-jest';
import { EntityId } from '@shared/domain';
import { InternalServerErrorException } from '@nestjs/common';
import { classEntityFactory } from '@src/modules/class/entity/testing/factory/class.entity.factory';
import { ObjectId } from '@mikro-orm/mongodb';
import { setupEntities } from '@shared/testing';
import { ClassService } from './class.service';
import { classEntityFactory } from '@src/modules/class/entity/testing/factory/class.entity.factory';
import { Class } from '../domain';
import { classFactory } from '../domain/testing/factory/class.factory';
import { ClassesRepo } from '../repo';
import { ClassMapper } from '../repo/mapper';
import { ClassService } from './class.service';

describe(ClassService.name, () => {
let module: TestingModule;
Expand Down Expand Up @@ -39,38 +41,35 @@ describe(ClassService.name, () => {
await module.close();
});

describe('findUserDataFromClasses', () => {
describe('when finding by userId', () => {
describe('findClassesForSchool', () => {
describe('when the school has classes', () => {
const setup = () => {
const userId1 = new ObjectId();
const userId2 = new ObjectId();
const userId3 = new ObjectId();
const class1 = classEntityFactory.withUserIds([userId1, userId2]).build();
const class2 = classEntityFactory.withUserIds([userId1, userId3]).build();
classEntityFactory.withUserIds([userId2, userId3]).build();
const schoolId: string = new ObjectId().toHexString();

const mappedClasses = ClassMapper.mapToDOs([class1, class2]);
const classes: Class[] = classFactory.buildList(3);

classesRepo.findAllByUserId.mockResolvedValue(mappedClasses);
classesRepo.findAllBySchoolId.mockResolvedValueOnce(classes);

return {
userId1,
schoolId,
classes,
};
};

it('should call classesRepo.findAllByUserId', async () => {
const { userId1 } = setup();
await service.deleteUserDataFromClasses(userId1.toHexString());
it('should call the repo', async () => {
const { schoolId } = setup();

expect(classesRepo.findAllByUserId).toBeCalledWith(userId1.toHexString());
await service.findClassesForSchool(schoolId);

expect(classesRepo.findAllBySchoolId).toHaveBeenCalledWith(schoolId);
});

it('should return array of two teams with user', async () => {
const { userId1 } = setup();
it('should return the classes', async () => {
const { schoolId, classes } = setup();

const result = await service.findUserDataFromClasses(userId1.toHexString());
const result: Class[] = await service.findClassesForSchool(schoolId);

expect(result.length).toEqual(2);
expect(result).toEqual(classes);
});
});
});
Expand Down
6 changes: 3 additions & 3 deletions apps/server/src/modules/class/service/class.service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { EntityId } from '@shared/domain';
import { ClassesRepo } from '../repo';
import { Class } from '../domain';
import { ClassesRepo } from '../repo';

@Injectable()
export class ClassService {
constructor(private readonly classesRepo: ClassesRepo) {}

public async findUserDataFromClasses(userId: EntityId): Promise<Class[]> {
const classes = await this.classesRepo.findAllByUserId(userId);
public async findClassesForSchool(schoolId: EntityId): Promise<Class[]> {
const classes: Class[] = await this.classesRepo.findAllBySchoolId(schoolId);

return classes;
}
Expand Down
140 changes: 140 additions & 0 deletions apps/server/src/modules/group/controller/api-test/group.api.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { EntityManager } from '@mikro-orm/mongodb';
import { HttpStatus, INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { Role, RoleName, SchoolEntity, SortOrder, SystemEntity, User } from '@shared/domain';
import {
groupEntityFactory,
roleFactory,
schoolFactory,
systemFactory,
TestApiClient,
UserAndAccountTestFactory,
userFactory,
} from '@shared/testing';
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 { GroupEntity, GroupEntityTypes } from '../../entity';
import { ClassInfoSearchListResponse, ClassSortBy } from '../dto';

const baseRouteName = '/groups';

describe('Group (API)', () => {
let app: INestApplication;
let em: EntityManager;
let testApiClient: TestApiClient;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [ServerTestModule],
}).compile();

app = module.createNestApplication();
await app.init();
em = module.get(EntityManager);
testApiClient = new TestApiClient(app, baseRouteName);
});

afterAll(async () => {
await app.close();
});

describe('findClassesForSchool', () => {
describe('when an admin requests a list of classes', () => {
const setup = async () => {
const school: SchoolEntity = schoolFactory.buildWithId();
const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school });

const teacherRole: Role = roleFactory.buildWithId({ name: RoleName.TEACHER });
const teacherUser: User = userFactory.buildWithId({ school, roles: [teacherRole] });
const system: SystemEntity = systemFactory.buildWithId();
const clazz: ClassEntity = classEntityFactory.buildWithId({
name: 'Group A',
schoolId: school._id,
teacherIds: [teacherUser._id],
source: undefined,
});
const group: GroupEntity = groupEntityFactory.buildWithId({
name: 'Group B',
type: GroupEntityTypes.CLASS,
externalSource: {
externalId: 'externalId',
system,
},
organization: school,
users: [
{
user: adminUser,
role: teacherRole,
},
],
});

await em.persistAndFlush([school, adminAccount, adminUser, teacherRole, teacherUser, system, clazz, group]);
em.clear();

const adminClient = await testApiClient.login(adminAccount);

return {
adminClient,
group,
clazz,
system,
adminUser,
teacherUser,
};
};

it('should return the classes of his school', async () => {
const { adminClient, group, clazz, system, adminUser, teacherUser } = await setup();

const response = await adminClient.get(`/class`).query({
skip: 0,
limit: 2,
sortBy: ClassSortBy.NAME,
sortOrder: SortOrder.desc,
});

expect(response.body).toEqual<ClassInfoSearchListResponse>({
total: 2,
data: [
{
name: group.name,
externalSourceName: system.displayName,
teachers: [adminUser.lastName],
},
{
name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name,
teachers: [teacherUser.lastName],
},
],
skip: 0,
limit: 2,
});
});
});

describe('when an invalid user requests a list of classes', () => {
const setup = async () => {
const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent();

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

const studentClient = await testApiClient.login(studentAccount);

return {
studentClient,
};
};

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

const response = await studentClient.get(`/class`);

expect(response.status).toEqual(HttpStatus.FORBIDDEN);
});
});
});
});
2 changes: 2 additions & 0 deletions apps/server/src/modules/group/controller/dto/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './request';
export * from './response';
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { SortingParams } from '@shared/controller';
import { IsEnum, IsOptional } from 'class-validator';

export enum ClassSortBy {
NAME = 'name',
EXTERNAL_SOURCE_NAME = 'externalSourceName',
}

export class ClassSortParams extends SortingParams<ClassSortBy> {
@IsOptional()
@IsEnum(ClassSortBy)
@ApiPropertyOptional({ enum: ClassSortBy })
sortBy?: ClassSortBy;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './class-sort-params';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { PaginationResponse } from '@shared/controller';
import { ClassInfoResponse } from './class-info.response';

export class ClassInfoSearchListResponse extends PaginationResponse<ClassInfoResponse[]> {
constructor(data: ClassInfoResponse[], total: number, skip?: number, limit?: number) {
super(total, skip, limit);
this.data = data;
}

@ApiProperty({ type: [ClassInfoResponse] })
data: ClassInfoResponse[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class ClassInfoResponse {
@ApiProperty()
name: string;

@ApiPropertyOptional()
externalSourceName?: string;

@ApiProperty({ type: [String] })
teachers: string[];

constructor(props: ClassInfoResponse) {
this.name = props.name;
this.externalSourceName = props.externalSourceName;
this.teachers = props.teachers;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './class-info.response';
export * from './class-info-search-list.response';
Loading

0 comments on commit 5b4fa78

Please sign in to comment.