ToolJet/server/test/modules/group-permissions/e2e/group-admin.spec.ts
Shantanu Mane 89448cf492
Feature: Group Admin (#16210)
* fix: add FK constraints, unique constraint, and down() to group_admins migration

* fix: add explicit FK constraint names to group_admins migration

* feat: add GroupAdmin TypeORM entity

* fix: add Organization relation to GroupAdmin entity

* feat: add group-admin FEATURE_KEY entries and feature configs

* feat: add CE no-op stub for GroupAdminService

* feat: register GroupAdminService/Controller in module; add group-admin controller (CE stub + EE impl)

* feat: pass user to getAllGroup(); update interface and CE service signature

* test: add e2e tests for group-admin assign, revoke, scoped access, and auto-revocation

* fix: correct imports and tsconfig for group-admin e2e test

- fix entity imports: use @entities alias instead of src/entities
- fix module imports: use @modules alias instead of src/modules
- remove unused findEntityOrFail import from test-helper
- add jest to tsconfig types array for test type definitions
- add test-helper path mapping to tsconfig for proper resolution

* fix: register GroupAdmin entity in TypeOrmModule.forFeature for DI

* fix: grant workspace admins all group-admin feature keys in ability factory

* feat: add group-admin service methods

Add getGroupAdmins, getAddableAdmins, assignGroupAdmin, revokeGroupAdmin methods to groupPermissionV2Service following existing fetch pattern.

* feat: allow group-admin builders to access Groups link in workspace settings

Add is_group_admin and canManageGroups to conditionObj (both useState initializer
and subscribe handler). Change Groups link condition from ['admin'] to
['canManageGroups'] to allow admin OR group-admin builders.

* feat: add Group Admins tab to group permission resources

* fix: handle missing default groups for group-admin builders; hide admin-only controls

* feat: make permissions and granular access tabs read-only for group-admin builders; hide role-change in users tab

* feat: implement GroupAdminOrAdminRoute for session validation; enhance group admin management features

* feat: add GET_USER_ADMIN_GROUPS feature and update related permissions; enhance user group management

* feat: enhance group admin functionality; add builder role and improve admin assignment tests

* Fix: Add compiler options to tsconfig.build.json

* refactor: revert configs for test suite

* feat: enhance group admin functionality with feature access checks and user removal permissions

* feat: add option to duplicate group admins in duplicate group

* feat: enhance group permissions management

- Updated FeatureAbilityFactory to include additional checks for builder permissions and group-specific access.
- Modified GroupPermissionsController to apply FeatureAbilityGuard for various endpoints, ensuring proper permission checks.
- Enhanced GranularPermissionsController with appropriate guards for better access control.
- Refactored GroupExistenceGuard to improve group validation logic and error handling.
- Updated GroupPermissionsModule to streamline service and utility registrations.
- Added unit tests for FeatureAbilityFactory and GroupExistenceGuard to ensure robust permission handling.

* feat: implement afterUpdateUserRole method in RolesService

* feat: refactor GroupPermissionsUtilService usage in GroupPermissionsModule

* feat: enhance group permissions copyright and icons, minor bug fixes:

* Bug fixes

Co-authored-by: Copilot <copilot@github.com>

* bugz

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Rudhra Deep Biswas <rudra21ultra@gmail.com>
Co-authored-by: Copilot <copilot@github.com>
2026-04-30 19:29:03 +05:30

629 lines
26 KiB
TypeScript

import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import {
initTestApp,
closeTestApp,
createAdmin,
createBuilder,
createEndUser,
findEntity,
findEntities,
findEntityOrFail,
saveEntity,
createGroupPermission,
createUserGroupPermissions,
} from 'test-helper';
import { GroupPermissions } from '@entities/group_permissions.entity';
import { GroupAdmin } from '@entities/group_admin.entity';
import { GroupUsers } from 'src/entities/group_users.entity';
import { GROUP_PERMISSIONS_TYPE } from '@modules/group-permissions/constants';
/**
* Group Admin API | e2e tests (EE, enterprise plan).
*
* Endpoints under test:
* POST /api/v2/group-permissions/:id/admins | assign group admin
* GET /api/v2/group-permissions/:id/admins | list group admins
* DELETE /api/v2/group-permissions/:id/admins/:adminId | revoke group admin
* GET /api/v2/group-permissions | scoped list (group-admin builder)
*
* Revocation hooks:
* POST /api/organization-users/:orgUserId/archive | archive triggers revocation
* PUT /api/organization-users/:orgUserId | downgrade to end-user triggers revocation
*/
const email = (label: string) => `${label}-${Date.now().toString(36)}@tooljet.io`;
/** @group platform */
describe('GroupAdminController', () => {
let nestApp: INestApplication;
beforeAll(async () => {
({ app: nestApp } = await initTestApp({ edition: 'ee', plan: 'enterprise' }));
});
afterEach(() => {
jest.resetAllMocks();
});
afterAll(async () => {
await closeTestApp(nestApp);
}, 60_000);
// ---------------------------------------------------------------------------
// Shared helpers
// ---------------------------------------------------------------------------
/** Create a custom group directly in DB, returning the GroupPermissions row. */
async function createCustomGroup(orgId: string, name: string): Promise<GroupPermissions> {
return createGroupPermission(nestApp, {
organizationId: orgId,
name,
type: GROUP_PERMISSIONS_TYPE.CUSTOM_GROUP,
} as any);
}
/** POST /api/v2/group-permissions/:groupId/admins */
async function assignAdminViaApi(cookie: string[], workspaceId: string, groupId: string, userId: string) {
return request(nestApp.getHttpServer())
.post(`/api/v2/group-permissions/${groupId}/admins`)
.set('tj-workspace-id', workspaceId)
.set('Cookie', cookie)
.send({ userId });
}
/** DELETE /api/v2/group-permissions/:groupId/admins/:adminId */
async function revokeAdminViaApi(cookie: string[], workspaceId: string, groupId: string, groupAdminId: string) {
return request(nestApp.getHttpServer())
.delete(`/api/v2/group-permissions/${groupId}/admins/${groupAdminId}`)
.set('tj-workspace-id', workspaceId)
.set('Cookie', cookie);
}
// ---------------------------------------------------------------------------
// EE enterprise tests
// ---------------------------------------------------------------------------
describe('EE (plan: enterprise)', () => {
// -------------------------------------------------------------------------
// POST /api/v2/group-permissions/:id/admins | Assign group admin
// -------------------------------------------------------------------------
describe('POST /api/v2/group-permissions/:id/admins | Assign group admin', () => {
it('workspace admin can assign a builder as group admin → 201 + row in group_admins', async () => {
const admin = await createAdmin(nestApp, email('admin-assign'));
const builder = await createBuilder(nestApp, email('builder-target'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers');
const response = await assignAdminViaApi(admin.cookie, admin.workspace.id, group.id, builder.user.id);
expect(response.statusCode).toBe(201);
const row = await findEntity(GroupAdmin, {
userId: builder.user.id,
groupId: group.id,
organizationId: admin.workspace.id,
});
expect(row).not.toBeNull();
});
it('returns 400 when assigning an end-user as group admin', async () => {
const admin = await createAdmin(nestApp, email('admin-endusercheck'));
const endUser = await createEndUser(nestApp, email('enduser-target'), {
workspace: admin.workspace,
});
// createEndUser puts the user in the 'end-user' default permission group.
// isAdminOrBuilder() checks for membership in 'admin'/'builder' default groups,
// so this user is rejected without any extra DB manipulation.
const group = await createCustomGroup(admin.workspace.id, 'avengers');
const response = await assignAdminViaApi(admin.cookie, admin.workspace.id, group.id, endUser.user.id);
expect(response.statusCode).toBe(400);
});
it('is idempotent — assigning the same user twice returns existing row (no duplicate)', async () => {
const admin = await createAdmin(nestApp, email('admin-idempotent'));
const builder = await createBuilder(nestApp, email('builder-idempotent'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers');
const first = await assignAdminViaApi(admin.cookie, admin.workspace.id, group.id, builder.user.id);
expect(first.statusCode).toBe(201);
const second = await assignAdminViaApi(admin.cookie, admin.workspace.id, group.id, builder.user.id);
expect(second.statusCode).toBe(201);
expect(second.body.id).toBe(first.body.id);
const rows = await findEntities(GroupAdmin, { where: { userId: builder.user.id, groupId: group.id } });
expect(rows).toHaveLength(1);
});
it('returns 403 when a non-admin builder tries to assign a group admin', async () => {
const admin = await createAdmin(nestApp, email('admin-forassign'));
const builder = await createBuilder(nestApp, email('builder-actor'), {
workspace: admin.workspace,
});
const anotherBuilder = await createBuilder(nestApp, email('builder-target2'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers');
// builder (no group-admin) tries to assign
const response = await assignAdminViaApi(builder.cookie, admin.workspace.id, group.id, anotherBuilder.user.id);
expect(response.statusCode).toBe(403);
});
});
// -------------------------------------------------------------------------
// Scoped access: group-admin builder on GET /group-permissions
// -------------------------------------------------------------------------
describe('GET /api/v2/group-permissions | Scoped list for group-admin builder', () => {
it('group-admin builder can GET group list (sees administered groups)', async () => {
const admin = await createAdmin(nestApp, email('admin-scopedlist'));
const builder = await createBuilder(nestApp, email('builder-scopedlist'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers');
// Assign builder as group admin via DB (avoids ability-check on the assign endpoint itself)
await saveEntity(GroupAdmin, {
userId: builder.user.id,
groupId: group.id,
organizationId: admin.workspace.id,
});
const response = await request(nestApp.getHttpServer())
.get('/api/v2/group-permissions')
.set('tj-workspace-id', admin.workspace.id)
.set('Cookie', builder.cookie);
expect(response.statusCode).toBe(200);
const body = response.body;
const groups: any[] = Array.isArray(body) ? body : (body.groupPermissions ?? body.group_permissions ?? []);
const names = groups.map((g: any) => g.name ?? g.group);
expect(names).toContain('avengers');
});
it('builder with no group-admin assignments gets 403 on GET group list', async () => {
const admin = await createAdmin(nestApp, email('admin-noscope'));
const builder = await createBuilder(nestApp, email('builder-noscope'), {
workspace: admin.workspace,
});
const response = await request(nestApp.getHttpServer())
.get('/api/v2/group-permissions')
.set('tj-workspace-id', admin.workspace.id)
.set('Cookie', builder.cookie);
expect(response.statusCode).toBe(403);
});
});
// -------------------------------------------------------------------------
// Scoped add-user: group-admin can add users to their group
// -------------------------------------------------------------------------
describe('POST /api/v2/group-permissions/:id/users | Scoped add-user for group-admin', () => {
it('group-admin builder can add users to their administered group → 201', async () => {
const admin = await createAdmin(nestApp, email('admin-adduser'));
const builder = await createBuilder(nestApp, email('builder-adduser'), {
workspace: admin.workspace,
});
const targetUser = await createEndUser(nestApp, email('enduser-adduser'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers');
// Seed group-admin row directly
await saveEntity(GroupAdmin, {
userId: builder.user.id,
groupId: group.id,
organizationId: admin.workspace.id,
});
const response = await request(nestApp.getHttpServer())
.post(`/api/v2/group-permissions/${group.id}/users`)
.set('tj-workspace-id', admin.workspace.id)
.set('Cookie', builder.cookie)
.send({ userIds: [targetUser.user.id], groupId: group.id });
expect(response.statusCode).toBe(201);
});
it('group-admin builder cannot add users to a group they do NOT administer → 403', async () => {
const admin = await createAdmin(nestApp, email('admin-noadduser'));
const builder = await createBuilder(nestApp, email('builder-noadduser'), {
workspace: admin.workspace,
});
const targetUser = await createEndUser(nestApp, email('enduser-noadduser'), {
workspace: admin.workspace,
});
const groupA = await createCustomGroup(admin.workspace.id, 'avengers');
const groupB = await createCustomGroup(admin.workspace.id, 'titans');
// builder is only admin of groupA — groupB is unrelated
await saveEntity(GroupAdmin, {
userId: builder.user.id,
groupId: groupA.id,
organizationId: admin.workspace.id,
});
// Try to add to groupB
const response = await request(nestApp.getHttpServer())
.post(`/api/v2/group-permissions/${groupB.id}/users`)
.set('tj-workspace-id', admin.workspace.id)
.set('Cookie', builder.cookie)
.send({ userIds: [targetUser.user.id], groupId: groupB.id });
expect(response.statusCode).toBe(403);
});
});
// -------------------------------------------------------------------------
// DELETE /api/v2/group-permissions/users/:id | Scoped remove-user for group-admin
// -------------------------------------------------------------------------
describe('DELETE /api/v2/group-permissions/users/:id | Scoped remove-user for group-admin', () => {
it('group-admin builder can remove a user from their administered group → 200', async () => {
const admin = await createAdmin(nestApp, email('admin-rmuser'));
const builder = await createBuilder(nestApp, email('builder-rmuser'), {
workspace: admin.workspace,
});
const targetUser = await createEndUser(nestApp, email('enduser-rmuser'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers-rm');
// Seed the target user into the group and make builder a group admin
await createUserGroupPermissions(nestApp, targetUser.user as any, [group.name]);
await saveEntity(GroupAdmin, {
userId: builder.user.id,
groupId: group.id,
organizationId: admin.workspace.id,
});
const groupUser = await findEntityOrFail(GroupUsers, { groupId: group.id, userId: targetUser.user.id } as any);
const response = await request(nestApp.getHttpServer())
.delete(`/api/v2/group-permissions/users/${groupUser.id}`)
.set('tj-workspace-id', admin.workspace.id)
.set('Cookie', builder.cookie);
expect(response.statusCode).toBe(200);
const remaining = await findEntity(GroupUsers, { id: groupUser.id } as any);
expect(remaining).toBeNull();
});
it('group-admin builder cannot remove a user from a group they do NOT administer → 403', async () => {
const admin = await createAdmin(nestApp, email('admin-nrmuser'));
const builder = await createBuilder(nestApp, email('builder-nrmuser'), {
workspace: admin.workspace,
});
const targetUser = await createEndUser(nestApp, email('enduser-nrmuser'), {
workspace: admin.workspace,
});
const groupA = await createCustomGroup(admin.workspace.id, 'avengers-nrm');
const groupB = await createCustomGroup(admin.workspace.id, 'titans-nrm');
// builder administers groupA only; target user is in groupB
await saveEntity(GroupAdmin, {
userId: builder.user.id,
groupId: groupA.id,
organizationId: admin.workspace.id,
});
await createUserGroupPermissions(nestApp, targetUser.user as any, [groupB.name]);
const groupUser = await findEntityOrFail(GroupUsers, { groupId: groupB.id, userId: targetUser.user.id } as any);
const response = await request(nestApp.getHttpServer())
.delete(`/api/v2/group-permissions/users/${groupUser.id}`)
.set('tj-workspace-id', admin.workspace.id)
.set('Cookie', builder.cookie);
expect(response.statusCode).toBe(403);
});
});
// -------------------------------------------------------------------------
// Auto-revocation: archive
// -------------------------------------------------------------------------
describe('POST /api/organization-users/:id/archive | Group admin revoked on archive', () => {
it('group-admin row is deleted when the user is archived', async () => {
const admin = await createAdmin(nestApp, email('admin-archive'));
const builder = await createBuilder(nestApp, email('builder-archive'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers');
const groupAdminRow = await saveEntity(GroupAdmin, {
userId: builder.user.id,
groupId: group.id,
organizationId: admin.workspace.id,
});
// Archive the builder (orgUser.id is the archive target)
const archiveResponse = await request(nestApp.getHttpServer())
.post(`/api/organization-users/${builder.orgUser.id}/archive`)
.set('tj-workspace-id', admin.workspace.id)
.set('Cookie', admin.cookie)
.send({});
expect(archiveResponse.statusCode).toBe(201);
const remaining = await findEntity(GroupAdmin, { id: groupAdminRow.id });
expect(remaining).toBeNull();
});
});
// -------------------------------------------------------------------------
// Auto-revocation: role downgrade to end-user
// -------------------------------------------------------------------------
describe('PUT /api/organization-users/:id | Group admin revoked on downgrade to end-user', () => {
it('group-admin row is deleted when builder is downgraded to end-user', async () => {
const admin = await createAdmin(nestApp, email('admin-downgrade'));
const builder = await createBuilder(nestApp, email('builder-downgrade'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers');
const groupAdminRow = await saveEntity(GroupAdmin, {
userId: builder.user.id,
groupId: group.id,
organizationId: admin.workspace.id,
});
// Downgrade builder to end-user role
const updateResponse = await request(nestApp.getHttpServer())
.put(`/api/organization-users/${builder.orgUser.id}`)
.set('tj-workspace-id', admin.workspace.id)
.set('Cookie', admin.cookie)
.send({ role: 'end-user' });
// 200 or 201 depending on impl
expect([200, 201]).toContain(updateResponse.statusCode);
const remaining = await findEntity(GroupAdmin, { id: groupAdminRow.id });
expect(remaining).toBeNull();
});
});
// -------------------------------------------------------------------------
// GET /api/v2/group-permissions/:id/admins | List admins for group
// -------------------------------------------------------------------------
describe('GET /api/v2/group-permissions/:id/admins | List admins for group', () => {
it('workspace admin gets 200 and sees assigned group admins', async () => {
const admin = await createAdmin(nestApp, email('admin-listadmins'));
const builder = await createBuilder(nestApp, email('builder-listadmins'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers');
const groupAdminRow = await saveEntity(GroupAdmin, {
userId: builder.user.id,
groupId: group.id,
organizationId: admin.workspace.id,
});
const response = await request(nestApp.getHttpServer())
.get(`/api/v2/group-permissions/${group.id}/admins`)
.set('tj-workspace-id', admin.workspace.id)
.set('Cookie', admin.cookie);
expect(response.statusCode).toBe(200);
const ids = response.body.map((r: any) => r.id);
expect(ids).toContain(groupAdminRow.id);
});
it('group-admin builder can list admins for their own group → 200', async () => {
const admin = await createAdmin(nestApp, email('admin-listadmins-builder'));
const builder = await createBuilder(nestApp, email('builder-listadmins-builder'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers');
// Make builder a group admin
await saveEntity(GroupAdmin, {
userId: builder.user.id,
groupId: group.id,
organizationId: admin.workspace.id,
});
const response = await request(nestApp.getHttpServer())
.get(`/api/v2/group-permissions/${group.id}/admins`)
.set('tj-workspace-id', admin.workspace.id)
.set('Cookie', builder.cookie);
expect(response.statusCode).toBe(200);
});
it('plain builder (no group-admin assignment) gets 403', async () => {
const admin = await createAdmin(nestApp, email('admin-listadmins-403'));
const builder = await createBuilder(nestApp, email('builder-listadmins-403'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers');
const response = await request(nestApp.getHttpServer())
.get(`/api/v2/group-permissions/${group.id}/admins`)
.set('tj-workspace-id', admin.workspace.id)
.set('Cookie', builder.cookie);
expect(response.statusCode).toBe(403);
});
});
// -------------------------------------------------------------------------
// GET /api/v2/group-permissions/:id/admins/addable | Addable admins
// -------------------------------------------------------------------------
describe('GET /api/v2/group-permissions/:id/admins/addable | Addable admins', () => {
it('workspace admin gets 200 — builders and admins appear, end-users do not', async () => {
const admin = await createAdmin(nestApp, email('admin-addable'));
const builder = await createBuilder(nestApp, email('builder-addable'), {
workspace: admin.workspace,
});
const endUser = await createEndUser(nestApp, email('enduser-addable'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers');
const response = await request(nestApp.getHttpServer())
.get(`/api/v2/group-permissions/${group.id}/admins/addable`)
.set('tj-workspace-id', admin.workspace.id)
.set('Cookie', admin.cookie);
expect(response.statusCode).toBe(200);
const userIds: string[] = response.body.map((u: any) => u.id);
expect(userIds).toContain(builder.user.id);
expect(userIds).not.toContain(endUser.user.id);
});
it('end-user who is also in a custom group is still excluded from addable list', async () => {
// Regression: the old query joined group_users without restricting to
// the default group, so an end-user in any custom group appeared eligible.
const admin = await createAdmin(nestApp, email('admin-addable-regression'));
const endUser = await createEndUser(nestApp, email('enduser-customgroup'), {
workspace: admin.workspace,
});
// Put end-user in a custom group — should NOT make them eligible.
const customGroup = await createCustomGroup(admin.workspace.id, 'custom-group-1');
await createUserGroupPermissions(nestApp, endUser.user as any, [customGroup.name]);
const targetGroup = await createCustomGroup(admin.workspace.id, 'target-group');
const response = await request(nestApp.getHttpServer())
.get(`/api/v2/group-permissions/${targetGroup.id}/admins/addable`)
.set('tj-workspace-id', admin.workspace.id)
.set('Cookie', admin.cookie);
expect(response.statusCode).toBe(200);
const userIds: string[] = response.body.map((u: any) => u.id);
expect(userIds).not.toContain(endUser.user.id);
});
it('already-assigned group admin is excluded from addable list', async () => {
const admin = await createAdmin(nestApp, email('admin-addable-exclude'));
const builder = await createBuilder(nestApp, email('builder-addable-exclude'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers');
await saveEntity(GroupAdmin, {
userId: builder.user.id,
groupId: group.id,
organizationId: admin.workspace.id,
});
const response = await request(nestApp.getHttpServer())
.get(`/api/v2/group-permissions/${group.id}/admins/addable`)
.set('tj-workspace-id', admin.workspace.id)
.set('Cookie', admin.cookie);
expect(response.statusCode).toBe(200);
const userIds: string[] = response.body.map((u: any) => u.id);
expect(userIds).not.toContain(builder.user.id);
});
it('group-admin builder cannot access addable admins → 403', async () => {
// GET_ADDABLE_ADMINS is explicitly withheld from group-admin builders in
// the ability factory (comment: "Builders NEVER get: GET_ADDABLE_ADMINS").
const admin = await createAdmin(nestApp, email('admin-addable-builder403'));
const builder = await createBuilder(nestApp, email('builder-addable-builder403'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers');
await saveEntity(GroupAdmin, {
userId: builder.user.id,
groupId: group.id,
organizationId: admin.workspace.id,
});
const response = await request(nestApp.getHttpServer())
.get(`/api/v2/group-permissions/${group.id}/admins/addable`)
.set('tj-workspace-id', admin.workspace.id)
.set('Cookie', builder.cookie);
expect(response.statusCode).toBe(403);
});
});
// -------------------------------------------------------------------------
// DELETE /api/v2/group-permissions/:id/admins/:adminId | Revoke admin
// -------------------------------------------------------------------------
describe('DELETE /api/v2/group-permissions/:id/admins/:adminId | Revoke group admin', () => {
it('workspace admin can revoke a group admin assignment', async () => {
const admin = await createAdmin(nestApp, email('admin-revoke'));
const builder = await createBuilder(nestApp, email('builder-revoke'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers');
const groupAdminRow = await saveEntity(GroupAdmin, {
userId: builder.user.id,
groupId: group.id,
organizationId: admin.workspace.id,
});
const response = await revokeAdminViaApi(admin.cookie, admin.workspace.id, group.id, groupAdminRow.id);
expect(response.statusCode).toBe(200);
const remaining = await findEntity(GroupAdmin, { id: groupAdminRow.id });
expect(remaining).toBeNull();
});
it('returns 403 when a plain builder tries to revoke a group admin', async () => {
const admin = await createAdmin(nestApp, email('admin-revoke-403'));
const builder = await createBuilder(nestApp, email('builder-revoke-403'), {
workspace: admin.workspace,
});
const group = await createCustomGroup(admin.workspace.id, 'avengers');
const groupAdminRow = await saveEntity(GroupAdmin, {
userId: builder.user.id,
groupId: group.id,
organizationId: admin.workspace.id,
});
// Another builder (plain, no group-admin) tries to revoke
const anotherBuilder = await createBuilder(nestApp, email('builder-plain-revoke'), {
workspace: admin.workspace,
});
const response = await revokeAdminViaApi(anotherBuilder.cookie, admin.workspace.id, group.id, groupAdminRow.id);
expect(response.statusCode).toBe(403);
// Row must still exist
const row = await findEntity(GroupAdmin, { id: groupAdminRow.id });
expect(row).not.toBeNull();
});
});
});
});