test(user): write tests for ability and can
This commit is contained in:
46
packages/backend/src/helpers/user-ability.test.js
Normal file
46
packages/backend/src/helpers/user-ability.test.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import userAbility from './user-ability.js';
|
||||||
|
|
||||||
|
describe('userAbility', () => {
|
||||||
|
it('should return PureAbility instantiated with user permissions', () => {
|
||||||
|
const user = {
|
||||||
|
permissions: [
|
||||||
|
{
|
||||||
|
subject: 'Flow',
|
||||||
|
action: 'read',
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
role: {
|
||||||
|
name: 'User',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ability = userAbility(user);
|
||||||
|
|
||||||
|
expect(ability.rules).toStrictEqual(user.permissions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return permission-less PureAbility for user with no role', () => {
|
||||||
|
const user = {
|
||||||
|
permissions: [
|
||||||
|
{
|
||||||
|
subject: 'Flow',
|
||||||
|
action: 'read',
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
role: null,
|
||||||
|
};
|
||||||
|
const ability = userAbility(user);
|
||||||
|
|
||||||
|
expect(ability.rules).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return permission-less PureAbility for user with no permissions', () => {
|
||||||
|
const user = { permissions: null, role: { name: 'User' } };
|
||||||
|
const ability = userAbility(user);
|
||||||
|
|
||||||
|
expect(ability.rules).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
});
|
@@ -212,6 +212,10 @@ class User extends Base {
|
|||||||
return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`;
|
return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get ability() {
|
||||||
|
return userAbility(this);
|
||||||
|
}
|
||||||
|
|
||||||
static async authenticate(email, password) {
|
static async authenticate(email, password) {
|
||||||
const user = await User.query().findOne({
|
const user = await User.query().findOne({
|
||||||
email: email?.toLowerCase() || null,
|
email: email?.toLowerCase() || null,
|
||||||
@@ -583,6 +587,21 @@ class User extends Base {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
can(action, subject) {
|
||||||
|
const can = this.ability.can(action, subject);
|
||||||
|
|
||||||
|
if (!can) throw new NotAuthorizedError('The user is not authorized!');
|
||||||
|
|
||||||
|
const relevantRule = this.ability.relevantRuleFor(action, subject);
|
||||||
|
|
||||||
|
const conditions = relevantRule?.conditions || [];
|
||||||
|
const conditionMap = Object.fromEntries(
|
||||||
|
conditions.map((condition) => [condition, true])
|
||||||
|
);
|
||||||
|
|
||||||
|
return conditionMap;
|
||||||
|
}
|
||||||
|
|
||||||
async $beforeInsert(queryContext) {
|
async $beforeInsert(queryContext) {
|
||||||
await super.$beforeInsert(queryContext);
|
await super.$beforeInsert(queryContext);
|
||||||
|
|
||||||
@@ -634,33 +653,6 @@ class User extends Base {
|
|||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
get ability() {
|
|
||||||
return userAbility(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
can(action, subject) {
|
|
||||||
const can = this.ability.can(action, subject);
|
|
||||||
|
|
||||||
if (!can) throw new NotAuthorizedError('The user is not authorized!');
|
|
||||||
|
|
||||||
const relevantRule = this.ability.relevantRuleFor(action, subject);
|
|
||||||
|
|
||||||
const conditions = relevantRule?.conditions || [];
|
|
||||||
const conditionMap = Object.fromEntries(
|
|
||||||
conditions.map((condition) => [condition, true])
|
|
||||||
);
|
|
||||||
|
|
||||||
return conditionMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
cannot(action, subject) {
|
|
||||||
const cannot = this.ability.cannot(action, subject);
|
|
||||||
|
|
||||||
if (cannot) throw new NotAuthorizedError();
|
|
||||||
|
|
||||||
return cannot;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default User;
|
export default User;
|
||||||
|
@@ -20,6 +20,7 @@ import {
|
|||||||
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
||||||
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
} from '../helpers/remove-job-configuration.js';
|
} from '../helpers/remove-job-configuration.js';
|
||||||
|
import * as userAbilityModule from '../helpers/user-ability.js';
|
||||||
import { createUser } from '../../test/factories/user.js';
|
import { createUser } from '../../test/factories/user.js';
|
||||||
import { createConnection } from '../../test/factories/connection.js';
|
import { createConnection } from '../../test/factories/connection.js';
|
||||||
import { createRole } from '../../test/factories/role.js';
|
import { createRole } from '../../test/factories/role.js';
|
||||||
@@ -218,6 +219,18 @@ describe('User model', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ability should return userAbility for the user', () => {
|
||||||
|
const user = new User();
|
||||||
|
user.fullName = 'Sample user';
|
||||||
|
|
||||||
|
const userAbilitySpy = vi
|
||||||
|
.spyOn(userAbilityModule, 'default')
|
||||||
|
.mockReturnValue('user-ability');
|
||||||
|
|
||||||
|
expect(user.ability).toStrictEqual('user-ability');
|
||||||
|
expect(userAbilitySpy).toHaveBeenNthCalledWith(1, user);
|
||||||
|
});
|
||||||
|
|
||||||
describe('authenticate', () => {
|
describe('authenticate', () => {
|
||||||
it('should create and return the token for correct email and password', async () => {
|
it('should create and return the token for correct email and password', async () => {
|
||||||
const user = await createUser({
|
const user = await createUser({
|
||||||
@@ -1184,4 +1197,51 @@ describe('User model', () => {
|
|||||||
).rejects.toThrowError('NotFoundError');
|
).rejects.toThrowError('NotFoundError');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('can', () => {
|
||||||
|
it('should return conditions for the given action and subject of the user', async () => {
|
||||||
|
const userRole = await createRole({ name: 'User' });
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
roleId: userRole.id,
|
||||||
|
subject: 'Flow',
|
||||||
|
action: 'read',
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
roleId: userRole.id,
|
||||||
|
subject: 'Connection',
|
||||||
|
action: 'read',
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await createUser({ roleId: userRole.id });
|
||||||
|
|
||||||
|
const userWithRoleAndPermissions = await user
|
||||||
|
.$query()
|
||||||
|
.withGraphFetched({ role: true, permissions: true });
|
||||||
|
|
||||||
|
expect(userWithRoleAndPermissions.can('read', 'Flow')).toStrictEqual({
|
||||||
|
isCreator: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
userWithRoleAndPermissions.can('read', 'Connection')
|
||||||
|
).toStrictEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return not authorized error when the user is not permitted for the given action and subject', async () => {
|
||||||
|
const userRole = await createRole({ name: 'User' });
|
||||||
|
const user = await createUser({ roleId: userRole.id });
|
||||||
|
|
||||||
|
const userWithRoleAndPermissions = await user
|
||||||
|
.$query()
|
||||||
|
.withGraphFetched({ role: true, permissions: true });
|
||||||
|
|
||||||
|
expect(() => userWithRoleAndPermissions.can('read', 'Flow')).toThrowError(
|
||||||
|
'The user is not authorized!'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user