Skip to content

Commit

Permalink
Add DELETE endpoint for UserChangemakerPermission
Browse files Browse the repository at this point in the history
This endpoint makes it possible for a user with appropriate permissions
to remove a changemaker permission for a given user.

Issue #1250 Support associations between users and organizational entities
  • Loading branch information
slifty committed Nov 13, 2024
1 parent 56e72e8 commit 2a665f2
Show file tree
Hide file tree
Showing 9 changed files with 355 additions and 1 deletion.
164 changes: 164 additions & 0 deletions src/__tests__/userChangemakerPermissions.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { app } from '../app';
import {
createChangemaker,
createOrUpdateUserChangemakerPermission,
loadUserChangemakerPermission,
} from '../database';
import { expectTimestamp, loadTestUser } from '../test/utils';
import {
mockJwt as authHeader,
mockJwtWithAdminRole as authHeaderWithAdminRole,
} from '../test/mockJwt';
import { keycloakUserIdToString, Permission } from '../types';
import { NotFoundError } from '../errors';

describe('/users/changemakers/:changemakerId/permissions/:permission', () => {
describe('PUT /', () => {
Expand Down Expand Up @@ -123,4 +125,166 @@ describe('/users/changemakers/:changemakerId/permissions/:permission', () => {
});
});
});

