Skip to content

Commit 77b4596

Browse files
authored
Merge pull request #2292 from leggedrobotics/feat/1142-access-group-permissions-tests
test(backend): add integration tests for access group promote/demote (ref #1142)
2 parents 9855c47 + 2a9d002 commit 77b4596

1 file changed

Lines changed: 257 additions & 2 deletions

File tree

backend/tests/auth/access-groups/access-groups.test.ts

Lines changed: 257 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1-
import { AccessGroupEntity, UserEntity } from '@kleinkram/backend-common';
2-
import { AccessGroupRights, AccessGroupType } from '@kleinkram/shared';
1+
import {
2+
AccessGroupEntity,
3+
AccessGroupEventEntity,
4+
GroupMembershipEntity,
5+
UserEntity,
6+
} from '@kleinkram/backend-common';
7+
import {
8+
AccessGroupEventType,
9+
AccessGroupRights,
10+
AccessGroupType,
11+
} from '@kleinkram/shared';
312
import {
413
createAccessGroupUsingPost,
514
createProjectUsingPost,
@@ -11,6 +20,31 @@ import { DEFAULT_URL, generateAndFetchDatabaseUser } from '../utilities';
1120

1221
const DEFAULT_GROUP_UUID = '00000000-0000-0000-0000-000000000000';
1322

23+
async function pollForAuditEvent(
24+
groupUuid: string,
25+
type: AccessGroupEventType,
26+
maxRetries = 10,
27+
intervalMs = 50,
28+
): Promise<AccessGroupEventEntity> {
29+
const eventRepo = database.getRepository(AccessGroupEventEntity);
30+
for (let index = 0; index < maxRetries; index++) {
31+
const events = await eventRepo.find({
32+
where: {
33+
accessGroup: { uuid: groupUuid },
34+
},
35+
order: { createdAt: 'DESC' },
36+
});
37+
const found = events.find((event) => event.type === type);
38+
if (found) {
39+
return found;
40+
}
41+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
42+
}
43+
throw new Error(
44+
`Audit log event of type ${type} was not found for group ${groupUuid} after polling.`,
45+
);
46+
}
47+
1448
/**
1549
* This test suite tests the access control of the application.
1650
*
@@ -1308,4 +1342,225 @@ describe('Verify Access Groups Internal User Access - CRUD and Admin', () => {
13081342
);
13091343
expect(response.status).toBe(403);
13101344
});
1345+
1346+
test('if an editor can promote a group member to editor', async () => {
1347+
const { user: creator } = await generateAndFetchDatabaseUser(
1348+
'internal',
1349+
'user',
1350+
);
1351+
const { user: member } = await generateAndFetchDatabaseUser(
1352+
'internal',
1353+
'user',
1354+
);
1355+
1356+
const groupUuid = await createAccessGroupUsingPost(
1357+
{ name: 'promote_test_group' },
1358+
creator,
1359+
[creator, member],
1360+
);
1361+
1362+
// Member is not an editor initially
1363+
const membershipRepo = database.getRepository(GroupMembershipEntity);
1364+
const initialMembership = await membershipRepo.findOneOrFail({
1365+
where: {
1366+
accessGroup: { uuid: groupUuid },
1367+
user: { uuid: member.uuid },
1368+
},
1369+
});
1370+
expect(initialMembership.canEditGroup).toBe(false);
1371+
1372+
// Creator (editor) promotes member to editor
1373+
const headers = new HeaderCreator(creator);
1374+
headers.addHeader('Content-Type', 'application/json');
1375+
const response = await fetch(
1376+
`${DEFAULT_URL}/access-groups/${groupUuid}/users/${member.uuid}/permissions`,
1377+
{
1378+
method: 'PUT',
1379+
headers: headers.getHeaders(),
1380+
body: JSON.stringify({ canEditGroup: true }),
1381+
},
1382+
);
1383+
expect(response.status).toBeLessThan(300);
1384+
1385+
// Verify updated membership is returned
1386+
const responseJson = (await response.json()) as {
1387+
canEditGroup: boolean;
1388+
};
1389+
expect(responseJson.canEditGroup).toBe(true);
1390+
1391+
// Verify in database
1392+
const updatedMembership = await membershipRepo.findOneOrFail({
1393+
where: {
1394+
accessGroup: { uuid: groupUuid },
1395+
user: { uuid: member.uuid },
1396+
},
1397+
});
1398+
expect(updatedMembership.canEditGroup).toBe(true);
1399+
1400+
// Verify PROMOTE_USER event was logged in audit log
1401+
const promoteEvent = await pollForAuditEvent(
1402+
groupUuid,
1403+
AccessGroupEventType.PROMOTE_USER,
1404+
);
1405+
expect(promoteEvent).toBeDefined();
1406+
expect(promoteEvent.details.userUuid).toBe(member.uuid);
1407+
});
1408+
1409+
test('if an editor can demote a group editor to a normal member', async () => {
1410+
const { user: creator } = await generateAndFetchDatabaseUser(
1411+
'internal',
1412+
'user',
1413+
);
1414+
const { user: member } = await generateAndFetchDatabaseUser(
1415+
'internal',
1416+
'user',
1417+
);
1418+
1419+
const groupUuid = await createAccessGroupUsingPost(
1420+
{ name: 'demote_test_group' },
1421+
creator,
1422+
[creator, member],
1423+
);
1424+
1425+
// Promote member to editor via DB first
1426+
const membershipRepo = database.getRepository(GroupMembershipEntity);
1427+
const membership = await membershipRepo.findOneOrFail({
1428+
where: {
1429+
accessGroup: { uuid: groupUuid },
1430+
user: { uuid: member.uuid },
1431+
},
1432+
});
1433+
membership.canEditGroup = true;
1434+
await membershipRepo.save(membership);
1435+
1436+
// Creator (editor) demotes member to normal user
1437+
const headers = new HeaderCreator(creator);
1438+
headers.addHeader('Content-Type', 'application/json');
1439+
const response = await fetch(
1440+
`${DEFAULT_URL}/access-groups/${groupUuid}/users/${member.uuid}/permissions`,
1441+
{
1442+
method: 'PUT',
1443+
headers: headers.getHeaders(),
1444+
body: JSON.stringify({ canEditGroup: false }),
1445+
},
1446+
);
1447+
expect(response.status).toBeLessThan(300);
1448+
1449+
const responseJson = (await response.json()) as {
1450+
canEditGroup: boolean;
1451+
};
1452+
expect(responseJson.canEditGroup).toBe(false);
1453+
1454+
// Verify in database
1455+
const updatedMembership = await membershipRepo.findOneOrFail({
1456+
where: {
1457+
accessGroup: { uuid: groupUuid },
1458+
user: { uuid: member.uuid },
1459+
},
1460+
});
1461+
expect(updatedMembership.canEditGroup).toBe(false);
1462+
1463+
// Verify DEMOTE_USER event was logged in audit log
1464+
const demoteEvent = await pollForAuditEvent(
1465+
groupUuid,
1466+
AccessGroupEventType.DEMOTE_USER,
1467+
);
1468+
expect(demoteEvent).toBeDefined();
1469+
expect(demoteEvent.details.userUuid).toBe(member.uuid);
1470+
});
1471+
1472+
test('if demoting the last editor returns a conflict error', async () => {
1473+
const { user: creator } = await generateAndFetchDatabaseUser(
1474+
'internal',
1475+
'user',
1476+
);
1477+
1478+
const groupUuid = await createAccessGroupUsingPost(
1479+
{ name: 'demote_last_editor_test_group' },
1480+
creator,
1481+
[creator],
1482+
);
1483+
1484+
// Attempt to demote the creator (the only editor)
1485+
const headers = new HeaderCreator(creator);
1486+
headers.addHeader('Content-Type', 'application/json');
1487+
const response = await fetch(
1488+
`${DEFAULT_URL}/access-groups/${groupUuid}/users/${creator.uuid}/permissions`,
1489+
{
1490+
method: 'PUT',
1491+
headers: headers.getHeaders(),
1492+
body: JSON.stringify({ canEditGroup: false }),
1493+
},
1494+
);
1495+
expect(response.status).toBe(409);
1496+
});
1497+
1498+
test('if a non-editor cannot promote or demote a user', async () => {
1499+
const { user: creator } = await generateAndFetchDatabaseUser(
1500+
'internal',
1501+
'user',
1502+
);
1503+
const { user: member } = await generateAndFetchDatabaseUser(
1504+
'internal',
1505+
'user',
1506+
);
1507+
const { user: unrelated } = await generateAndFetchDatabaseUser(
1508+
'internal',
1509+
'user',
1510+
);
1511+
1512+
const groupUuid = await createAccessGroupUsingPost(
1513+
{ name: 'non_editor_permission_test_group' },
1514+
creator,
1515+
[creator, member],
1516+
);
1517+
1518+
// Member (non-editor) tries to promote themselves
1519+
const memberHeaders = new HeaderCreator(member);
1520+
memberHeaders.addHeader('Content-Type', 'application/json');
1521+
const memberResponse = await fetch(
1522+
`${DEFAULT_URL}/access-groups/${groupUuid}/users/${member.uuid}/permissions`,
1523+
{
1524+
method: 'PUT',
1525+
headers: memberHeaders.getHeaders(),
1526+
body: JSON.stringify({ canEditGroup: true }),
1527+
},
1528+
);
1529+
expect(memberResponse.status).toBe(403);
1530+
1531+
// Member (non-editor) tries to demote the creator (editor)
1532+
const memberDemoteResponse = await fetch(
1533+
`${DEFAULT_URL}/access-groups/${groupUuid}/users/${creator.uuid}/permissions`,
1534+
{
1535+
method: 'PUT',
1536+
headers: memberHeaders.getHeaders(),
1537+
body: JSON.stringify({ canEditGroup: false }),
1538+
},
1539+
);
1540+
expect(memberDemoteResponse.status).toBe(403);
1541+
1542+
// Unrelated user tries to promote member
1543+
const unrelatedHeaders = new HeaderCreator(unrelated);
1544+
unrelatedHeaders.addHeader('Content-Type', 'application/json');
1545+
const unrelatedResponse = await fetch(
1546+
`${DEFAULT_URL}/access-groups/${groupUuid}/users/${member.uuid}/permissions`,
1547+
{
1548+
method: 'PUT',
1549+
headers: unrelatedHeaders.getHeaders(),
1550+
body: JSON.stringify({ canEditGroup: true }),
1551+
},
1552+
);
1553+
expect(unrelatedResponse.status).toBe(403);
1554+
1555+
// Unrelated user tries to demote the creator (editor)
1556+
const unrelatedDemoteResponse = await fetch(
1557+
`${DEFAULT_URL}/access-groups/${groupUuid}/users/${creator.uuid}/permissions`,
1558+
{
1559+
method: 'PUT',
1560+
headers: unrelatedHeaders.getHeaders(),
1561+
body: JSON.stringify({ canEditGroup: false }),
1562+
},
1563+
);
1564+
expect(unrelatedDemoteResponse.status).toBe(403);
1565+
});
13111566
});

0 commit comments

Comments
 (0)