refactor(role): remove transactions and tidy up logic in model (#2141)

* refactor(role): remove returning this in model methods

* refactor(role): assert altering admin in model before update and delete

* refactor(role): rename overridePermissions with updatePermissions in model

* refactor(role): remove transactions in model

* refactor(role): remove transactions in model

* refactor(role): return with permissions upon update in model

* fix(role): assert admin check on old instance in model

* refactor(role): fetch and use current role in preventAlteringAdmin
This commit is contained in:
Ali BARIN
2024-10-28 14:57:33 +01:00
committed by GitHub
parent 036db63a33
commit 96544df7d5
3 changed files with 48 additions and 212 deletions

View File

@@ -92,21 +92,4 @@ describe('DELETE /api/v1/admin/roles/:roleId', () => {
}, },
}); });
}); });
it('should not delete role and permissions on unsuccessful response', async () => {
const role = await createRole();
const permission = await createPermission({ roleId: role.id });
await createUser({ roleId: role.id });
await request(app)
.delete(`/api/v1/admin/roles/${role.id}`)
.set('Authorization', token)
.expect(422);
const refetchedRole = await role.$query();
const refetchedPermission = await permission.$query();
expect(refetchedRole).toStrictEqual(role);
expect(refetchedPermission).toStrictEqual(permission);
});
}); });

View File

