Merge pull request #1731 from automatisch/AUT-841

refactor: rewrite useRole with RQ
This commit is contained in:
Ali BARIN
2024-03-15 13:19:41 +01:00
committed by GitHub
8 changed files with 35 additions and 227 deletions

View File

@@ -1,17 +0,0 @@
import Role from '../../models/role.js';
const getRole = async (_parent, params, context) => {
context.currentUser.can('read', 'Role');
return await Role.query()
.leftJoinRelated({
permissions: true,
})
.withGraphFetched({
permissions: true,
})
.findById(params.id)
.throwIfNotFound();
};
export default getRole;

View File

@@ -1,164 +0,0 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../app';
import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id';
import Crypto from 'crypto';
import { createRole } from '../../../test/factories/role';
import { createPermission } from '../../../test/factories/permission';
import { createUser } from '../../../test/factories/user';
import * as license from '../../helpers/license.ee';
describe('graphQL getRole query', () => {
let validRole,
invalidRoleId,
queryWithValidRole,
queryWithInvalidRole,
userWithPermissions,
userWithoutPermissions,
tokenWithPermissions,
tokenWithoutPermissions,
permissionOne,
permissionTwo;
beforeEach(async () => {
validRole = await createRole();
invalidRoleId = Crypto.randomUUID();
queryWithValidRole = `
query {
getRole(id: "${validRole.id}") {
id
name
key
description
isAdmin
permissions {
id
action
subject
conditions
}
}
}
`;
queryWithInvalidRole = `
query {
getRole(id: "${invalidRoleId}") {
id
name
}
}
`;
permissionOne = await createPermission({
action: 'read',
subject: 'Role',
roleId: validRole.id,
});
permissionTwo = await createPermission({
action: 'read',
subject: 'User',
roleId: validRole.id,
});
userWithPermissions = await createUser({
roleId: validRole.id,
});
userWithoutPermissions = await createUser();
tokenWithPermissions = createAuthTokenByUserId(userWithPermissions.id);
tokenWithoutPermissions = createAuthTokenByUserId(
userWithoutPermissions.id
);
});
describe('and with valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
});
describe('and without permissions', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithoutPermissions)
.send({ query: queryWithValidRole })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
describe('and correct permissions', () => {
it('should return role data for a valid role id', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithPermissions)
.send({ query: queryWithValidRole })
.expect(200);
const expectedResponsePayload = {
data: {
getRole: {
description: validRole.description,
id: validRole.id,
isAdmin: validRole.key === 'admin',
key: validRole.key,
name: validRole.name,
permissions: [
{
action: permissionOne.action,
conditions: permissionOne.conditions,
id: permissionOne.id,
subject: permissionOne.subject,
},
{
action: permissionTwo.action,
conditions: permissionTwo.conditions,
id: permissionTwo.id,
subject: permissionTwo.subject,
},
],
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
it('should return not found for invalid role id', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithPermissions)
.send({ query: queryWithInvalidRole })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('NotFoundError');
});
});
});
describe('and without valid license', () => {
beforeEach(async () => {
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(false);
});
describe('and correct permissions', () => {
it('should throw not authorized error', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', tokenWithPermissions)
.send({ query: queryWithInvalidRole })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
});
});

View File

@@ -12,7 +12,6 @@ import getFlows from './queries/get-flows.js';
import getInvoices from './queries/get-invoices.ee.js'; import getInvoices from './queries/get-invoices.ee.js';
import getNotifications from './queries/get-notifications.js'; import getNotifications from './queries/get-notifications.js';
import getPermissionCatalog from './queries/get-permission-catalog.ee.js'; import getPermissionCatalog from './queries/get-permission-catalog.ee.js';
import getRole from './queries/get-role.ee.js';
import getSamlAuthProviderRoleMappings from './queries/get-saml-auth-provider-role-mappings.ee.js'; import getSamlAuthProviderRoleMappings from './queries/get-saml-auth-provider-role-mappings.ee.js';
import getSamlAuthProvider from './queries/get-saml-auth-provider.ee.js'; import getSamlAuthProvider from './queries/get-saml-auth-provider.ee.js';
import getStepWithTestExecutions from './queries/get-step-with-test-executions.js'; import getStepWithTestExecutions from './queries/get-step-with-test-executions.js';
@@ -38,7 +37,6 @@ const queryResolvers = {
getInvoices, getInvoices,
getNotifications, getNotifications,
getPermissionCatalog, getPermissionCatalog,
getRole,
getSamlAuthProvider, getSamlAuthProvider,
getSamlAuthProviderRoleMappings, getSamlAuthProviderRoleMappings,
getStepWithTestExecutions, getStepWithTestExecutions,

View File

@@ -28,7 +28,6 @@ type Query {
getConfig(keys: [String]): JSONObject getConfig(keys: [String]): JSONObject
getInvoices: [Invoice] getInvoices: [Invoice]
getPermissionCatalog: PermissionCatalog getPermissionCatalog: PermissionCatalog
getRole(id: String!): Role
getNotifications: [Notification] getNotifications: [Notification]
getSamlAuthProvider: SamlAuthProvider getSamlAuthProvider: SamlAuthProvider
getSamlAuthProviderRoleMappings(id: String!): [SamlAuthProvidersRoleMapping] getSamlAuthProviderRoleMappings(id: String!): [SamlAuthProvidersRoleMapping]

View File

@@ -1,18 +0,0 @@
import { gql } from '@apollo/client';
export const GET_ROLE = gql`
query GetRole($id: String!) {
getRole(id: $id) {
id
key
name
description
isAdmin
permissions {
id
action
subject
conditions
}
}
}
`;

View File

@@ -1,20 +1,21 @@
export function getRoleWithComputedPermissions(role) { export function getRoleWithComputedPermissions(role) {
if (!role) return {}; if (!role) return {};
const computedPermissions = role.permissions.reduce( const computedPermissions = role.permissions?.reduce(
(computedPermissions, permission) => ({ (computedPermissions, permission) => ({
...computedPermissions, ...computedPermissions,
[permission.subject]: { [permission.subject]: {
...(computedPermissions[permission.subject] || {}), ...(computedPermissions[permission.subject] || {}),
[permission.action]: { [permission.action]: {
conditions: Object.fromEntries( conditions: Object.fromEntries(
permission.conditions.map((condition) => [condition, true]) permission.conditions.map((condition) => [condition, true]),
), ),
value: true, value: true,
}, },
}, },
}), }),
{} {},
); );
return { return {
...role, ...role,
computedPermissions, computedPermissions,
@@ -22,6 +23,7 @@ export function getRoleWithComputedPermissions(role) {
} }
export function getPermissions(computedPermissions) { export function getPermissions(computedPermissions) {
if (!computedPermissions) return []; if (!computedPermissions) return [];
return Object.entries(computedPermissions).reduce( return Object.entries(computedPermissions).reduce(
(permissions, computedPermissionEntry) => { (permissions, computedPermissionEntry) => {
const [subject, actionsWithConditions] = computedPermissionEntry; const [subject, actionsWithConditions] = computedPermissionEntry;
@@ -40,6 +42,6 @@ export function getPermissions(computedPermissions) {
} }
return permissions; return permissions;
}, },
[] [],
); );
} }

View File

@@ -1,19 +1,19 @@
import * as React from 'react'; import { useQuery } from '@tanstack/react-query';
import { useLazyQuery } from '@apollo/client';
import { GET_ROLE } from 'graphql/queries/get-role.ee'; import api from 'helpers/api';
export default function useRole(roleId) {
const [getRole, { data, loading }] = useLazyQuery(GET_ROLE); export default function useRole({ roleId }) {
React.useEffect(() => { const query = useQuery({
if (roleId) { queryKey: ['role', roleId],
getRole({ queryFn: async ({ signal }) => {
variables: { const { data } = await api.get(`/v1/admin/roles/${roleId}`, {
id: roleId, signal,
},
}); });
}
}, [roleId]); return data;
return { },
role: data?.getRole, enabled: !!roleId,
loading, });
};
return query;
} }

View File

@@ -6,6 +6,7 @@ import Stack from '@mui/material/Stack';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import * as React from 'react'; import * as React from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import Container from 'components/Container'; import Container from 'components/Container';
import Form from 'components/Form'; import Form from 'components/Form';
import PageTitle from 'components/PageTitle'; import PageTitle from 'components/PageTitle';
@@ -19,13 +20,16 @@ import {
} from 'helpers/computePermissions.ee'; } from 'helpers/computePermissions.ee';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useRole from 'hooks/useRole.ee'; import useRole from 'hooks/useRole.ee';
export default function EditRole() { export default function EditRole() {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const [updateRole, { loading }] = useMutation(UPDATE_ROLE); const [updateRole, { loading }] = useMutation(UPDATE_ROLE);
const navigate = useNavigate(); const navigate = useNavigate();
const { roleId } = useParams(); const { roleId } = useParams();
const { role, loading: roleLoading } = useRole(roleId); const { data, loading: isRoleLoading } = useRole({ roleId });
const role = data?.data;
const enqueueSnackbar = useEnqueueSnackbar(); const enqueueSnackbar = useEnqueueSnackbar();
const handleRoleUpdate = async (roleData) => { const handleRoleUpdate = async (roleData) => {
try { try {
const newPermissions = getPermissions(roleData.computedPermissions); const newPermissions = getPermissions(roleData.computedPermissions);
@@ -39,18 +43,22 @@ export default function EditRole() {
}, },
}, },
}); });
enqueueSnackbar(formatMessage('editRole.successfullyUpdated'), { enqueueSnackbar(formatMessage('editRole.successfullyUpdated'), {
variant: 'success', variant: 'success',
SnackbarProps: { SnackbarProps: {
'data-test': 'snackbar-edit-role-success', 'data-test': 'snackbar-edit-role-success',
}, },
}); });
navigate(URLS.ROLES); navigate(URLS.ROLES);
} catch (error) { } catch (error) {
throw new Error('Failed while updating!'); throw new Error('Failed while updating!');
} }
}; };
const roleWithComputedPermissions = getRoleWithComputedPermissions(role); const roleWithComputedPermissions = getRoleWithComputedPermissions(role);
return ( return (
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}> <Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
<Grid container item xs={12} sm={10} md={9}> <Grid container item xs={12} sm={10} md={9}>
@@ -66,13 +74,13 @@ export default function EditRole() {
onSubmit={handleRoleUpdate} onSubmit={handleRoleUpdate}
> >
<Stack direction="column" gap={2}> <Stack direction="column" gap={2}>
{roleLoading && ( {isRoleLoading && (
<> <>
<Skeleton variant="rounded" height={55} /> <Skeleton variant="rounded" height={55} />
<Skeleton variant="rounded" height={55} /> <Skeleton variant="rounded" height={55} />
</> </>
)} )}
{!roleLoading && role && ( {!isRoleLoading && role && (
<> <>
<TextField <TextField
disabled={role.isAdmin} disabled={role.isAdmin}
@@ -104,7 +112,7 @@ export default function EditRole() {
color="primary" color="primary"
sx={{ boxShadow: 2 }} sx={{ boxShadow: 2 }}
loading={loading} loading={loading}
disabled={role?.isAdmin || roleLoading} disabled={role?.isAdmin || isRoleLoading}
data-test="update-button" data-test="update-button"
> >
{formatMessage('editRole.submit')} {formatMessage('editRole.submit')}