Skip to content

Commit 670a832

Browse files
committed
tests(api): more tests for users controller
1 parent 9c3ea4d commit 670a832

File tree

8 files changed

+256
-51
lines changed

8 files changed

+256
-51
lines changed

modules/libs/protocol/routes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const Routes = (baseUrl: string = '') => ({
1919
update: (id: string) => `${baseUrl}/edu/roles/${id}`,
2020
delete: (id: string) => `${baseUrl}/edu/roles/${id}`,
2121
},
22-
user: (userId: string) => ({
22+
user: (userId?: string) => ({
2323
get: () => `${baseUrl}/edu/users/${userId}`,
2424
find: () => `${baseUrl}/edu/users`,
2525
update: () => `${baseUrl}/edu/users/${userId}`,

modules/services/api/src/edu/controllers/users/specs/context.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { faker } from '@faker-js/faker';
22
import { INestApplication } from '@nestjs/common';
3+
import { AuthService } from '@vidya/api/auth/services';
34
import {
45
RolesService,
56
SchoolsService,
@@ -16,6 +17,9 @@ export type Context = {
1617
users: {
1718
oneAdmin: User;
1819
};
20+
tokens: {
21+
oneAdmin: string;
22+
};
1923
};
2024
two: {
2125
school: School;
@@ -26,6 +30,12 @@ export type Context = {
2630
twoAdmin: User;
2731
};
2832
};
33+
empty: {
34+
tokens: {
35+
dummy: string;
36+
empty: string;
37+
};
38+
};
2939
};
3040

3141
export const createContext = async (
@@ -34,6 +44,7 @@ export const createContext = async (
3444
const rolesService = app.get(RolesService);
3545
const schoolsService = app.get(SchoolsService);
3646
const usersService = app.get(UsersService);
47+
const authService = app.get(AuthService);
3748

3849
/* -------------------------------------------------------------------------- */
3950
/* Organizations */
@@ -81,6 +92,20 @@ export const createContext = async (
8192
roles: [twoAdmin],
8293
});
8394

95+
/* -------------------------------------------------------------------------- */
96+
/* Tokens */
97+
/* -------------------------------------------------------------------------- */
98+
99+
const oneAdminToken = await authService.generateTokens(orgAdminUser.id, [
100+
{ sid: one.id, p: orgAdmin.permissions },
101+
]);
102+
103+
const dummyToken = await authService.generateTokens(faker.string.uuid(), [
104+
{ sid: faker.string.uuid(), p: ['users:read'] },
105+
]);
106+
107+
const emptyToken = await authService.generateTokens(faker.string.uuid(), []);
108+
84109
/* -------------------------------------------------------------------------- */
85110
/* Return */
86111
/* -------------------------------------------------------------------------- */
@@ -94,6 +119,9 @@ export const createContext = async (
94119
users: {
95120
oneAdmin: orgAdminUser,
96121
},
122+
tokens: {
123+
oneAdmin: oneAdminToken.accessToken,
124+
},
97125
},
98126
two: {
99127
school: two,
@@ -104,5 +132,11 @@ export const createContext = async (
104132
twoAdmin: twoAdminUser,
105133
},
106134
},
135+
empty: {
136+
tokens: {
137+
dummy: dummyToken.accessToken,
138+
empty: emptyToken.accessToken,
139+
},
140+
},
107141
};
108142
};
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { Mapper } from '@automapper/core';
2+
import { DEFAULT_MAPPER_TOKEN } from '@automapper/nestjs';
3+
import { faker } from '@faker-js/faker';
4+
import { INestApplication } from '@nestjs/common';
5+
import * as dto from '@vidya/api/edu/dto';
6+
import { createTestingApp } from '@vidya/api/edu/shared';
7+
import * as entities from '@vidya/entities';
8+
import { Routes } from '@vidya/protocol';
9+
import { instanceToPlain } from 'class-transformer';
10+
import * as request from 'supertest';
11+
12+
import { Context, createContext } from './context';
13+
14+
describe('/edu/users', () => {
15+
let app: INestApplication;
16+
let ctx: Context;
17+
let mapper: Mapper;
18+
19+
beforeEach(async () => {
20+
app = await createTestingApp();
21+
ctx = await createContext(app);
22+
mapper = app.get(DEFAULT_MAPPER_TOKEN);
23+
});
24+
25+
afterAll(async () => {
26+
await app.close();
27+
});
28+
29+
/* -------------------------------------------------------------------------- */
30+
/* Authentication Validation */
31+
/* -------------------------------------------------------------------------- */
32+
33+
it(`GET /edu/users returns 401 for unauthenticated user`, () => {
34+
return request(app.getHttpServer())
35+
.get(Routes().edu.user().find())
36+
.expect(401)
37+
.expect({
38+
message: 'Unauthorized',
39+
statusCode: 401,
40+
});
41+
});
42+
43+
it(`GET /edu/users returns 403 for unauthorized user`, async () => {
44+
return request(app.getHttpServer())
45+
.get(Routes().edu.user().find())
46+
.set('Authorization', `Bearer ${ctx.empty.tokens.empty}`)
47+
.expect(403)
48+
.expect({
49+
message: 'User does not have permission',
50+
error: 'Forbidden',
51+
statusCode: 403,
52+
});
53+
});
54+
55+
/* -------------------------------------------------------------------------- */
56+
/* Positive Cases */
57+
/* -------------------------------------------------------------------------- */
58+
59+
it(`GET /edu/users/:id returns the user by Id`, async () => {
60+
return request(app.getHttpServer())
61+
.get(Routes().edu.user(ctx.one.users.oneAdmin.id).get())
62+
.set('Authorization', `Bearer ${ctx.one.tokens.oneAdmin}`)
63+
.expect(200)
64+
.expect({
65+
...instanceToPlain(
66+
mapper.map(
67+
ctx.one.users.oneAdmin,
68+
entities.User,
69+
dto.GetUserResponse,
70+
),
71+
),
72+
});
73+
});
74+
75+
it(`GET /edu/users returns permitted users`, async () => {
76+
return request(app.getHttpServer())
77+
.get(Routes().edu.user().find())
78+
.set('Authorization', `Bearer ${ctx.one.tokens.oneAdmin}`)
79+
.expect(200)
80+
.expect({
81+
items: instanceToPlain(
82+
mapper.mapArray(
83+
[ctx.one.users.oneAdmin],
84+
entities.User,
85+
dto.UserSummary,
86+
),
87+
),
88+
});
89+
});
90+
91+
it(`GET /edu/users filtered by schoolId`, async () => {
92+
return request(app.getHttpServer())
93+
.get(Routes().edu.user().find())
94+
.query({ schoolId: ctx.two.school.id })
95+
.set('Authorization', `Bearer ${ctx.one.tokens.oneAdmin}`)
96+
.expect(200)
97+
.expect({ items: [] });
98+
});
99+
100+
/* -------------------------------------------------------------------------- */
101+
/* Negative Cases */
102+
/* -------------------------------------------------------------------------- */
103+
104+
it(`GET /edu/users/:id returns 404 if user is not found`, async () => {
105+
return request(app.getHttpServer())
106+
.get(Routes().edu.user(faker.string.uuid()).get())
107+
.set('Authorization', `Bearer ${ctx.one.tokens.oneAdmin}`)
108+
.expect(404);
109+
});
110+
111+
it(`GET /edu/users/:id returns 404 user has no access to it`, async () => {
112+
return request(app.getHttpServer())
113+
.get(Routes().edu.user(ctx.two.users.twoAdmin.id).get())
114+
.set('Authorization', `Bearer ${ctx.one.tokens.oneAdmin}`)
115+
.expect(404);
116+
});
117+
118+
it(`GET /edu/users returns nothing if user do not have permissions`, async () => {
119+
return request(app.getHttpServer())
120+
.get(Routes().edu.user().find())
121+
.set('Authorization', `Bearer ${ctx.empty.tokens.dummy}`)
122+
.expect(200)
123+
.expect({ items: [] });
124+
});
125+
});

modules/services/api/src/edu/controllers/users/specs/users.controller.test.get.spec.ts

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { INestApplication } from '@nestjs/common';
44
import { UserPermissions } from '@vidya/api/auth/utils';
55
import { UsersController } from '@vidya/api/edu/controllers';
66
import * as dto from '@vidya/api/edu/dto';
7-
import { UsersService } from '@vidya/api/edu/services';
87
import { createTestingApp } from '@vidya/api/edu/shared';
98
import { Role } from '@vidya/entities';
109
import * as entities from '@vidya/entities';
@@ -43,6 +42,10 @@ describe('UsersController', () => {
4342
);
4443
}
4544

45+
/* -------------------------------------------------------------------------- */
46+
/* Get One */
47+
/* -------------------------------------------------------------------------- */
48+
4649
describe('getOne', () => {
4750
it('returns user by Id', async () => {
4851
const res = await ctr.getOne(
@@ -99,38 +102,4 @@ describe('UsersController', () => {
99102
expectUsers(res, [ctx.one.users.oneAdmin]);
100103
});
101104
});
102-
103-
/* -------------------------------------------------------------------------- */
104-
/* Update One */
105-
/* -------------------------------------------------------------------------- */
106-
107-
describe('updateOne', () => {
108-
it('updates user by Id', async () => {
109-
const res = await ctr.updateOne(
110-
new dto.UpdateUserRequest({ name: 'Updated Name' }),
111-
ctx.one.users.oneAdmin.id,
112-
getPermissions([ctx.one.roles.oneAdmin]),
113-
);
114-
115-
expect(res).toEqual(
116-
mapper.map(
117-
await app
118-
.get(UsersService)
119-
.findOneBy({ id: ctx.one.users.oneAdmin.id }),
120-
entities.User,
121-
dto.UpdateUserResponse,
122-
),
123-
);
124-
});
125-
126-
it('throws if user do not have permission', async () => {
127-
await expect(async () => {
128-
await ctr.updateOne(
129-
new dto.UpdateUserRequest({ name: 'Updated Name' }),
130-
ctx.one.users.oneAdmin.id,
131-
getPermissions([ctx.two.roles.twoAdmin]),
132-
);
133-
}).rejects.toThrow(`User does not have permission`);
134-
});
135-
});
136105
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Mapper } from '@automapper/core';
2+
import { DEFAULT_MAPPER_TOKEN } from '@automapper/nestjs';
3+
import { INestApplication } from '@nestjs/common';
4+
import { UserPermissions } from '@vidya/api/auth/utils';
5+
import { UsersController } from '@vidya/api/edu/controllers';
6+
import * as dto from '@vidya/api/edu/dto';
7+
import { UsersService } from '@vidya/api/edu/services';
8+
import { createTestingApp } from '@vidya/api/edu/shared';
9+
import { Role } from '@vidya/entities';
10+
import * as entities from '@vidya/entities';
11+
12+
import { Context, createContext } from './context';
13+
14+
describe('UsersController', () => {
15+
let app: INestApplication;
16+
let ctx: Context;
17+
let ctr: UsersController;
18+
let mapper: Mapper;
19+
20+
beforeEach(async () => {
21+
app = await createTestingApp();
22+
ctx = await createContext(app);
23+
ctr = app.get(UsersController);
24+
mapper = app.get(DEFAULT_MAPPER_TOKEN);
25+
});
26+
27+
function getPermissions(roles: Role[]): UserPermissions {
28+
return new UserPermissions(
29+
roles.map((r) => ({
30+
sid: r.schoolId,
31+
p: r.permissions,
32+
})),
33+
);
34+
}
35+
36+
/* -------------------------------------------------------------------------- */
37+
/* Update One */
38+
/* -------------------------------------------------------------------------- */
39+
40+
describe('updateOne', () => {
41+
it('updates user by Id', async () => {
42+
const res = await ctr.updateOne(
43+
new dto.UpdateUserRequest({ name: 'Updated Name' }),
44+
ctx.one.users.oneAdmin.id,
45+
getPermissions([ctx.one.roles.oneAdmin]),
46+
);
47+
48+
expect(res).toEqual(
49+
mapper.map(
50+
await app
51+
.get(UsersService)
52+
.findOneBy({ id: ctx.one.users.oneAdmin.id }),
53+
entities.User,
54+
dto.UpdateUserResponse,
55+
),
56+
);
57+
});
58+
59+
it('throws if user do not have permission', async () => {
60+
await expect(async () => {
61+
await ctr.updateOne(
62+
new dto.UpdateUserRequest({ name: 'Updated Name' }),
63+
ctx.one.users.oneAdmin.id,
64+
getPermissions([ctx.two.roles.twoAdmin]),
65+
);
66+
}).rejects.toThrow(`User does not have permission`);
67+
});
68+
});
69+
});

modules/services/api/src/edu/controllers/users/users.controller.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
NotFoundException,
77
Param,
88
ParseUUIDPipe,
9+
Query,
910
UseGuards,
1011
} from '@nestjs/common';
1112
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
@@ -63,7 +64,7 @@ export class UsersController {
6364

6465
@Crud.GetMany(Routes().edu.user(':id').find())
6566
async getMany(
66-
@Body() request: dto.GetUsersQuery,
67+
@Query() query: dto.GetUsersQuery,
6768
@UserWithPermissions() userPermissions: UserPermissions,
6869
): Promise<GetUsersResponse> {
6970
userPermissions.check(['users:read']);
@@ -72,10 +73,9 @@ export class UsersController {
7273
.findAll({
7374
where: {
7475
roles: {
75-
schoolId: request.schoolId,
76+
schoolId: query.schoolId,
7677
},
7778
},
78-
relations: ['roles'],
7979
});
8080
return new dto.GetUsersResponse({
8181
items: this.mapper.mapArray(users, entities.User, dto.UserSummary),

modules/services/api/src/edu/edu.module.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import {
1717
} from './controllers';
1818
import { RolesMappingProfile } from './mappers/roles.mapper';
1919
import { UsersMappingProfile } from './mappers/users.mapper';
20-
import { IsRoleExistConstraint, IsUserExistConstraint } from './validations';
20+
import {
21+
IsRoleExistConstraint,
22+
IsSchoolExistConstraint,
23+
IsUserExistConstraint,
24+
} from './validations';
2125

2226
@Module({
2327
imports: [TypeOrmModule.forFeature([User, Role, UserRole, School])],
@@ -30,6 +34,7 @@ import { IsRoleExistConstraint, IsUserExistConstraint } from './validations';
3034
RolesMappingProfile,
3135
IsRoleExistConstraint,
3236
IsUserExistConstraint,
37+
IsSchoolExistConstraint,
3338
RevokedTokensService,
3439
UsersMappingProfile,
3540
],

0 commit comments

Comments
 (0)