@@ -52,17 +52,19 @@ class Role extends Base {
return await this.query().findOne({ name: 'Admin' }); return await this.query().findOne({ name: 'Admin' });
} }
preventAlteringAdmin() { async preventAlteringAdmin() {
if (this.isAdmin) { const currentRole = await Role.query().findById(this.id);
if (currentRole.isAdmin) {
throw new NotAuthorizedError('The admin role cannot be altered!'); throw new NotAuthorizedError('The admin role cannot be altered!');
} }
} }
async deletePermissions(trx) { async deletePermissions() {
return await this.$relatedQuery('permissions', trx).delete(); return await this.$relatedQuery('permissions').delete();
} }
async createPermissions(permissions, trx) { async createPermissions(permissions) {
if (permissions?.length) { if (permissions?.length) {
const validPermissions = Permission.filter(permissions).map( const validPermissions = Permission.filter(permissions).map(
(permission) => ({ (permission) => ({
@@ -71,50 +73,40 @@ class Role extends Base {
}) })
); );
await Permission.query(trx).insert(validPermissions); await Permission.query().insert(validPermissions);
} }
return this;
} }
async overridePermissions(permissions, trx) { async updatePermissions(permissions) {
await this.deletePermissions(trx); await this.deletePermissions();
await this.createPermissions(permissions, trx); await this.createPermissions(permissions);
return this;
} }
async updateWithPermissions(data) { async updateWithPermissions(data) {
this.preventAlteringAdmin();
const { name, description, permissions } = data; const { name, description, permissions } = data;
return await Role.transaction(async (trx) => { await this.updatePermissions(permissions);
await this.overridePermissions(permissions, trx);
await this.$query(trx).patch({ await this.$query().patchAndFetch({
name, id: this.id,
description, name,
}); description,
// TODO: consider removing returning permissions as they're not utilized
return await this.$query(trx)
.leftJoinRelated({
permissions: true,
})
.withGraphFetched({
permissions: true,
});
}); });
return await this.$query()
.leftJoinRelated({
permissions: true,
})
.withGraphFetched({
permissions: true,
});
} }
async deleteWithPermissions() { async deleteWithPermissions() {
return await Role.transaction(async (trx) => { await this.deletePermissions();
await this.deletePermissions(trx);
return await this.$query(trx).delete(); return await this.$query().delete();
});
} }
async assertNoRoleUserExists() { async assertNoRoleUserExists() {
@@ -164,10 +156,16 @@ class Role extends Base {
await this.assertNoConfigurationUsage(); await this.assertNoConfigurationUsage();
} }
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
await this.preventAlteringAdmin();
}
async $beforeDelete(queryContext) { async $beforeDelete(queryContext) {
await super.$beforeDelete(queryContext); await super.$beforeDelete(queryContext);
this.preventAlteringAdmin(); await this.preventAlteringAdmin();
await this.assertRoleIsNotUsed(); await this.assertRoleIsNotUsed();
} }

View File

@@ -71,47 +71,28 @@ describe('Role model', () => {
}); });
describe('preventAlteringAdmin', () => { describe('preventAlteringAdmin', () => {
it('preventAlteringAdmin should throw an error when altering admin role', () => { it('preventAlteringAdmin should throw an error when altering admin role', async () => {
const role = new Role(); const role = await createRole({ name: 'Admin' });
role.name = 'Admin';
expect(() => role.preventAlteringAdmin()).toThrowError( await expect(() => role.preventAlteringAdmin()).rejects.toThrowError(
'The admin role cannot be altered!' 'The admin role cannot be altered!'
); );
}); });
it('preventAlteringAdmin should not throw an error when altering non-admin roles', () => { it('preventAlteringAdmin should not throw an error when altering non-admin roles', async () => {
const role = new Role(); const role = await createRole({ name: 'User' });
role.name = 'User';
expect(() => role.preventAlteringAdmin()).not.toThrowError(); expect(await role.preventAlteringAdmin()).toBe(undefined);
}); });
}); });
describe('deletePermissions', () => { it("deletePermissions should delete role's permissions", async () => {
it("should delete role's permissions", async () => { const role = await createRole({ name: 'User' });
const role = await createRole({ name: 'User' }); await createPermission({ roleId: role.id });
await createPermission({ roleId: role.id });
await role.deletePermissions(); await role.deletePermissions();
expect(await role.$relatedQuery('permissions')).toStrictEqual([]); expect(await role.$relatedQuery('permissions')).toStrictEqual([]);
});
it('should accept transaction', async () => {
const transaction = vi.fn();
const relatedQuerySpy = vi
.spyOn(Role.prototype, '$relatedQuery')
.mockReturnValue({ delete: () => {} });
const role = await createRole({ name: 'User' });
await role.deletePermissions(transaction);
expect(relatedQuerySpy).toHaveBeenCalledWith('permissions', transaction);
1;
});
}); });
describe('createPermissions', () => { describe('createPermissions', () => {
@@ -144,32 +125,13 @@ describe('Role model', () => {
expect(permissionFilterSpy).toHaveBeenCalledWith(permissions); expect(permissionFilterSpy).toHaveBeenCalledWith(permissions);
}); });
it('should accept transaction', async () => {
const transaction = vi.fn();
const permissionQuerySpy = vi
.spyOn(Permission, 'query')
.mockReturnValue({ insert: () => {} });
const role = await createRole({ name: 'User' });
await role.createPermissions(
[{ action: 'read', subject: 'Flow', conditions: [] }],
transaction
);
expect(permissionQuerySpy).toHaveBeenCalledWith(transaction);
1;
});
}); });
it('overridePermissions should delete existing permissions and create new permissions', async () => { it('updatePermissions should delete existing permissions and create new permissions', async () => {
const permissionsData = [ const permissionsData = [
{ action: 'read', subject: 'Flow', conditions: [] }, { action: 'read', subject: 'Flow', conditions: [] },
]; ];
const transaction = vi.fn();
const deletePermissionsSpy = vi const deletePermissionsSpy = vi
.spyOn(Role.prototype, 'deletePermissions') .spyOn(Role.prototype, 'deletePermissions')
.mockResolvedValueOnce(); .mockResolvedValueOnce();
@@ -179,18 +141,14 @@ describe('Role model', () => {
const role = await createRole({ name: 'User' }); const role = await createRole({ name: 'User' });
await role.overridePermissions(permissionsData, transaction); await role.updatePermissions(permissionsData);
expect(deletePermissionsSpy.mock.invocationCallOrder[0]).toBeLessThan( expect(deletePermissionsSpy.mock.invocationCallOrder[0]).toBeLessThan(
createPermissionsSpy.mock.invocationCallOrder[0] createPermissionsSpy.mock.invocationCallOrder[0]
); );
expect(deletePermissionsSpy).toHaveBeenNthCalledWith(1, transaction); expect(deletePermissionsSpy).toHaveBeenNthCalledWith(1);
expect(createPermissionsSpy).toHaveBeenNthCalledWith( expect(createPermissionsSpy).toHaveBeenNthCalledWith(1, permissionsData);
1,
permissionsData,
transaction
);
}); });
describe('updateWithPermissions', () => { describe('updateWithPermissions', () => {
@@ -224,83 +182,6 @@ describe('Role model', () => {
expect(roleWithPermissions).toMatchObject(newRoleData); expect(roleWithPermissions).toMatchObject(newRoleData);
}); });
it('should throw an error while updating the admin role', async () => {
const role = new Role();
role.name = 'Admin';
await expect(() => role.updateWithPermissions()).rejects.toThrowError(
'The admin role cannot be altered!'
);
});
it('should use transaction', async () => {
const transaction = vi.fn();
const overridePermissionsSpy = vi
.spyOn(Role.prototype, 'overridePermissions')
.mockResolvedValue();
const querySpy = vi.spyOn(Role.prototype, '$query').mockReturnValue({
patch: vi.fn().mockReturnValue(Promise.resolve()),
leftJoinRelated: vi.fn().mockReturnThis(),
withGraphFetched: vi.fn().mockResolvedValue({}),
});
const transactionSpy = vi
.spyOn(Role, 'transaction')
.mockImplementation(async (callback) => {
return await callback(transaction);
});
const role = await createRole({ name: 'User' });
const newRoleData = {
name: 'New user',
description: 'Updated user role',
permissions: [{ action: 'read', subject: 'Flow', conditions: [] }],
};
await role.updateWithPermissions(newRoleData);
expect(transactionSpy).toHaveBeenCalledOnce();
expect(overridePermissionsSpy).toHaveBeenCalledWith(
newRoleData.permissions,
transaction
);
expect(querySpy).toHaveBeenNthCalledWith(2, transaction);
});
it('should revert changes when an error occurs', async () => {
const role = await createRole({ name: 'User' });
await createPermission({
roleId: role.id,
subject: 'Flow',
action: 'read',
conditions: [],
});
const roleWithPermissions = await role
.$query()
.leftJoinRelated({ permissions: true })
.withGraphFetched({ permissions: true });
await expect(() =>
role.updateWithPermissions({
name: false,
description: 123,
})
).rejects.toThrowError(
'name: must be string, description: must be string,null'
);
const refetchedRoleWithPermissions = await role
.$query()
.leftJoinRelated({ permissions: true })
.withGraphFetched({ permissions: true });
expect(roleWithPermissions).toStrictEqual(refetchedRoleWithPermissions);
});
}); });
describe('deleteWithPermissions', () => { describe('deleteWithPermissions', () => {
@@ -323,32 +204,6 @@ describe('Role model', () => {
expect(refetchedRole).toBe(undefined); expect(refetchedRole).toBe(undefined);
expect(rolePermissions).toStrictEqual([]); expect(rolePermissions).toStrictEqual([]);
}); });
it('should use transaction', async () => {
const transaction = vi.fn();
const deletePermissionsSpy = vi
.spyOn(Role.prototype, 'deletePermissions')
.mockResolvedValue();
const querySpy = vi.spyOn(Role.prototype, '$query').mockReturnValue({
delete: vi.fn().mockReturnValue(Promise.resolve()),
});
const transactionSpy = vi
.spyOn(Role, 'transaction')
.mockImplementation(async (callback) => {
return await callback(transaction);
});
const role = await createRole({ name: 'User' });
await role.deleteWithPermissions();
expect(transactionSpy).toHaveBeenCalledOnce();
expect(deletePermissionsSpy).toHaveBeenCalledWith(transaction);
expect(querySpy).toHaveBeenCalledWith(transaction);
});
}); });
describe('assertNoRoleUserExists', () => { describe('assertNoRoleUserExists', () => {