-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
find teachers of school * enable user repo to search by role * user service function to find users by role and school * endpoint in school api to find teachers of school find public teachers of school * add discoverability as field in nest (already exists in DB/feathers) * repo functionality to search by discoverability * add service function to get public teachers, based on configuration * uc for finding teachers of school gets public teachers instead when user is not part of the school various * in order to make api tests with roles work, we had to introduce caching lables for the role cache, used to invalidate the cache in tests. --------- Co-authored-by: hoeppner.dataport <[email protected]>
- Loading branch information
1 parent
23f1326
commit 30c64c9
Showing
23 changed files
with
718 additions
and
10 deletions.
There are no files selected for viewing
29 changes: 29 additions & 0 deletions
29
apps/server/src/modules/school/api/dto/response/school-user.response.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { ApiProperty } from '@nestjs/swagger'; | ||
import { PaginationResponse } from '@shared/controller'; | ||
|
||
export class SchoolUserResponse { | ||
@ApiProperty() | ||
firstName!: string; | ||
|
||
@ApiProperty() | ||
lastName!: string; | ||
|
||
@ApiProperty() | ||
id!: string; | ||
|
||
constructor(props: SchoolUserResponse) { | ||
this.id = props.id; | ||
this.firstName = props.firstName; | ||
this.lastName = props.lastName; | ||
} | ||
} | ||
|
||
export class SchoolUserListResponse extends PaginationResponse<SchoolUserResponse[]> { | ||
constructor(data: SchoolUserResponse[], total: number, skip?: number, limit?: number) { | ||
super(total, skip, limit); | ||
this.data = data; | ||
} | ||
|
||
@ApiProperty({ type: [SchoolUserResponse] }) | ||
data: SchoolUserResponse[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './school-systems.response.mapper'; | ||
export * from './school.response.mapper'; | ||
export * from './school-user.response.mapper'; |
22 changes: 22 additions & 0 deletions
22
apps/server/src/modules/school/api/mapper/school-user.response.mapper.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { PaginationParams } from '@shared/controller'; | ||
import { Page, UserDO } from '@shared/domain/domainobject'; | ||
import { SchoolUserListResponse, SchoolUserResponse } from '../dto/response/school-user.response'; | ||
|
||
export class SchoolUserResponseMapper { | ||
public static mapToResponse(user: UserDO): SchoolUserResponse { | ||
const res = new SchoolUserResponse({ | ||
id: user.id || '', | ||
firstName: user.firstName, | ||
lastName: user.lastName, | ||
}); | ||
|
||
return res; | ||
} | ||
|
||
static mapToListResponse(users: Page<UserDO>, pagination?: PaginationParams): SchoolUserListResponse { | ||
const data: SchoolUserResponse[] = users.data.map((user) => this.mapToResponse(user)); | ||
const response = new SchoolUserListResponse(data, users.total, pagination?.skip, pagination?.limit); | ||
|
||
return response; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
217 changes: 217 additions & 0 deletions
217
apps/server/src/modules/school/api/test/school-users.api.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; | ||
import { HttpStatus, INestApplication } from '@nestjs/common'; | ||
import { Test } from '@nestjs/testing'; | ||
import { | ||
cleanupCollections, | ||
schoolEntityFactory, | ||
TestApiClient, | ||
UserAndAccountTestFactory, | ||
userFactory, | ||
} from '@shared/testing'; | ||
import { ServerTestModule } from '@src/modules/server'; | ||
import { SchoolUserListResponse } from '../dto/response/school-user.response'; | ||
|
||
describe('School Controller (API)', () => { | ||
let app: INestApplication; | ||
let em: EntityManager; | ||
let testApiClient: TestApiClient; | ||
|
||
beforeAll(async () => { | ||
const moduleFixture = await Test.createTestingModule({ | ||
imports: [ServerTestModule], | ||
}).compile(); | ||
|
||
app = moduleFixture.createNestApplication(); | ||
await app.init(); | ||
em = app.get(EntityManager); | ||
testApiClient = new TestApiClient(app, 'school'); | ||
}); | ||
|
||
beforeEach(async () => { | ||
await cleanupCollections(em); | ||
await em.clearCache('roles-cache-teacher'); | ||
}); | ||
|
||
afterAll(async () => { | ||
await app.close(); | ||
}); | ||
|
||
describe('get Teachers', () => { | ||
describe('when no user is logged in', () => { | ||
it('should return 401', async () => { | ||
const someId = new ObjectId().toHexString(); | ||
|
||
const response = await testApiClient.get(`${someId}/teachers`); | ||
|
||
expect(response.status).toEqual(HttpStatus.UNAUTHORIZED); | ||
}); | ||
}); | ||
|
||
describe('when schoolId is invalid format', () => { | ||
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 400', async () => { | ||
const { loggedInClient } = await setup(); | ||
|
||
const response = await loggedInClient.get(`/123/teachers`); | ||
|
||
expect(response.status).toEqual(HttpStatus.BAD_REQUEST); | ||
expect(response.body).toEqual( | ||
expect.objectContaining({ | ||
validationErrors: [{ errors: ['schoolId must be a mongodb id'], field: ['schoolId'] }], | ||
}) | ||
); | ||
}); | ||
}); | ||
|
||
describe('when schoolId doesnt 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 404', async () => { | ||
const { loggedInClient } = await setup(); | ||
const someId = new ObjectId().toHexString(); | ||
|
||
const response = await loggedInClient.get(`/${someId}/teachers`); | ||
|
||
expect(response.status).toEqual(HttpStatus.NOT_FOUND); | ||
}); | ||
}); | ||
|
||
describe('when user is not in the correct school', () => { | ||
const setup = async () => { | ||
const school = schoolEntityFactory.build(); | ||
const otherSchool = schoolEntityFactory.build(); | ||
const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school: otherSchool }); | ||
const teacherRole = teacherUser.roles[0]; | ||
const teachersOfSchool = userFactory.buildList(3, { school, roles: [teacherRole] }); | ||
const publicTeachersOfSchool = userFactory.buildList(2, { school, roles: [teacherRole], discoverable: true }); | ||
|
||
await em.persistAndFlush([teacherAccount, teacherUser, ...teachersOfSchool, ...publicTeachersOfSchool]); | ||
em.clear(); | ||
|
||
const loggedInClient = await testApiClient.login(teacherAccount); | ||
|
||
return { loggedInClient, teacherUser, teachersOfSchool, school, publicTeachersOfSchool }; | ||
}; | ||
|
||
it('should return only public teachers', async () => { | ||
const { loggedInClient, school, publicTeachersOfSchool } = await setup(); | ||
|
||
const response = await loggedInClient.get(`${school.id}/teachers`); | ||
const body = response.body as SchoolUserListResponse; | ||
|
||
expect(response.status).toEqual(HttpStatus.OK); | ||
expect(body.total).toEqual(publicTeachersOfSchool.length); | ||
expect(body.data).toEqual( | ||
expect.arrayContaining([ | ||
...publicTeachersOfSchool.map((teacher) => { | ||
return { | ||
id: teacher.id, | ||
firstName: teacher.firstName, | ||
lastName: teacher.lastName, | ||
}; | ||
}), | ||
]) | ||
); | ||
}); | ||
}); | ||
|
||
describe('when user has no permission to view teachers', () => { | ||
const setup = async () => { | ||
const school = schoolEntityFactory.build(); | ||
const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent({ school }); | ||
const teacherRole = studentUser.roles[0]; | ||
const teachersOfSchool = userFactory.buildList(3, { school, roles: [teacherRole] }); | ||
|
||
await em.persistAndFlush([studentAccount, studentUser, ...teachersOfSchool]); | ||
em.clear(); | ||
|
||
const loggedInClient = await testApiClient.login(studentAccount); | ||
|
||
return { loggedInClient, studentUser, teachersOfSchool, school }; | ||
}; | ||
|
||
it('should return 403', async () => { | ||
const { loggedInClient, school } = await setup(); | ||
|
||
const response = await loggedInClient.get(`${school.id}/teachers`); | ||
|
||
expect(response.status).toEqual(HttpStatus.UNAUTHORIZED); | ||
}); | ||
}); | ||
|
||
describe('when user has permission to view teachers', () => { | ||
const setup = async () => { | ||
const school = schoolEntityFactory.build(); | ||
const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }); | ||
const teacherRole = teacherUser.roles[0]; | ||
const teachersOfSchool = userFactory.buildList(3, { school, roles: [teacherRole] }); | ||
|
||
const otherSchool = schoolEntityFactory.build(); | ||
const teachersOfOtherSchool = userFactory.buildList(3, { school: otherSchool, roles: [teacherRole] }); | ||
|
||
await em.persistAndFlush([teacherAccount, teacherUser, ...teachersOfSchool, ...teachersOfOtherSchool]); | ||
em.clear(); | ||
|
||
const loggedInClient = await testApiClient.login(teacherAccount); | ||
|
||
return { loggedInClient, teacherUser, teachersOfSchool, school }; | ||
}; | ||
|
||
it('should return 200 with teachers', async () => { | ||
const { loggedInClient, teacherUser, teachersOfSchool, school } = await setup(); | ||
|
||
const response = await loggedInClient.get(`${school.id}/teachers`); | ||
|
||
const body = response.body as SchoolUserListResponse; | ||
|
||
expect(response.status).toEqual(HttpStatus.OK); | ||
expect(body.data).toEqual( | ||
expect.arrayContaining([ | ||
expect.objectContaining({ | ||
id: teacherUser.id, | ||
firstName: teacherUser.firstName, | ||
lastName: teacherUser.lastName, | ||
}), | ||
...teachersOfSchool.map((teacher) => { | ||
return { | ||
id: teacher.id, | ||
firstName: teacher.firstName, | ||
lastName: teacher.lastName, | ||
}; | ||
}), | ||
]) | ||
); | ||
}); | ||
|
||
it('should paginate', async () => { | ||
const { loggedInClient, school } = await setup(); | ||
|
||
const response = await loggedInClient.get(`${school.id}/teachers`).query({ skip: 1, limit: 1 }); | ||
const body = response.body as SchoolUserListResponse; | ||
|
||
expect(body.data).toHaveLength(1); | ||
expect(body.total).toEqual(4); | ||
expect(body.skip).toEqual(1); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.