describe('DELETE /', () => {
it('returns 401 if the request lacks authentication', async () => {
const user = await loadTestUser();
const changemaker = await createChangemaker({
taxId: '11-1111111',
name: 'Example Inc.',
});
await request(app)
.delete(
`/users/${keycloakUserIdToString(user.keycloakUserId)}/changemakers/${changemaker.id}/permissions/${Permission.MANAGE}`,
)
.send()
.expect(401);
});

it('returns 401 if the authenticated user lacks permission', async () => {
const user = await loadTestUser();
const changemaker = await createChangemaker({
taxId: '11-1111111',
name: 'Example Inc.',
});
await request(app)
.delete(
`/users/${keycloakUserIdToString(user.keycloakUserId)}/changemakers/${changemaker.id}/permissions/${Permission.MANAGE}`,
)
.set(authHeader)
.send()
.expect(401);
});

it('returns 400 if the userId is not a valid keycloak user ID', async () => {
await request(app)
.delete(`/users/notaguid/changemakers/1/permissions/${Permission.MANAGE}`)
.set(authHeaderWithAdminRole)
.send()
.expect(400);
});

it('returns 400 if the changemaker ID is not a valid ID', async () => {
const user = await loadTestUser();
await request(app)
.delete(
`/users/${user.keycloakUserId}/changemakers/notanId/permissions/${Permission.MANAGE}`,

Check failure on line 171 in src/__tests__/userChangemakerPermissions.int.test.ts

View workflow job for this annotation

GitHub Actions / lint

'user.keycloakUserId' will use Object's default stringification format ('[object Object]') when stringified

Check failure on line 171 in src/__tests__/userChangemakerPermissions.int.test.ts

View workflow job for this annotation

GitHub Actions / lint

Invalid type "KeycloakUserId" of template literal expression
)
.set(authHeaderWithAdminRole)
.send()
.expect(400);
});

it('returns 400 if the permission is not a valid permission', async () => {
const user = await loadTestUser();
await request(app)
.delete(
`/users/${user.keycloakUserId}/changemakers/1/permissions/notAPermission`,

Check failure on line 182 in src/__tests__/userChangemakerPermissions.int.test.ts

View workflow job for this annotation

GitHub Actions / lint

'user.keycloakUserId' will use Object's default stringification format ('[object Object]') when stringified

Check failure on line 182 in src/__tests__/userChangemakerPermissions.int.test.ts

View workflow job for this annotation

GitHub Actions / lint

Invalid type "KeycloakUserId" of template literal expression
)
.set(authHeaderWithAdminRole)
.send()
.expect(400);
});

it('returns 404 if the permission does not exist', async () => {
const user = await loadTestUser();
const changemaker = await createChangemaker({
taxId: '11-1111111',
name: 'Example Inc.',
});
await request(app)
.delete(
`/users/${keycloakUserIdToString(user.keycloakUserId)}/changemakers/${changemaker.id}/permissions/${Permission.MANAGE}`,
)
.set(authHeaderWithAdminRole)
.send()
.expect(404);
});

it('deletes the user changemaker permission when the user has administrative credentials', async () => {
const user = await loadTestUser();
const changemaker = await createChangemaker({
taxId: '11-1111111',
name: 'Example Inc.',
});
await createOrUpdateUserChangemakerPermission({
userKeycloakUserId: user.keycloakUserId,
changemakerId: changemaker.id,
permission: Permission.EDIT,
createdBy: user.keycloakUserId,
});
const permission = await loadUserChangemakerPermission(
user.keycloakUserId,
changemaker.id,
Permission.EDIT,
);
expect(permission).toEqual({
changemakerId: changemaker.id,
createdAt: expectTimestamp,
createdBy: user.keycloakUserId,
permission: Permission.EDIT,
userKeycloakUserId: user.keycloakUserId,
});
await request(app)
.delete(
`/users/${keycloakUserIdToString(user.keycloakUserId)}/changemakers/${changemaker.id}/permissions/${Permission.EDIT}`,
)
.set(authHeaderWithAdminRole)
.send()
.expect(204);
await expect(
loadUserChangemakerPermission(
user.keycloakUserId,
changemaker.id,
Permission.EDIT,
),
).rejects.toThrow(NotFoundError);
});

it('deletes the user changemaker permission when the user has permission to manage the changemaker', async () => {
const user = await loadTestUser();
const changemaker = await createChangemaker({
taxId: '11-1111111',
name: 'Example Inc.',
});
await createOrUpdateUserChangemakerPermission({
userKeycloakUserId: user.keycloakUserId,
changemakerId: changemaker.id,
permission: Permission.MANAGE,
createdBy: user.keycloakUserId,
});
await createOrUpdateUserChangemakerPermission({
userKeycloakUserId: user.keycloakUserId,
changemakerId: changemaker.id,
permission: Permission.EDIT,
createdBy: user.keycloakUserId,
});
const permission = await loadUserChangemakerPermission(
user.keycloakUserId,
changemaker.id,
Permission.EDIT,
);
expect(permission).toEqual({
changemakerId: changemaker.id,
createdAt: expectTimestamp,
createdBy: user.keycloakUserId,
permission: Permission.EDIT,
userKeycloakUserId: user.keycloakUserId,
});
await request(app)
.delete(
`/users/${keycloakUserIdToString(user.keycloakUserId)}/changemakers/${changemaker.id}/permissions/${Permission.EDIT}`,
)
.set(authHeader)
.send()
.expect(204);
await expect(
loadUserChangemakerPermission(
user.keycloakUserId,
changemaker.id,
Permission.EDIT,
),
).rejects.toThrow(NotFoundError);
});
});
});
2 changes: 2 additions & 0 deletions src/database/operations/userChangemakerPermissions/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './createOrUpdateUserChangemakerPermission';
export * from './loadUserChangemakerPermission';
export * from './removeUserChangemakerPermission';
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { db } from '../../db';
import { NotFoundError } from '../../../errors';
import {
keycloakUserIdToString,
type Id,
type JsonResultSet,
type KeycloakUserId,
type Permission,
type UserChangemakerPermission,
} from '../../../types';

