Merge pull request #539 from Hexastack/538-issue-user-module-strictnullchecks-issues

fix: User module issues
This commit is contained in:
Med Marrouchi 2025-01-09 10:54:43 +01:00 committed by GitHub
commit 41f3c128f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 157 additions and 125 deletions

View File

@ -60,7 +60,7 @@ describe('AuthController', () => {
let invitationService: InvitationService; let invitationService: InvitationService;
let roleService: RoleService; let roleService: RoleService;
let jwtService: JwtService; let jwtService: JwtService;
let role: Role; let role: Role | null;
let baseUser: UserCreateDto; let baseUser: UserCreateDto;
beforeAll(async () => { beforeAll(async () => {
@ -136,7 +136,8 @@ describe('AuthController', () => {
username: 'test', username: 'test',
first_name: 'test', first_name: 'test',
last_name: 'test', last_name: 'test',
roles: [role.id], roles: [role!.id],
avatar: null,
}; };
await invitationService.create(baseUser); await invitationService.create(baseUser);
}); });
@ -157,6 +158,7 @@ describe('AuthController', () => {
email: 'test@test.test', email: 'test@test.test',
password: 'test', password: 'test',
roles: ['invalid role value'], roles: ['invalid role value'],
avatar: null,
}; };
await expect(authController.signup(userCreateDto)).rejects.toThrow( await expect(authController.signup(userCreateDto)).rejects.toThrow(
@ -174,6 +176,7 @@ describe('AuthController', () => {
email: 'test@test.test', email: 'test@test.test',
password: 'test', password: 'test',
roles: ['659564cb4aa383c0d0dbc688'], roles: ['659564cb4aa383c0d0dbc688'],
avatar: null,
}; };
const result = await authController.signup(userCreateDto); const result = await authController.signup(userCreateDto);
expect(userService.create).toHaveBeenCalledWith(userCreateDto); expect(userService.create).toHaveBeenCalledWith(userCreateDto);

View File

@ -87,7 +87,7 @@ export class LocalAuthController extends BaseAuthController {
logger: LoggerService, logger: LoggerService,
private readonly userService: UserService, private readonly userService: UserService,
private readonly validateAccountService: ValidateAccountService, private readonly validateAccountService: ValidateAccountService,
private readonly invitationService?: InvitationService, private readonly invitationService: InvitationService,
) { ) {
super(logger); super(logger);
} }

View File

@ -28,7 +28,7 @@ import { ModelRepository } from '../repositories/model.repository';
import { PermissionRepository } from '../repositories/permission.repository'; import { PermissionRepository } from '../repositories/permission.repository';
import { RoleRepository } from '../repositories/role.repository'; import { RoleRepository } from '../repositories/role.repository';
import { UserRepository } from '../repositories/user.repository'; import { UserRepository } from '../repositories/user.repository';
import { ModelModel } from '../schemas/model.schema'; import { ModelFull, ModelModel } from '../schemas/model.schema';
import { PermissionModel } from '../schemas/permission.schema'; import { PermissionModel } from '../schemas/permission.schema';
import { RoleModel } from '../schemas/role.schema'; import { RoleModel } from '../schemas/role.schema';
import { UserModel } from '../schemas/user.schema'; import { UserModel } from '../schemas/user.schema';
@ -104,11 +104,12 @@ describe('ModelController', () => {
it('should find models, and for each model populate the corresponding permissions', async () => { it('should find models, and for each model populate the corresponding permissions', async () => {
jest.spyOn(modelService, 'findAndPopulate'); jest.spyOn(modelService, 'findAndPopulate');
const allRoles = await modelService.findAll(); const allModels = await modelService.findAll();
const allPermissions = await permissionService.findAll(); const allPermissions = await permissionService.findAll();
const result = await modelController.find(['permissions'], {}); const result = await modelController.find(['permissions'], {});
const modelsWithPermissionsAndUsers = allRoles.reduce((acc, currRole) => { const modelsWithPermissionsAndUsers = allModels.reduce(
(acc, currRole) => {
const modelWithPermissionsAndUsers = { const modelWithPermissionsAndUsers = {
...currRole, ...currRole,
permissions: allPermissions.filter((currPermission) => { permissions: allPermissions.filter((currPermission) => {
@ -118,7 +119,9 @@ describe('ModelController', () => {
acc.push(modelWithPermissionsAndUsers); acc.push(modelWithPermissionsAndUsers);
return acc; return acc;
}, []); },
[] as ModelFull[],
);
expect(modelService.findAndPopulate).toHaveBeenCalledWith({}); expect(modelService.findAndPopulate).toHaveBeenCalledWith({});
expect(result).toEqualPayload(modelsWithPermissionsAndUsers); expect(result).toEqualPayload(modelsWithPermissionsAndUsers);

View File

@ -47,7 +47,7 @@ import { UserRepository } from '../repositories/user.repository';
import { InvitationModel } from '../schemas/invitation.schema'; import { InvitationModel } from '../schemas/invitation.schema';
import { PermissionModel } from '../schemas/permission.schema'; import { PermissionModel } from '../schemas/permission.schema';
import { Role, RoleModel } from '../schemas/role.schema'; import { Role, RoleModel } from '../schemas/role.schema';
import { User, UserModel } from '../schemas/user.schema'; import { User, UserFull, UserModel } from '../schemas/user.schema';
import { PasswordResetService } from '../services/passwordReset.service'; import { PasswordResetService } from '../services/passwordReset.service';
import { PermissionService } from '../services/permission.service'; import { PermissionService } from '../services/permission.service';
import { RoleService } from '../services/role.service'; import { RoleService } from '../services/role.service';
@ -63,9 +63,9 @@ describe('UserController', () => {
let roleService: RoleService; let roleService: RoleService;
let invitationService: InvitationService; let invitationService: InvitationService;
let notFoundId: string; let notFoundId: string;
let role: Role; let role: Role | null;
let roles: Role[]; let roles: Role[];
let user: User; let user: User | null;
let passwordResetService: PasswordResetService; let passwordResetService: PasswordResetService;
let jwtService: JwtService; let jwtService: JwtService;
beforeAll(async () => { beforeAll(async () => {
@ -157,12 +157,12 @@ describe('UserController', () => {
describe('findOne', () => { describe('findOne', () => {
it('should find one user and populate its roles', async () => { it('should find one user and populate its roles', async () => {
jest.spyOn(userService, 'findOneAndPopulate'); jest.spyOn(userService, 'findOneAndPopulate');
const result = await userController.findOne(user.id, ['roles']); const result = await userController.findOne(user!.id, ['roles']);
expect(userService.findOneAndPopulate).toHaveBeenCalledWith(user.id); expect(userService.findOneAndPopulate).toHaveBeenCalledWith(user!.id);
expect(result).toEqualPayload( expect(result).toEqualPayload(
{ {
...userFixtures.find(({ username }) => username === 'admin'), ...userFixtures.find(({ username }) => username === 'admin'),
roles: roles.filter(({ id }) => user.roles.includes(id)), roles: roles.filter(({ id }) => user!.roles.includes(id)),
}, },
[...IGNORED_FIELDS, 'password', 'provider'], [...IGNORED_FIELDS, 'password', 'provider'],
); );
@ -176,13 +176,17 @@ describe('UserController', () => {
jest.spyOn(userService, 'findPageAndPopulate'); jest.spyOn(userService, 'findPageAndPopulate');
const result = await userService.findPageAndPopulate({}, pageQuery); const result = await userService.findPageAndPopulate({}, pageQuery);
const usersWithRoles = userFixtures.reduce((acc, currUser) => { const usersWithRoles = userFixtures.reduce(
(acc, currUser) => {
acc.push({ acc.push({
...currUser, ...currUser,
roles: roles.filter(({ id }) => user.roles.includes(id)), roles: roles.filter(({ id }) => user?.roles?.includes(id)),
avatar: null,
}); });
return acc; return acc;
}, []); },
[] as Omit<UserFull, 'id' | 'createdAt' | 'updatedAt'>[],
);
expect(userService.findPageAndPopulate).toHaveBeenCalledWith( expect(userService.findPageAndPopulate).toHaveBeenCalledWith(
{}, {},
@ -205,7 +209,8 @@ describe('UserController', () => {
last_name: 'testUser', last_name: 'testUser',
email: 'test@test.test', email: 'test@test.test',
password: 'test', password: 'test',
roles: [role.id], roles: [role!.id],
avatar: null,
}; };
const result = await userController.create(userDto); const result = await userController.create(userDto);
expect(userService.create).toHaveBeenCalledWith(userDto); expect(userService.create).toHaveBeenCalledWith(userDto);
@ -224,16 +229,16 @@ describe('UserController', () => {
it('should return updated user', async () => { it('should return updated user', async () => {
jest.spyOn(userService, 'updateOne'); jest.spyOn(userService, 'updateOne');
const result = await userController.updateOne( const result = await userController.updateOne(
{ user: { id: user.id } } as any, { user: { id: user!.id } } as any,
user.id, user!.id,
updateDto, updateDto,
); );
expect(userService.updateOne).toHaveBeenCalledWith(user.id, updateDto); expect(userService.updateOne).toHaveBeenCalledWith(user!.id, updateDto);
expect(result).toEqualPayload( expect(result).toEqualPayload(
{ {
...userFixtures.find(({ username }) => username === 'admin'), ...userFixtures.find(({ username }) => username === 'admin'),
...updateDto, ...updateDto,
roles: user.roles, roles: user!.roles,
}, },
[...IGNORED_FIELDS, 'password', 'provider'], [...IGNORED_FIELDS, 'password', 'provider'],
); );
@ -243,44 +248,43 @@ describe('UserController', () => {
describe('updateStateAndRoles', () => { describe('updateStateAndRoles', () => {
it('should return updated user', async () => { it('should return updated user', async () => {
const updateDto: UserUpdateStateAndRolesDto = { const updateDto: UserUpdateStateAndRolesDto = {
roles: [role.id], roles: [role!.id],
}; };
jest.spyOn(userService, 'updateOne'); jest.spyOn(userService, 'updateOne');
const result = await userController.updateStateAndRoles( const result = await userController.updateStateAndRoles(
user.id, user!.id,
updateDto, updateDto,
{ {
passport: { passport: {
user: { id: user.id }, user: { id: user!.id },
}, },
} as ExpressSession, } as ExpressSession,
); );
expect(userService.updateOne).toHaveBeenCalledWith(user.id, updateDto); expect(userService.updateOne).toHaveBeenCalledWith(user!.id, updateDto);
expect(result).toEqualPayload( expect(result).toEqualPayload(
{ {
...userFixtures.find(({ username }) => username === 'admin'), ...userFixtures.find(({ username }) => username === 'admin'),
...updateDto, ...updateDto,
}, },
[...IGNORED_FIELDS, 'first_name', 'password', 'provider'], [...IGNORED_FIELDS, 'first_name', 'password', 'provider'],
); );
}); });
it('should return updated user after adding an extra role', async () => { it('should return updated user after adding an extra role', async () => {
const updateDto: UserUpdateStateAndRolesDto = { const updateDto: UserUpdateStateAndRolesDto = {
roles: [role.id, roles[1].id], roles: [role!.id, roles[1].id],
}; };
jest.spyOn(userService, 'updateOne'); jest.spyOn(userService, 'updateOne');
const result = await userController.updateStateAndRoles( const result = await userController.updateStateAndRoles(
user.id, user!.id,
updateDto, updateDto,
{ {
passport: { passport: {
user: { id: user.id }, user: { id: user!.id },
}, },
} as ExpressSession, } as ExpressSession,
); );
expect(userService.updateOne).toHaveBeenCalledWith(user.id, updateDto); expect(userService.updateOne).toHaveBeenCalledWith(user!.id, updateDto);
expect(result).toEqualPayload( expect(result).toEqualPayload(
{ {
...userFixtures.find(({ username }) => username === 'admin'), ...userFixtures.find(({ username }) => username === 'admin'),
@ -296,9 +300,9 @@ describe('UserController', () => {
state: false, state: false,
}; };
await expect( await expect(
userController.updateStateAndRoles(user.id, updateDto, { userController.updateStateAndRoles(user!.id, updateDto, {
passport: { passport: {
user: { id: user.id }, user: { id: user!.id },
}, },
} as ExpressSession), } as ExpressSession),
).rejects.toThrow(ForbiddenException); ).rejects.toThrow(ForbiddenException);
@ -309,9 +313,9 @@ describe('UserController', () => {
roles: [], roles: [],
}; };
await expect( await expect(
userController.updateStateAndRoles(user.id, updateDto, { userController.updateStateAndRoles(user!.id, updateDto, {
passport: { passport: {
user: { id: user.id }, user: { id: user!.id },
}, },
} as ExpressSession), } as ExpressSession),
).rejects.toThrow(ForbiddenException); ).rejects.toThrow(ForbiddenException);
@ -343,8 +347,8 @@ describe('UserController', () => {
describe('deleteOne', () => { describe('deleteOne', () => {
it('should delete user by id', async () => { it('should delete user by id', async () => {
const result = await userController.deleteOne(user.id); const result = await userController.deleteOne(user!.id);
notFoundId = user.id; notFoundId = user!.id;
expect(result).toEqual({ expect(result).toEqual({
acknowledged: true, acknowledged: true,
deletedCount: 1, deletedCount: 1,

View File

@ -142,12 +142,12 @@ export class ReadOnlyUserController extends BaseController<
); );
const currentPermissions = await this.permissionService.findAndPopulate({ const currentPermissions = await this.permissionService.findAndPopulate({
role: { role: {
$in: currentUser.roles.map(({ id }) => id), $in: currentUser?.roles.map(({ id }) => id),
}, },
}); });
return { return {
roles: currentUser.roles, roles: currentUser?.roles,
permissions: currentPermissions.map((permission) => { permissions: currentPermissions.map((permission) => {
if (permission.model) { if (permission.model) {
return { return {
@ -248,7 +248,9 @@ export class ReadWriteUserController extends ReadOnlyUserController {
roles: (await this.roleService.findAll()) roles: (await this.roleService.findAll())
.filter((role) => user.roles.includes(role.id)) .filter((role) => user.roles.includes(role.id))
.map((role) => role.id), .map((role) => role.id),
avatar: (await this.attachmentService.findOne(user.avatar))?.id, avatar: user.avatar
? (await this.attachmentService.findOne(user.avatar))?.id
: null,
}, },
}); });
return await this.userService.create(user); return await this.userService.create(user);
@ -285,7 +287,7 @@ export class ReadWriteUserController extends ReadOnlyUserController {
@Body() userUpdate: UserEditProfileDto, @Body() userUpdate: UserEditProfileDto,
@UploadedFile() avatarFile?: Express.Multer.File, @UploadedFile() avatarFile?: Express.Multer.File,
) { ) {
if (!('id' in req.user && req.user.id) || req.user.id !== id) { if (!(req.user && 'id' in req.user && req.user.id) || req.user.id !== id) {
throw new ForbiddenException(); throw new ForbiddenException();
} }
@ -339,18 +341,20 @@ export class ReadWriteUserController extends ReadOnlyUserController {
@Body() body: UserUpdateStateAndRolesDto, @Body() body: UserUpdateStateAndRolesDto,
@Session() session: ExpressSession, @Session() session: ExpressSession,
) { ) {
const oldRoles = (await this.userService.findOne(id)).roles; const oldRoles = (await this.userService.findOne(id))?.roles;
const newRoles = body.roles; const newRoles = body.roles;
const { id: adminRoleId } = await this.roleService.findOne({ const { id: adminRoleId } =
(await this.roleService.findOne({
name: 'admin', name: 'admin',
}); })) || {};
if (id === session.passport?.user?.id && body.state === false) { if (id === session.passport?.user?.id && body.state === false) {
throw new ForbiddenException('Your account state is protected'); throw new ForbiddenException('Your account state is protected');
} }
if ( if (
adminRoleId &&
session?.passport?.user?.id === id && session?.passport?.user?.id === id &&
oldRoles.includes(adminRoleId) && oldRoles?.includes(adminRoleId) &&
!newRoles.includes(adminRoleId) !newRoles?.includes(adminRoleId)
) { ) {
throw new ForbiddenException('Admin privileges are protected'); throw new ForbiddenException('Admin privileges are protected');
} }

View File

@ -60,7 +60,7 @@ export class UserCreateDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
@IsObjectId({ message: 'Avatar must be a valid ObjectId' }) @IsObjectId({ message: 'Avatar must be a valid ObjectId' })
avatar?: string; avatar: string | null = null;
} }
export class UserEditProfileDto extends OmitType(PartialType(UserCreateDto), [ export class UserEditProfileDto extends OmitType(PartialType(UserCreateDto), [

View File

@ -53,6 +53,7 @@ export class Ability implements CanActivate {
if (user?.roles?.length) { if (user?.roles?.length) {
if ( if (
_parsedUrl.pathname &&
[ [
// Allow access to all routes available for authenticated users // Allow access to all routes available for authenticated users
'/auth/logout', '/auth/logout',
@ -68,9 +69,9 @@ export class Ability implements CanActivate {
) { ) {
return true; return true;
} }
const modelFromPathname = _parsedUrl.pathname const modelFromPathname = _parsedUrl?.pathname
.split('/')[1] ?.split('/')[1]
.toLowerCase() as TModel; .toLowerCase() as TModel | undefined;
const permissions = await this.permissionService.getPermissions(); const permissions = await this.permissionService.getPermissions();
@ -80,6 +81,7 @@ export class Ability implements CanActivate {
.map(([_, value]) => value); .map(([_, value]) => value);
if ( if (
modelFromPathname &&
permissionsFromRoles.some((permission) => permissionsFromRoles.some((permission) =>
permission[modelFromPathname]?.includes(MethodToAction[method]), permission[modelFromPathname]?.includes(MethodToAction[method]),
) )

View File

@ -19,7 +19,10 @@ export class AuthSerializer extends PassportSerializer {
super(); super();
} }
serializeUser(user: User, done: (err: Error, user: SessionUser) => void) { serializeUser(
user: User,
done: (err: Error | null, user: SessionUser) => void,
) {
done(null, { done(null, {
id: user.id, id: user.id,
first_name: user.first_name, first_name: user.first_name,
@ -29,9 +32,9 @@ export class AuthSerializer extends PassportSerializer {
async deserializeUser( async deserializeUser(
payload: SessionUser, payload: SessionUser,
done: (err: Error, user: SessionUser) => void, done: (err: Error | null, user: SessionUser | null) => void,
) { ) {
const user = await this.userService.findOne(payload.id); const user = payload.id ? await this.userService.findOne(payload.id) : null;
user ? done(null, user) : done(null, null); done(null, user);
} }
} }

View File

@ -23,7 +23,11 @@ import {
rootMongooseTestModule, rootMongooseTestModule,
} from '@/utils/test/test'; } from '@/utils/test/test';
import { Invitation, InvitationModel } from '../schemas/invitation.schema'; import {
Invitation,
InvitationFull,
InvitationModel,
} from '../schemas/invitation.schema';
import { PermissionModel } from '../schemas/permission.schema'; import { PermissionModel } from '../schemas/permission.schema';
import { RoleModel } from '../schemas/role.schema'; import { RoleModel } from '../schemas/role.schema';
@ -79,10 +83,10 @@ describe('InvitationRepository', () => {
); );
const result = await invitationRepository.findOneAndPopulate( const result = await invitationRepository.findOneAndPopulate(
invitation.id, invitation!.id,
); );
expect(invitationModel.findById).toHaveBeenCalledWith( expect(invitationModel.findById).toHaveBeenCalledWith(
invitation.id, invitation!.id,
undefined, undefined,
); );
expect(result).toEqualPayload({ expect(result).toEqualPayload({
@ -111,7 +115,7 @@ describe('InvitationRepository', () => {
roles: allRoles.filter((role) => currInv.roles.includes(role.id)), roles: allRoles.filter((role) => currInv.roles.includes(role.id)),
}); });
return acc; return acc;
}, []); }, [] as InvitationFull[]);
expect(invitationModel.find).toHaveBeenCalledWith({}, undefined); expect(invitationModel.find).toHaveBeenCalledWith({}, undefined);
expect(result).toEqualPayload(invitationsWithRoles); expect(result).toEqualPayload(invitationsWithRoles);

View File

@ -20,7 +20,7 @@ import {
import { ModelRepository } from '../repositories/model.repository'; import { ModelRepository } from '../repositories/model.repository';
import { PermissionRepository } from '../repositories/permission.repository'; import { PermissionRepository } from '../repositories/permission.repository';
import { ModelModel } from '../schemas/model.schema'; import { ModelFull, ModelModel } from '../schemas/model.schema';
import { Permission, PermissionModel } from '../schemas/permission.schema'; import { Permission, PermissionModel } from '../schemas/permission.schema';
import { Model as ModelType } from './../schemas/model.schema'; import { Model as ModelType } from './../schemas/model.schema';
@ -29,7 +29,7 @@ describe('ModelRepository', () => {
let modelRepository: ModelRepository; let modelRepository: ModelRepository;
let permissionRepository: PermissionRepository; let permissionRepository: PermissionRepository;
let modelModel: Model<ModelType>; let modelModel: Model<ModelType>;
let model: ModelType; let model: ModelType | null;
let permissions: Permission[]; let permissions: Permission[];
beforeAll(async () => { beforeAll(async () => {
@ -45,7 +45,7 @@ describe('ModelRepository', () => {
modelRepository = module.get<ModelRepository>(ModelRepository); modelRepository = module.get<ModelRepository>(ModelRepository);
modelModel = module.get<Model<ModelType>>(getModelToken('Model')); modelModel = module.get<Model<ModelType>>(getModelToken('Model'));
model = await modelRepository.findOne({ name: 'ContentType' }); model = await modelRepository.findOne({ name: 'ContentType' });
permissions = await permissionRepository.find({ model: model.id }); permissions = await permissionRepository.find({ model: model!.id });
}); });
afterAll(closeInMongodConnection); afterAll(closeInMongodConnection);
@ -55,8 +55,8 @@ describe('ModelRepository', () => {
describe('findOneAndPopulate', () => { describe('findOneAndPopulate', () => {
it('should find a model and populate its permissions', async () => { it('should find a model and populate its permissions', async () => {
jest.spyOn(modelModel, 'findById'); jest.spyOn(modelModel, 'findById');
const result = await modelRepository.findOneAndPopulate(model.id); const result = await modelRepository.findOneAndPopulate(model!.id);
expect(modelModel.findById).toHaveBeenCalledWith(model.id, undefined); expect(modelModel.findById).toHaveBeenCalledWith(model!.id, undefined);
expect(result).toEqualPayload({ expect(result).toEqualPayload({
...modelFixtures.find(({ name }) => name === 'ContentType'), ...modelFixtures.find(({ name }) => name === 'ContentType'),
permissions, permissions,
@ -73,12 +73,12 @@ describe('ModelRepository', () => {
const modelsWithPermissions = allModels.reduce((acc, currModel) => { const modelsWithPermissions = allModels.reduce((acc, currModel) => {
acc.push({ acc.push({
...currModel, ...currModel,
permissions: allPermissions.filter((permission) => { permissions: allPermissions.filter(
return permission.model === currModel.id; (permission) => permission.model === currModel.id,
}), ),
}); });
return acc; return acc;
}, []); }, [] as ModelFull[]);
expect(modelModel.find).toHaveBeenCalledWith({}, undefined); expect(modelModel.find).toHaveBeenCalledWith({}, undefined);
expect(result).toEqualPayload(modelsWithPermissions); expect(result).toEqualPayload(modelsWithPermissions);
}); });

View File

@ -28,13 +28,13 @@ import { RoleRepository } from '../repositories/role.repository';
import { UserRepository } from '../repositories/user.repository'; import { UserRepository } from '../repositories/user.repository';
import { PermissionModel } from '../schemas/permission.schema'; import { PermissionModel } from '../schemas/permission.schema';
import { Role, RoleModel } from '../schemas/role.schema'; import { Role, RoleModel } from '../schemas/role.schema';
import { User, UserModel } from '../schemas/user.schema'; import { User, UserFull, UserModel } from '../schemas/user.schema';
describe('UserRepository', () => { describe('UserRepository', () => {
let roleRepository: RoleRepository; let roleRepository: RoleRepository;
let userRepository: UserRepository; let userRepository: UserRepository;
let userModel: Model<User>; let userModel: Model<User>;
let user: User; let user: User | null;
let allRoles: Role[]; let allRoles: Role[];
const FIELDS_TO_IGNORE: string[] = [ const FIELDS_TO_IGNORE: string[] = [
@ -90,12 +90,12 @@ describe('UserRepository', () => {
describe('findOneAndPopulate', () => { describe('findOneAndPopulate', () => {
it('should find one user and populate its role', async () => { it('should find one user and populate its role', async () => {
jest.spyOn(userModel, 'findById'); jest.spyOn(userModel, 'findById');
const result = await userRepository.findOneAndPopulate(user.id); const result = await userRepository.findOneAndPopulate(user!.id);
expect(userModel.findById).toHaveBeenCalledWith(user.id, undefined); expect(userModel.findById).toHaveBeenCalledWith(user!.id, undefined);
expect(result).toEqualPayload( expect(result).toEqualPayload(
{ {
...userFixtures.find(({ username }) => username === 'admin'), ...userFixtures.find(({ username }) => username === 'admin'),
roles: allRoles.filter(({ id }) => user.roles.includes(id)), roles: allRoles.filter(({ id }) => user!.roles.includes(id)),
}, },
FIELDS_TO_IGNORE, FIELDS_TO_IGNORE,
); );
@ -113,10 +113,11 @@ describe('UserRepository', () => {
const usersWithRoles = allUsers.reduce((acc, currUser) => { const usersWithRoles = allUsers.reduce((acc, currUser) => {
acc.push({ acc.push({
...currUser, ...currUser,
roles: allRoles.filter(({ id }) => user.roles.includes(id)), roles: allRoles.filter(({ id }) => user?.roles.includes(id)),
avatar: null,
}); });
return acc; return acc;
}, []); }, [] as UserFull[]);
expect(userModel.find).toHaveBeenCalledWith({}, undefined); expect(userModel.find).toHaveBeenCalledWith({}, undefined);
expect(result).toEqualPayload(usersWithRoles); expect(result).toEqualPayload(usersWithRoles);

View File

@ -91,7 +91,7 @@ export class UserStub extends BaseSchema {
ref: 'Attachment', ref: 'Attachment',
default: null, default: null,
}) })
avatar?: unknown; avatar: unknown;
@Prop({ @Prop({
type: String, type: String,
@ -112,7 +112,7 @@ export class User extends UserStub {
roles: string[]; roles: string[];
@Transform(({ obj }) => obj.avatar?.toString() || null) @Transform(({ obj }) => obj.avatar?.toString() || null)
avatar?: string; avatar: string | null;
} }
@Schema({ timestamps: true }) @Schema({ timestamps: true })

View File

@ -17,6 +17,7 @@ export const userModels = (roles: string[]): UserCreateDto[] => {
email: 'admin@admin.admin', email: 'admin@admin.admin',
password: 'adminadmin', password: 'adminadmin',
roles, roles,
avatar: null,
}, },
]; ];
}; };

View File

@ -82,7 +82,7 @@ describe('AuthService', () => {
{}, {},
undefined, undefined,
); );
expect(result.id).toBe(user.id); expect(result!.id).toBe(user!.id);
}); });
it('should not validate user if the provided password is incorrect', async () => { it('should not validate user if the provided password is incorrect', async () => {
const result = await authService.validateUser( const result = await authService.validateUser(

View File

@ -150,7 +150,7 @@ describe('InvitationService', () => {
const role = await roleRepository.findOne({}); const role = await roleRepository.findOne({});
const newInvitation: InvitationCreateDto = { const newInvitation: InvitationCreateDto = {
email: 'test@testland.tst', email: 'test@testland.tst',
roles: [role.id.toString()], roles: [role!.id.toString()],
}; };
jest.spyOn(invitationRepository, 'create'); jest.spyOn(invitationRepository, 'create');

View File

@ -39,10 +39,10 @@ export class InvitationService extends BaseService<
@Inject(InvitationRepository) @Inject(InvitationRepository)
readonly repository: InvitationRepository, readonly repository: InvitationRepository,
@Inject(JwtService) private readonly jwtService: JwtService, @Inject(JwtService) private readonly jwtService: JwtService,
@Optional() private readonly mailerService: MailerService | undefined,
private logger: LoggerService, private logger: LoggerService,
protected readonly i18n: I18nService, protected readonly i18n: I18nService,
public readonly languageService: LanguageService, public readonly languageService: LanguageService,
@Optional() private readonly mailerService?: MailerService,
) { ) {
super(repository); super(repository);
} }

View File

@ -19,7 +19,7 @@ import {
import { ModelRepository } from '../repositories/model.repository'; import { ModelRepository } from '../repositories/model.repository';
import { PermissionRepository } from '../repositories/permission.repository'; import { PermissionRepository } from '../repositories/permission.repository';
import { Model, ModelModel } from '../schemas/model.schema'; import { Model, ModelFull, ModelModel } from '../schemas/model.schema';
import { Permission, PermissionModel } from '../schemas/permission.schema'; import { Permission, PermissionModel } from '../schemas/permission.schema';
import { ModelService } from './model.service'; import { ModelService } from './model.service';
@ -28,7 +28,7 @@ describe('ModelService', () => {
let modelService: ModelService; let modelService: ModelService;
let modelRepository: ModelRepository; let modelRepository: ModelRepository;
let permissionRepository: PermissionRepository; let permissionRepository: PermissionRepository;
let model: Model; let model: Model | null;
let permissions: Permission[]; let permissions: Permission[];
beforeAll(async () => { beforeAll(async () => {
@ -49,7 +49,7 @@ describe('ModelService', () => {
module.get<PermissionRepository>(PermissionRepository); module.get<PermissionRepository>(PermissionRepository);
modelRepository = module.get<ModelRepository>(ModelRepository); modelRepository = module.get<ModelRepository>(ModelRepository);
model = await modelRepository.findOne({ name: 'ContentType' }); model = await modelRepository.findOne({ name: 'ContentType' });
permissions = await permissionRepository.find({ model: model.id }); permissions = await permissionRepository.find({ model: model!.id });
}); });
afterAll(closeInMongodConnection); afterAll(closeInMongodConnection);
@ -58,7 +58,7 @@ describe('ModelService', () => {
describe('findOneAndPopulate', () => { describe('findOneAndPopulate', () => {
it('should find a model and populate its permissions', async () => { it('should find a model and populate its permissions', async () => {
const result = await modelService.findOneAndPopulate(model.id); const result = await modelService.findOneAndPopulate(model!.id);
expect(result).toEqualPayload({ expect(result).toEqualPayload({
...modelFixtures.find(({ name }) => name === 'ContentType'), ...modelFixtures.find(({ name }) => name === 'ContentType'),
permissions, permissions,
@ -74,12 +74,12 @@ describe('ModelService', () => {
const modelsWithPermissions = models.reduce((acc, currModel) => { const modelsWithPermissions = models.reduce((acc, currModel) => {
acc.push({ acc.push({
...currModel, ...currModel,
permissions: permissions.filter((permission) => { permissions: permissions.filter(
return permission.model === currModel.id; (permission) => permission.model === currModel.id,
}), ),
}); });
return acc; return acc;
}, []); }, [] as ModelFull[]);
expect(modelRepository.findAndPopulate).toHaveBeenCalledWith( expect(modelRepository.findAndPopulate).toHaveBeenCalledWith(
{}, {},
undefined, undefined,

View File

@ -103,16 +103,15 @@ describe('PasswordResetService', () => {
}).compile(); }).compile();
passwordResetService = passwordResetService =
module.get<PasswordResetService>(PasswordResetService); module.get<PasswordResetService>(PasswordResetService);
mailerService = module.get<MailerService>(MailerService); mailerService = module.get<MailerService>(MailerService);
jwtService = module.get<JwtService>(JwtService); jwtService = module.get<JwtService>(JwtService);
userModel = module.get<Model<User>>(getModelToken('User')); userModel = module.get<Model<User>>(getModelToken('User'));
}); });
afterAll(async () => {
await closeInMongodConnection(); afterAll(closeInMongodConnection);
});
afterEach(jest.clearAllMocks); afterEach(jest.clearAllMocks);
describe('requestReset', () => { describe('requestReset', () => {
it('should send an email with a token', async () => { it('should send an email with a token', async () => {
const sendMailSpy = jest.spyOn(mailerService, 'sendMail'); const sendMailSpy = jest.spyOn(mailerService, 'sendMail');
@ -152,8 +151,8 @@ describe('PasswordResetService', () => {
expect(verifySpy).toHaveBeenCalled(); expect(verifySpy).toHaveBeenCalled();
const user = await userModel.findOne({ email: users[0].email }); const user = await userModel.findOne({ email: users[0].email });
expect(user.resetToken).toBeNull(); expect(user!.resetToken).toBeNull();
expect(compareSync('newPassword', user.password)).toBeTruthy(); expect(compareSync('newPassword', user!.password)).toBeTruthy();
}); });
}); });
}); });

View File

@ -32,11 +32,11 @@ import { UserService } from './user.service';
export class PasswordResetService { export class PasswordResetService {
constructor( constructor(
@Inject(JwtService) private readonly jwtService: JwtService, @Inject(JwtService) private readonly jwtService: JwtService,
@Optional() private readonly mailerService: MailerService | undefined,
private logger: LoggerService, private logger: LoggerService,
private readonly userService: UserService, private readonly userService: UserService,
public readonly i18n: I18nService, public readonly i18n: I18nService,
public readonly languageService: LanguageService, public readonly languageService: LanguageService,
@Optional() private readonly mailerService?: MailerService,
) {} ) {}
public readonly jwtSignOptions: JwtSignOptions = { public readonly jwtSignOptions: JwtSignOptions = {
@ -105,7 +105,7 @@ export class PasswordResetService {
// first step is to check if the token has been used // first step is to check if the token has been used
const user = await this.userService.findOne({ email: payload.email }); const user = await this.userService.findOne({ email: payload.email });
if (!user.resetToken || compareSync(user.resetToken, token)) { if (!user?.resetToken || compareSync(user.resetToken, token)) {
throw new UnauthorizedException('Invalid token'); throw new UnauthorizedException('Invalid token');
} }

View File

@ -29,7 +29,7 @@ import { RoleRepository } from '../repositories/role.repository';
import { UserRepository } from '../repositories/user.repository'; import { UserRepository } from '../repositories/user.repository';
import { PermissionModel } from '../schemas/permission.schema'; import { PermissionModel } from '../schemas/permission.schema';
import { Role, RoleModel } from '../schemas/role.schema'; import { Role, RoleModel } from '../schemas/role.schema';
import { User, UserModel } from '../schemas/user.schema'; import { User, UserFull, UserModel } from '../schemas/user.schema';
import { PermissionService } from './permission.service'; import { PermissionService } from './permission.service';
import { RoleService } from './role.service'; import { RoleService } from './role.service';
@ -39,7 +39,7 @@ describe('UserService', () => {
let userService: UserService; let userService: UserService;
let roleRepository: RoleRepository; let roleRepository: RoleRepository;
let userRepository: UserRepository; let userRepository: UserRepository;
let user: User; let user: User | null;
let allRoles: Role[]; let allRoles: Role[];
const FIELDS_TO_IGNORE: string[] = [ const FIELDS_TO_IGNORE: string[] = [
...IGNORED_TEST_FIELDS, ...IGNORED_TEST_FIELDS,
@ -99,15 +99,15 @@ describe('UserService', () => {
describe('findOneAndPopulate', () => { describe('findOneAndPopulate', () => {
it('should find one user and populate its role', async () => { it('should find one user and populate its role', async () => {
jest.spyOn(userRepository, 'findOneAndPopulate'); jest.spyOn(userRepository, 'findOneAndPopulate');
const result = await userService.findOneAndPopulate(user.id); const result = await userService.findOneAndPopulate(user!.id);
expect(userRepository.findOneAndPopulate).toHaveBeenCalledWith( expect(userRepository.findOneAndPopulate).toHaveBeenCalledWith(
user.id, user!.id,
undefined, undefined,
); );
expect(result).toEqualPayload( expect(result).toEqualPayload(
{ {
...userFixtures.find(({ username }) => username === 'admin'), ...userFixtures.find(({ username }) => username === 'admin'),
roles: allRoles.filter(({ id }) => user.roles.includes(id)), roles: allRoles.filter(({ id }) => user!.roles.includes(id)),
}, },
FIELDS_TO_IGNORE, FIELDS_TO_IGNORE,
); );
@ -120,13 +120,17 @@ describe('UserService', () => {
jest.spyOn(userRepository, 'findPageAndPopulate'); jest.spyOn(userRepository, 'findPageAndPopulate');
const allUsers = await userRepository.findAll(); const allUsers = await userRepository.findAll();
const result = await userService.findPageAndPopulate({}, pageQuery); const result = await userService.findPageAndPopulate({}, pageQuery);
const usersWithRoles = allUsers.reduce((acc, currUser) => { const usersWithRoles = allUsers.reduce(
(acc, { avatar: _avatar, roles: _roles, ...rest }) => {
acc.push({ acc.push({
...currUser, ...rest,
roles: allRoles.filter(({ id }) => user.roles.includes(id)), roles: allRoles.filter(({ id }) => user?.roles?.includes(id)),
avatar: null,
}); });
return acc; return acc;
}, []); },
[] as UserFull[],
);
expect(userRepository.findPageAndPopulate).toHaveBeenCalledWith( expect(userRepository.findPageAndPopulate).toHaveBeenCalledWith(
{}, {},

View File

@ -36,10 +36,10 @@ export class ValidateAccountService {
constructor( constructor(
@Inject(JwtService) private readonly jwtService: JwtService, @Inject(JwtService) private readonly jwtService: JwtService,
private readonly userService: UserService, private readonly userService: UserService,
@Optional() private readonly mailerService: MailerService | undefined,
private logger: LoggerService, private logger: LoggerService,
private readonly i18n: I18nService, private readonly i18n: I18nService,
private readonly languageService: LanguageService, private readonly languageService: LanguageService,
@Optional() private readonly mailerService?: MailerService,
) {} ) {}
/** /**

View File

@ -9,7 +9,7 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { UserCreateDto } from '@/user/dto/user.dto'; import { UserCreateDto } from '@/user/dto/user.dto';
import { UserModel, User } from '@/user/schemas/user.schema'; import { User, UserModel } from '@/user/schemas/user.schema';
import { hash } from '@/user/utilities/bcryptjs'; import { hash } from '@/user/utilities/bcryptjs';
import { getFixturesWithDefaultValues } from '../defaultValues'; import { getFixturesWithDefaultValues } from '../defaultValues';
@ -25,6 +25,7 @@ export const users: UserCreateDto[] = [
email: 'admin@admin.admin', email: 'admin@admin.admin',
password: 'adminadmin', password: 'adminadmin',
roles: ['0', '1'], roles: ['0', '1'],
avatar: null,
}, },
]; ];
@ -34,7 +35,6 @@ export const userDefaultValues: TFixturesDefaultValues<User> = {
timezone: 'Europe/Berlin', timezone: 'Europe/Berlin',
sendEmail: false, sendEmail: false,
resetCount: 0, resetCount: 0,
avatar: null,
}; };
export const getUserFixtures = (users: UserCreateDto[]) => export const getUserFixtures = (users: UserCreateDto[]) =>

View File

@ -55,7 +55,7 @@ export type RecursivePartial<T> = {
: T[P]; : T[P];
}; };
//base controller validator types //base controller validator types
type TAllowedKeys<T, TStub, TValue = string[]> = { type TAllowedKeys<T, TStub, TValue = (string | null | undefined)[]> = {
[key in keyof Record< [key in keyof Record<
TFilterKeysOfType< TFilterKeysOfType<
TFilterPopulateFields<TFilterKeysOfNeverType<T>, TStub>, TFilterPopulateFields<TFilterKeysOfNeverType<T>, TStub>,
@ -69,13 +69,17 @@ export type TValidateProps<T, TStub> = {
dto: dto:
| Partial<TAllowedKeys<T, TStub>> | Partial<TAllowedKeys<T, TStub>>
| Partial<TAllowedKeys<T, TStub, string>>; | Partial<TAllowedKeys<T, TStub, string>>;
allowedIds: Partial<TAllowedKeys<T, TStub> & TAllowedKeys<T, TStub, string>>; allowedIds: TAllowedKeys<T, TStub> &
TAllowedKeys<T, TStub, string | null | undefined>;
}; };
//populate types //populate types
export type TFilterPopulateFields<T, TStub> = Omit< export type TFilterPopulateFields<T, TStub> = Omit<
T, T,
TFilterKeysOfType<TStub, string | number | boolean | object> TFilterKeysOfType<
TStub,
null | undefined | string | number | boolean | object
>
>; >;
//search filter types //search filter types