diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index d3ed3d247a..f12a08d6b1 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -281,6 +281,7 @@ export class Server extends AbstractServer { activeWorkflowRunner, Container.get(RoleService), userService, + Container.get(License), ), Container.get(SamlController), Container.get(SourceControlController), @@ -296,6 +297,7 @@ export class Server extends AbstractServer { internalHooks, externalHooks, Container.get(UserService), + Container.get(License), postHog, ), Container.get(VariablesController), diff --git a/packages/cli/src/controllers/invitation.controller.ts b/packages/cli/src/controllers/invitation.controller.ts index c68d3a544e..6d09bcdc5a 100644 --- a/packages/cli/src/controllers/invitation.controller.ts +++ b/packages/cli/src/controllers/invitation.controller.ts @@ -28,6 +28,7 @@ export class InvitationController { private readonly internalHooks: IInternalHooksClass, private readonly externalHooks: IExternalHooksClass, private readonly userService: UserService, + private readonly license: License, private readonly postHog?: PostHogClient, ) {} @@ -88,11 +89,26 @@ export class InvitationController { `Request to send email invite(s) to user(s) failed because of an invalid email address: ${invite.email}`, ); } + + if (invite.role && !['member', 'admin'].includes(invite.role)) { + throw new BadRequestError( + `Cannot invite user with invalid role: ${invite.role}. Please ensure all invitees' roles are either 'member' or 'admin'.`, + ); + } + + if (invite.role === 'admin' && !this.license.isAdvancedPermissionsLicensed()) { + throw new UnauthorizedError( + 'Cannot invite admin user without advanced permissions. Please upgrade to a license that includes this feature.', + ); + } }); - const emails = req.body.map((e) => e.email); + const attributes = req.body.map(({ email, role }) => ({ + email, + role: role ?? 'member', + })); - const { usersInvited, usersCreated } = await this.userService.inviteMembers(req.user, emails); + const { usersInvited, usersCreated } = await this.userService.inviteUsers(req.user, attributes); await this.externalHooks.run('user.invited', [usersCreated]); diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index 8aee90a46f..619c18a8e0 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -19,6 +19,7 @@ import { Logger } from '@/Logger'; import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { License } from '@/License'; @Authorized() @RestController('/users') @@ -32,6 +33,7 @@ export class UsersController { private readonly activeWorkflowRunner: ActiveWorkflowRunner, private readonly roleService: RoleService, private readonly userService: UserService, + private readonly license: License, ) {} static ERROR_MESSAGES = { @@ -43,6 +45,7 @@ export class UsersController { NO_ADMIN_ON_OWNER: 'Admin cannot change role on global owner', NO_OWNER_ON_OWNER: 'Owner cannot change role on global owner', NO_USER_TO_OWNER: 'Cannot promote user to global owner', + NO_ADMIN_IF_UNLICENSED: 'Admin role is not available without a license', }, } as const; @@ -336,6 +339,7 @@ export class UsersController { NO_USER_TO_OWNER, NO_USER, NO_OWNER_ON_OWNER, + NO_ADMIN_IF_UNLICENSED, } = UsersController.ERROR_MESSAGES.CHANGE_ROLE; if (req.user.globalRole.scope === 'global' && req.user.globalRole.name === 'member') { @@ -364,6 +368,14 @@ export class UsersController { throw new NotFoundError(NO_USER); } + if ( + newRole.scope === 'global' && + newRole.name === 'admin' && + !this.license.isAdvancedPermissionsLicensed() + ) { + throw new UnauthorizedError(NO_ADMIN_IF_UNLICENSED); + } + if ( req.user.globalRole.scope === 'global' && req.user.globalRole.name === 'admin' && diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 61857409a4..0730465027 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -296,7 +296,11 @@ export declare namespace PasswordResetRequest { // ---------------------------------- export declare namespace UserRequest { - export type Invite = AuthenticatedRequest<{}, {}, Array<{ email: string }>>; + export type Invite = AuthenticatedRequest< + {}, + {}, + Array<{ email: string; role?: 'member' | 'admin' }> + >; export type InviteResponse = { user: { id: string; email: string; inviteAcceptUrl?: string; emailSent: boolean }; diff --git a/packages/cli/src/services/user.service.ts b/packages/cli/src/services/user.service.ts index 5f6e39625c..2500d9cfe5 100644 --- a/packages/cli/src/services/user.service.ts +++ b/packages/cli/src/services/user.service.ts @@ -238,18 +238,19 @@ export class UserService { ); } - public async inviteMembers(owner: User, emails: string[]) { + async inviteUsers(owner: User, attributes: Array<{ email: string; role: 'member' | 'admin' }>) { const memberRole = await this.roleService.findGlobalMemberRole(); + const adminRole = await this.roleService.findGlobalAdminRole(); const existingUsers = await this.findMany({ - where: { email: In(emails) }, + where: { email: In(attributes.map(({ email }) => email)) }, relations: ['globalRole'], select: ['email', 'password', 'id'], }); const existUsersEmails = existingUsers.map((user) => user.email); - const toCreateUsers = emails.filter((email) => !existUsersEmails.includes(email)); + const toCreateUsers = attributes.filter(({ email }) => !existUsersEmails.includes(email)); const pendingUsersToInvite = existingUsers.filter((email) => email.isPending); @@ -264,10 +265,10 @@ export class UserService { try { await this.getManager().transaction(async (transactionManager) => Promise.all( - toCreateUsers.map(async (email) => { + toCreateUsers.map(async ({ email, role }) => { const newUser = Object.assign(new User(), { email, - globalRole: memberRole, + globalRole: role === 'member' ? memberRole : adminRole, }); const savedUser = await transactionManager.save(newUser); createdUsers.set(email, savedUser.id); @@ -285,6 +286,6 @@ export class UserService { const usersInvited = await this.sendEmails(owner, Object.fromEntries(createdUsers)); - return { usersInvited, usersCreated: toCreateUsers }; + return { usersInvited, usersCreated: toCreateUsers.map(({ email }) => email) }; } } diff --git a/packages/cli/test/integration/invitations.api.test.ts b/packages/cli/test/integration/invitations.api.test.ts index 6fec335501..a7abe1fe0e 100644 --- a/packages/cli/test/integration/invitations.api.test.ts +++ b/packages/cli/test/integration/invitations.api.test.ts @@ -1,7 +1,6 @@ import validator from 'validator'; import type { SuperAgentTest } from 'supertest'; -import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; import { compareHash } from '@/UserManagement/UserManagementHelper'; import { UserManagementMailer } from '@/UserManagement/email/UserManagementMailer'; @@ -18,74 +17,46 @@ import { } from './shared/random'; import * as testDb from './shared/testDb'; import * as utils from './shared/utils/'; -import { getAllRoles } from './shared/db/roles'; +import { getGlobalAdminRole, getGlobalMemberRole } from './shared/db/roles'; import { createMember, createOwner, createUser, createUserShell } from './shared/db/users'; import { ExternalHooks } from '@/ExternalHooks'; import { InternalHooks } from '@/InternalHooks'; - -let credentialOwnerRole: Role; -let globalMemberRole: Role; -let workflowOwnerRole: Role; - -let owner: User; -let member: User; -let authOwnerAgent: SuperAgentTest; -let authlessAgent: SuperAgentTest; +import type { UserInvitationResponse } from './shared/utils/users'; +import { + assertInviteUserSuccessResponse, + assertInvitedUsersOnDb, + assertInviteUserErrorResponse, +} from './shared/utils/users'; +import { mocked } from 'jest-mock'; +import { License } from '@/License'; mockInstance(InternalHooks); + +const license = mockInstance(License, { + isAdvancedPermissionsLicensed: jest.fn().mockReturnValue(true), + isWithinUsersLimit: jest.fn().mockReturnValue(true), +}); + const externalHooks = mockInstance(ExternalHooks); const mailer = mockInstance(UserManagementMailer, { isEmailSetUp: true }); const testServer = utils.setupTestServer({ endpointGroups: ['invitations'] }); -type UserInvitationResponse = { - user: Pick & { inviteAcceptUrl: string; emailSent: boolean }; - error?: string; -}; - -beforeAll(async () => { - const [_, fetchedGlobalMemberRole, fetchedWorkflowOwnerRole, fetchedCredentialOwnerRole] = - await getAllRoles(); - - credentialOwnerRole = fetchedCredentialOwnerRole; - globalMemberRole = fetchedGlobalMemberRole; - workflowOwnerRole = fetchedWorkflowOwnerRole; -}); - -beforeEach(async () => { - jest.resetAllMocks(); - await testDb.truncate(['User', 'SharedCredentials', 'SharedWorkflow', 'Workflow', 'Credentials']); - owner = await createOwner(); - member = await createMember(); - authOwnerAgent = testServer.authAgentFor(owner); - authlessAgent = testServer.authlessAgent; -}); - -const assertInviteUserSuccessResponse = (data: UserInvitationResponse) => { - expect(validator.isUUID(data.user.id)).toBe(true); - expect(data.user.inviteAcceptUrl).toBeUndefined(); - expect(data.user.email).toBeDefined(); - expect(data.user.emailSent).toBe(true); -}; - -const assertInviteUserErrorResponse = (data: UserInvitationResponse) => { - expect(validator.isUUID(data.user.id)).toBe(true); - expect(data.user.inviteAcceptUrl).toBeDefined(); - expect(data.user.email).toBeDefined(); - expect(data.user.emailSent).toBe(false); - expect(data.error).toBeDefined(); -}; - -const assertInvitedUsersOnDb = (user: User) => { - expect(user.firstName).toBeNull(); - expect(user.lastName).toBeNull(); - expect(user.personalizationAnswers).toBeNull(); - expect(user.password).toBeNull(); - expect(user.isPending).toBe(true); -}; - describe('POST /invitations/:id/accept', () => { - test('should fill out a user shell', async () => { + let owner: User; + + let authlessAgent: SuperAgentTest; + + beforeAll(async () => { + await testDb.truncate(['User']); + + owner = await createOwner(); + + authlessAgent = testServer.authlessAgent; + }); + + test('should fill out a member shell', async () => { + const globalMemberRole = await getGlobalMemberRole(); const memberShell = await createUserShell(globalMemberRole); const memberData = { @@ -96,11 +67,9 @@ describe('POST /invitations/:id/accept', () => { }; const response = await authlessAgent - .post( - `/invitations/${memberShell.id}/ - accept`, - ) - .send(memberData); + .post(`/invitations/${memberShell.id}/accept`) + .send(memberData) + .expect(200); const { id, @@ -121,21 +90,78 @@ describe('POST /invitations/:id/accept', () => { expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); expect(isPending).toBe(false); - expect(globalRole).toBeDefined(); + expect(globalRole.scope).toBe('global'); + expect(globalRole.name).toBe('member'); expect(apiKey).not.toBeDefined(); const authToken = utils.getAuthToken(response); expect(authToken).toBeDefined(); - const member = await Container.get(UserRepository).findOneByOrFail({ id: memberShell.id }); - expect(member.firstName).toBe(memberData.firstName); - expect(member.lastName).toBe(memberData.lastName); - expect(member.password).not.toBe(memberData.password); + const storedMember = await Container.get(UserRepository).findOneByOrFail({ + id: memberShell.id, + }); + + expect(storedMember.firstName).toBe(memberData.firstName); + expect(storedMember.lastName).toBe(memberData.lastName); + expect(storedMember.password).not.toBe(memberData.password); }); - test('should fail with invalid inputs', async () => { + test('should fill out an admin shell', async () => { + const globalAdminRole = await getGlobalAdminRole(); + const adminShell = await createUserShell(globalAdminRole); + + const memberData = { + inviterId: owner.id, + firstName: randomName(), + lastName: randomName(), + password: randomValidPassword(), + }; + + const response = await authlessAgent + .post(`/invitations/${adminShell.id}/accept`) + .send(memberData) + .expect(200); + + const { + id, + email, + firstName, + lastName, + personalizationAnswers, + password, + globalRole, + isPending, + apiKey, + } = response.body.data; + + expect(validator.isUUID(id)).toBe(true); + expect(email).toBeDefined(); + expect(firstName).toBe(memberData.firstName); + expect(lastName).toBe(memberData.lastName); + expect(personalizationAnswers).toBeNull(); + expect(password).toBeUndefined(); + expect(isPending).toBe(false); + expect(globalRole.scope).toBe('global'); + expect(globalRole.name).toBe('admin'); + expect(apiKey).not.toBeDefined(); + + const authToken = utils.getAuthToken(response); + expect(authToken).toBeDefined(); + + const storedAdmin = await Container.get(UserRepository).findOneByOrFail({ + id: adminShell.id, + }); + + expect(storedAdmin.firstName).toBe(memberData.firstName); + expect(storedAdmin.lastName).toBe(memberData.lastName); + expect(storedAdmin.password).not.toBe(memberData.password); + }); + + test('should fail with invalid payloads', async () => { const memberShellEmail = randomEmail(); + const globalMemberRole = await getGlobalMemberRole(); + const memberShell = await Container.get(UserRepository).save({ email: memberShellEmail, globalRole: globalMemberRole, @@ -171,51 +197,62 @@ describe('POST /invitations/:id/accept', () => { ]; for (const invalidPayload of invalidPayloads) { - const response = await authlessAgent + await authlessAgent .post(`/invitations/${memberShell.id}/accept`) - .send(invalidPayload); - expect(response.statusCode).toBe(400); + .send(invalidPayload) + .expect(400); - const storedUser = await Container.get(UserRepository).findOneOrFail({ + const storedMemberShell = await Container.get(UserRepository).findOneOrFail({ where: { email: memberShellEmail }, }); - expect(storedUser.firstName).toBeNull(); - expect(storedUser.lastName).toBeNull(); - expect(storedUser.password).toBeNull(); + expect(storedMemberShell.firstName).toBeNull(); + expect(storedMemberShell.lastName).toBeNull(); + expect(storedMemberShell.password).toBeNull(); } }); test('should fail with already accepted invite', async () => { + const globalMemberRole = await getGlobalMemberRole(); const member = await createUser({ globalRole: globalMemberRole }); - const newMemberData = { + const memberData = { inviterId: owner.id, firstName: randomName(), lastName: randomName(), password: randomValidPassword(), }; - const response = await authlessAgent - .post(`/invitations/${member.id}/accept`) - .send(newMemberData); - - expect(response.statusCode).toBe(400); + await authlessAgent.post(`/invitations/${member.id}/accept`).send(memberData).expect(400); const storedMember = await Container.get(UserRepository).findOneOrFail({ where: { email: member.email }, }); - expect(storedMember.firstName).not.toBe(newMemberData.firstName); - expect(storedMember.lastName).not.toBe(newMemberData.lastName); + + expect(storedMember.firstName).not.toBe(memberData.firstName); + expect(storedMember.lastName).not.toBe(memberData.lastName); + expect(storedMember.password).not.toBe(memberData.password); const comparisonResult = await compareHash(member.password, storedMember.password); + expect(comparisonResult).toBe(false); - expect(storedMember.password).not.toBe(newMemberData.password); }); }); describe('POST /invitations', () => { - test('should fail with invalid inputs', async () => { + let owner: User; + let member: User; + let ownerAgent: SuperAgentTest; + + beforeAll(async () => { + await testDb.truncate(['User']); + + owner = await createOwner(); + member = await createMember(); + ownerAgent = testServer.authAgentFor(owner); + }); + + test('should fail with invalid payloads', async () => { const invalidPayloads = [ randomEmail(), [randomEmail()], @@ -226,48 +263,96 @@ describe('POST /invitations', () => { await Promise.all( invalidPayloads.map(async (invalidPayload) => { - const response = await authOwnerAgent.post('/invitations').send(invalidPayload); - expect(response.statusCode).toBe(400); + await ownerAgent.post('/invitations').send(invalidPayload).expect(400); - const users = await Container.get(UserRepository).find(); - expect(users.length).toBe(2); // DB unaffected + const usersCount = await Container.get(UserRepository).count(); + + expect(usersCount).toBe(2); // DB unaffected }), ); }); - test('should ignore an empty payload', async () => { - const response = await authOwnerAgent.post('/invitations').send([]); + test('should return 200 on empty payload', async () => { + const response = await ownerAgent.post('/invitations').send([]).expect(200); - const { data } = response.body; + expect(response.body.data).toStrictEqual([]); - expect(response.statusCode).toBe(200); - expect(Array.isArray(data)).toBe(true); - expect(data.length).toBe(0); + const usersCount = await Container.get(UserRepository).count(); - const users = await Container.get(UserRepository).find(); - expect(users.length).toBe(2); + expect(usersCount).toBe(2); }); - test('should succeed if emailing is not set up', async () => { - mailer.invite.mockResolvedValueOnce({ emailSent: false }); - const usersToInvite = randomEmail(); - const response = await authOwnerAgent.post('/invitations').send([{ email: usersToInvite }]); + test('should return 200 if emailing is not set up', async () => { + mailer.invite.mockResolvedValue({ emailSent: false }); + + const response = await ownerAgent + .post('/invitations') + .send([{ email: randomEmail() }]) + .expect(200); - expect(response.statusCode).toBe(200); expect(response.body.data).toBeInstanceOf(Array); expect(response.body.data.length).toBe(1); + const { user } = response.body.data[0]; + expect(user.inviteAcceptUrl).toBeDefined(); + const inviteUrl = new URL(user.inviteAcceptUrl); + expect(inviteUrl.searchParams.get('inviterId')).toBe(owner.id); expect(inviteUrl.searchParams.get('inviteeId')).toBe(user.id); }); + test('should create member shell', async () => { + mailer.invite.mockResolvedValue({ emailSent: false }); + + const response = await ownerAgent + .post('/invitations') + .send([{ email: randomEmail() }]) + .expect(200); + + const [result] = response.body.data as UserInvitationResponse[]; + + const storedUser = await Container.get(UserRepository).findOneByOrFail({ + id: result.user.id, + }); + + assertInvitedUsersOnDb(storedUser); + }); + + test('should create admin shell if licensed', async () => { + mailer.invite.mockResolvedValue({ emailSent: false }); + + const response = await ownerAgent + .post('/invitations') + .send([{ email: randomEmail(), role: 'admin' }]) + .expect(200); + + const [result] = response.body.data as UserInvitationResponse[]; + + const storedUser = await Container.get(UserRepository).findOneByOrFail({ + id: result.user.id, + }); + + assertInvitedUsersOnDb(storedUser); + }); + + test('should fail to create admin shell if not licensed', async () => { + license.isAdvancedPermissionsLicensed.mockReturnValue(false); + mailer.invite.mockResolvedValue({ emailSent: false }); + + await ownerAgent + .post('/invitations') + .send([{ email: randomEmail(), role: 'admin' }]) + .expect(403); + }); + test('should email invites and create user shells but ignore existing', async () => { - const internalHooks = Container.get(InternalHooks); + externalHooks.run.mockClear(); - mailer.invite.mockImplementation(async () => ({ emailSent: true })); + mailer.invite.mockResolvedValue({ emailSent: true }); + const globalMemberRole = await getGlobalMemberRole(); const memberShell = await createUserShell(globalMemberRole); const newUser = randomEmail(); @@ -281,54 +366,54 @@ describe('POST /invitations', () => { const payload = testEmails.map((email) => ({ email })); - const response = await authOwnerAgent.post('/invitations').send(payload); + const response = await ownerAgent.post('/invitations').send(payload).expect(200); - expect(response.statusCode).toBe(200); + const internalHooks = Container.get(InternalHooks); expect(internalHooks.onUserTransactionalEmail).toHaveBeenCalledTimes(usersToInvite.length); expect(externalHooks.run).toHaveBeenCalledTimes(1); + const [hookName, hookData] = externalHooks.run.mock.calls[0]; + expect(hookName).toBe('user.invited'); expect(hookData?.[0]).toStrictEqual(usersToCreate); - for (const invitationResponse of response.body.data as UserInvitationResponse[]) { + const result = response.body.data as UserInvitationResponse[]; + + for (const invitationResponse of result) { + assertInviteUserSuccessResponse(invitationResponse); + const storedUser = await Container.get(UserRepository).findOneByOrFail({ id: invitationResponse.user.id, }); - assertInviteUserSuccessResponse(invitationResponse); - assertInvitedUsersOnDb(storedUser); } - for (const [onUserTransactionalEmailParameter] of internalHooks.onUserTransactionalEmail.mock - .calls) { + const calls = mocked(internalHooks).onUserTransactionalEmail.mock.calls; + + for (const [onUserTransactionalEmailParameter] of calls) { expect(onUserTransactionalEmailParameter.user_id).toBeDefined(); expect(onUserTransactionalEmailParameter.message_type).toBe('New user invite'); expect(onUserTransactionalEmailParameter.public_api).toBe(false); } }); - test('should return error when invite method throws an error', async () => { - const error = 'failed to send email'; - + test('should return 200 when invite method throws error', async () => { mailer.invite.mockImplementation(async () => { - throw new Error(error); + throw new Error('failed to send email'); }); - const newUser = randomEmail(); - - const usersToCreate = [newUser]; - - const payload = usersToCreate.map((email) => ({ email })); - - const response = await authOwnerAgent.post('/invitations').send(payload); + const response = await ownerAgent + .post('/invitations') + .send([{ email: randomEmail() }]) + .expect(200); expect(response.body.data).toBeInstanceOf(Array); expect(response.body.data.length).toBe(1); - expect(response.statusCode).toBe(200); - const invitationResponse = response.body.data[0]; + + const [invitationResponse] = response.body.data; assertInviteUserErrorResponse(invitationResponse); }); diff --git a/packages/cli/test/integration/shared/utils/testServer.ts b/packages/cli/test/integration/shared/utils/testServer.ts index fbd99aef13..0e2216f5da 100644 --- a/packages/cli/test/integration/shared/utils/testServer.ts +++ b/packages/cli/test/integration/shared/utils/testServer.ts @@ -250,6 +250,7 @@ export const setupTestServer = ({ Container.get(ActiveWorkflowRunner), Container.get(RS), Container.get(US), + Container.get(License), ), ); break; @@ -268,6 +269,7 @@ export const setupTestServer = ({ Container.get(InternalHooks), Container.get(EHS), Container.get(USE), + Container.get(License), ), ); break; diff --git a/packages/cli/test/integration/shared/utils/users.ts b/packages/cli/test/integration/shared/utils/users.ts index a32a0a781a..c2b4d6be9b 100644 --- a/packages/cli/test/integration/shared/utils/users.ts +++ b/packages/cli/test/integration/shared/utils/users.ts @@ -1,4 +1,6 @@ +import validator from 'validator'; import type { PublicUser } from '@/Interfaces'; +import type { User } from '@/databases/entities/User'; export const validateUser = (user: PublicUser) => { expect(typeof user.id).toBe('string'); @@ -13,3 +15,31 @@ export const validateUser = (user: PublicUser) => { expect(user.password).toBeUndefined(); expect(user.globalRole).toBeDefined(); }; + +export const assertInviteUserSuccessResponse = (data: UserInvitationResponse) => { + expect(validator.isUUID(data.user.id)).toBe(true); + expect(data.user.inviteAcceptUrl).toBeUndefined(); + expect(data.user.email).toBeDefined(); + expect(data.user.emailSent).toBe(true); +}; + +export const assertInviteUserErrorResponse = (data: UserInvitationResponse) => { + expect(validator.isUUID(data.user.id)).toBe(true); + expect(data.user.inviteAcceptUrl).toBeDefined(); + expect(data.user.email).toBeDefined(); + expect(data.user.emailSent).toBe(false); + expect(data.error).toBeDefined(); +}; + +export const assertInvitedUsersOnDb = (user: User) => { + expect(user.firstName).toBeNull(); + expect(user.lastName).toBeNull(); + expect(user.personalizationAnswers).toBeNull(); + expect(user.password).toBeNull(); + expect(user.isPending).toBe(true); +}; + +export type UserInvitationResponse = { + user: Pick & { inviteAcceptUrl: string; emailSent: boolean }; + error?: string; +}; diff --git a/packages/cli/test/integration/users.api.test.ts b/packages/cli/test/integration/users.api.test.ts index ff15cb0655..342df5f672 100644 --- a/packages/cli/test/integration/users.api.test.ts +++ b/packages/cli/test/integration/users.api.test.ts @@ -18,9 +18,16 @@ import * as testDb from './shared/testDb'; import type { SuperAgentTest } from 'supertest'; import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; +import { License } from '@/License'; +import { mockInstance } from '../shared/mocking'; const testServer = utils.setupTestServer({ endpointGroups: ['users'] }); +const license = mockInstance(License, { + isAdvancedPermissionsLicensed: jest.fn().mockReturnValue(true), + isWithinUsersLimit: jest.fn().mockReturnValue(true), +}); + describe('GET /users', () => { let owner: User; let member: User; @@ -362,6 +369,7 @@ describe('PATCH /users/:id/role', () => { NO_USER_TO_OWNER, NO_USER, NO_OWNER_ON_OWNER, + NO_ADMIN_IF_UNLICENSED, } = UsersController.ERROR_MESSAGES.CHANGE_ROLE; beforeAll(async () => { @@ -518,6 +526,17 @@ describe('PATCH /users/:id/role', () => { expect(response.body.message).toBe(NO_USER_TO_OWNER); }); + test('should fail to promote member to admin if not licensed', async () => { + license.isAdvancedPermissionsLicensed.mockReturnValueOnce(false); + + const response = await adminAgent.patch(`/users/${member.id}/role`).send({ + newRole: { scope: 'global', name: 'admin' }, + }); + + expect(response.statusCode).toBe(403); + expect(response.body.message).toBe(NO_ADMIN_IF_UNLICENSED); + }); + test('should be able to demote admin to member', async () => { const response = await adminAgent.patch(`/users/${otherAdmin.id}/role`).send({ newRole: { scope: 'global', name: 'member' }, @@ -556,7 +575,7 @@ describe('PATCH /users/:id/role', () => { adminAgent = testServer.authAgentFor(admin); }); - test('should be able to promote member to admin', async () => { + test('should be able to promote member to admin if licensed', async () => { const response = await adminAgent.patch(`/users/${member.id}/role`).send({ newRole: { scope: 'global', name: 'admin' }, }); @@ -613,7 +632,18 @@ describe('PATCH /users/:id/role', () => { expect(response.body.message).toBe(NO_USER_TO_OWNER); }); - test('should be able to promote member to admin', async () => { + test('should fail to promote member to admin if not licensed', async () => { + license.isAdvancedPermissionsLicensed.mockReturnValueOnce(false); + + const response = await ownerAgent.patch(`/users/${member.id}/role`).send({ + newRole: { scope: 'global', name: 'admin' }, + }); + + expect(response.statusCode).toBe(403); + expect(response.body.message).toBe(NO_ADMIN_IF_UNLICENSED); + }); + + test('should be able to promote member to admin if licensed', async () => { const response = await ownerAgent.patch(`/users/${member.id}/role`).send({ newRole: { scope: 'global', name: 'admin' }, });