export const loadUserChangemakerPermission = async (
userKeycloakUserId: KeycloakUserId,
changemakerId: Id,
permission: Permission,
): Promise<UserChangemakerPermission> => {
const result = await db.sql<JsonResultSet<UserChangemakerPermission>>(
'userChangemakerPermissions.selectByPrimaryKey',
{
userKeycloakUserId,
changemakerId,
permission,
},
);
const object = result.rows[0]?.object;
if (object === undefined) {
throw new NotFoundError(`Entity not found`, {
entityType: 'UserchangemakerPermission',
entityPrimaryKey: {
userKeycloakUserId: keycloakUserIdToString(userKeycloakUserId),
changemakerId,
permission,
},
});
}
return object;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { NotFoundError } from '../../../errors';
import { keycloakUserIdToString } from '../../../types';
import { db } from '../../db';
import type { KeycloakUserId, Permission } from '../../../types';

const removeUserChangemakerPermission = async (
userKeycloakUserId: KeycloakUserId,
changemakerId: string,
permission: Permission,
): Promise<void> => {
const result = await db.sql('userChangemakerPermissions.deleteOne', {
userKeycloakUserId,
permission,
changemakerId,
});

if (result.row_count === 0) {
throw new NotFoundError(
'The item did not exist and could not be deleted.',
{
entityType: 'UserChangemakerPermission',
entityPrimaryKey: {
userKeycloakUserId: keycloakUserIdToString(userKeycloakUserId),
permission,
changemakerId,
},
},
);
}
};

export { removeUserChangemakerPermission };
4 changes: 4 additions & 0 deletions src/database/queries/userChangemakerPermissions/deleteOne.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DELETE FROM user_changemaker_permissions
WHERE user_keycloak_user_id = :userKeycloakUserId
AND permission = :permission::permission_t
AND changemaker_id = :changemakerId;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
SELECT user_changemaker_permission_to_json(user_changemaker_permissions.*) AS "object"
FROM user_changemaker_permissions
WHERE user_keycloak_user_id = :userKeycloakUserId
AND changemaker_id = :changemakerId
AND permission = :permission
61 changes: 60 additions & 1 deletion src/handlers/userChangemakerPermissionsHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { createOrUpdateUserChangemakerPermission } from '../database';
import {
createOrUpdateUserChangemakerPermission,
removeUserChangemakerPermission,
} from '../database';
import {
isAuthContext,
isId,
Expand All @@ -14,6 +17,61 @@ import {
} from '../errors';
import type { Request, Response, NextFunction } from 'express';

const deleteUserChangemakerPermission = (
req: Request,
res: Response,
next: NextFunction,
): void => {
if (!isAuthContext(req)) {
next(new FailedMiddlewareError('Unexpected lack of auth context.'));
return;

Check warning on line 27 in src/handlers/userChangemakerPermissionsHandlers.ts

View check run for this annotation

Codecov / codecov/patch

src/handlers/userChangemakerPermissionsHandlers.ts#L26-L27

Added lines #L26 - L27 were not covered by tests
}

const { userKeycloakUserId, changemakerId, permission } = req.params;
if (!isKeycloakUserId(userKeycloakUserId)) {
next(
new InputValidationError(
'Invalid userKeycloakUserId parameter.',
isKeycloakUserId.errors ?? [],
),
);
return;
}
if (!isId(changemakerId)) {
next(
new InputValidationError(
'Invalid changemakerId parameter.',
isId.errors ?? [],
),
);
return;
}
if (!isPermission(permission)) {
next(
new InputValidationError(
'Invalid permission parameter.',
isPermission.errors ?? [],
),
);
return;
}

(async () => {
await removeUserChangemakerPermission(
userKeycloakUserId,
changemakerId,
permission,
);
res.status(204).contentType('application/json').send();
})().catch((error: unknown) => {
if (isTinyPgErrorWithQueryContext(error)) {
next(new DatabaseError('Error deleting item.', error));
return;

Check warning on line 69 in src/handlers/userChangemakerPermissionsHandlers.ts

View check run for this annotation

Codecov / codecov/patch

src/handlers/userChangemakerPermissionsHandlers.ts#L68-L69

Added lines #L68 - L69 were not covered by tests
}
next(error);
});
};

const putUserChangemakerPermission = (
req: Request,
res: Response,
Expand Down Expand Up @@ -86,6 +144,7 @@ const putUserChangemakerPermission = (
};

const userChangemakerPermissionsHandlers = {
deleteUserChangemakerPermission,
putUserChangemakerPermission,
};

Expand Down
Loading

0 comments on commit 2a665f2

Please sign in to comment.