UNPKG

@budibase/worker

Version:
1,385 lines (1,155 loc) • 47.2 kB
import { InviteUsersResponse, OIDCUser, User } from "@budibase/types" import { accounts as _accounts, events, tenancy } from "@budibase/backend-core" import { mocks as featureMocks } from "@budibase/backend-core/tests" import * as userSdk from "../../../../sdk/users" import { TestConfiguration, mocks, structures } from "../../../../tests" jest.mock("nodemailer") const sendMailMock = mocks.email.mock() const accounts = jest.mocked(_accounts) describe("/api/global/users", () => { const config = new TestConfiguration() beforeAll(async () => { await config.beforeAll() }) afterAll(async () => { await config.afterAll() }) beforeEach(async () => { jest.clearAllMocks() featureMocks.licenses.useCloudFree() await mocks.licenses.setUsersQuota(1000) await mocks.licenses.setCreatorsQuota(1000) }) async function createBuilderUser() { const saveResponse = await config.api.users.saveUser( structures.users.builderUser(), 200 ) const { body: user } = await config.api.users.getUser(saveResponse.body._id) await config.login(user) return user } describe("POST /api/global/users/invite", () => { it("should be able to generate an invitation", async () => { const email = structures.users.newEmail() const { code, res } = await config.api.users.sendUserInvite( sendMailMock, email ) expect(res.body?.message).toBe("Invitation has been sent.") expect(res.body?.unsuccessful.length).toBe(0) expect(res.body?.successful.length).toBe(1) expect(res.body?.successful[0].email).toBe(email) expect(sendMailMock).toHaveBeenCalled() expect(code).toBeDefined() expect(events.user.invited).toHaveBeenCalledTimes(1) }) it("should not be able to generate an invitation for existing user", async () => { const { code, res } = await config.api.users.sendUserInvite( sendMailMock, config.user!.email, 400 ) expect(res.body.message).toBe(`Unavailable`) expect(sendMailMock).toHaveBeenCalledTimes(0) expect(code).toBeUndefined() expect(events.user.invited).toHaveBeenCalledTimes(0) }) it("should not invite the same user twice", async () => { const email = structures.users.newEmail() await config.api.users.sendUserInvite(sendMailMock, email) jest.clearAllMocks() const { code, res } = await config.api.users.sendUserInvite( sendMailMock, email, 400 ) expect(res.body.message).toBe(`Unavailable`) expect(sendMailMock).toHaveBeenCalledTimes(0) expect(code).toBeUndefined() expect(events.user.invited).toHaveBeenCalledTimes(0) }) it("should not allow creator users to access single invite endpoint", async () => { const user = await createBuilderUser() const { res } = await config.withUser(user, () => config.api.users.sendUserInvite( sendMailMock, structures.users.newEmail(), 403 ) ) expect(res.body.message).toBe("Admin user only endpoint.") }) it("should be able to create new user from invite", async () => { const email = structures.users.newEmail() const { code } = await config.api.users.sendUserInvite( sendMailMock, email ) const res = await config.api.users.acceptInvite(code) expect(res.body._id).toBeDefined() const user = await config.getUser(email) expect(user).toBeDefined() expect(user!._id).toEqual(res.body._id) expect(events.user.inviteAccepted).toHaveBeenCalledTimes(1) expect(events.user.inviteAccepted).toHaveBeenCalledWith(user) }) }) describe("POST /api/global/users/invite/:code/:role", () => { it("should be able to add workspace id to invite", async () => { const email = structures.users.newEmail() const { code } = await config.api.users.sendUserInvite( sendMailMock, email ) const appId = "app_123456789" const role = "BASIC" const res = await config.withApp(appId, () => config.api.users.addWorkspaceIdToInvite(code, role) ) expect(res.body.info.apps).toBeDefined() expect(res.body.info.apps[appId]).toBe(role) }) it("should handle invalid invite code", async () => { const appId = "app_123456789" const role = "BASIC" await config.withApp(appId, () => config.api.users.addWorkspaceIdToInvite("invalid_code", role, 400) ) }) it("should not allow builders to edit invites for apps they don't have access to", async () => { const { code } = await config.api.users.sendUserInvite( sendMailMock, structures.users.newEmail() ) // Create a builder user with access to another app const builderUser = await config.createUser({ ...structures.users.user(), builder: { global: false, apps: ["app_allowed_123"], }, admin: { global: false }, }) await config.withUser(builderUser, () => config.withApp("app_no_access", () => config.api.users.addWorkspaceIdToInvite(code, "BASIC", 403) ) ) }) it("should allow builders to edit invites for apps they have access to", async () => { const { code } = await config.api.users.sendUserInvite( sendMailMock, structures.users.newEmail() ) const appId = "app_allowed_123" const role = "BASIC" // Create a builder user with access to the specific app const builderUser = await config.createUser({ ...structures.users.user(), builder: { global: false, apps: [appId], // Has access to this specific app }, admin: { global: false }, }) await config.login(builderUser) const res = await config.withUser(builderUser, () => config.withApp(appId, () => config.api.users.addWorkspaceIdToInvite(code, role, 200) ) ) expect(res.body.info.apps[appId]).toBe(role) }) it("should not allow builders to edit invites for any app", async () => { const { code } = await config.api.users.sendUserInvite( sendMailMock, structures.users.newEmail() ) const appId = "app_any_123" const role = "BASIC" // Create a global builder user const builderUser = await config.createUser({ ...structures.users.user(), builder: { global: true, }, admin: { global: false }, }) await config.login(builderUser) await config.withUser(builderUser, async () => config.withApp(appId, () => config.api.users.addWorkspaceIdToInvite(code, role, 403) ) ) }) }) describe("DELETE /api/global/users/invite/:code", () => { it("should be able to remove workspace id from invite", async () => { const email = structures.users.newEmail() const { code } = await config.api.users.sendUserInvite( sendMailMock, email ) const appId = "app_123456789" const role = "BASIC" // First add the workspace await config.withApp(appId, () => config.api.users.addWorkspaceIdToInvite(code, role) ) // Then remove it const res = await config.withApp(appId, () => config.api.users.removeWorkspaceIdFromInvite(code) ) expect(res.body.info.apps).toBeDefined() expect(res.body.info.apps[appId]).toBeUndefined() }) it("should handle removing non-existent workspace id", async () => { const email = structures.users.newEmail() const { code } = await config.api.users.sendUserInvite( sendMailMock, email ) const appId = "app_nonexistent" const res = await config.withApp(appId, () => config.api.users.removeWorkspaceIdFromInvite(code) ) expect(res.body.info.apps).toBeDefined() expect(res.body.info.apps[appId]).toBeUndefined() }) it("should handle invalid invite code", async () => { const appId = "app_123456789" await config.withApp(appId, () => config.api.users.removeWorkspaceIdFromInvite("invalid_code", 400) ) }) it("should not allow builders to delete invites for apps they don't have access to", async () => { const { code } = await config.api.users.sendUserInvite( sendMailMock, structures.users.newEmail() ) const appId = "app_no_access" const role = "BASIC" // First add the workspace as admin await config.withApp(appId, () => config.api.users.addWorkspaceIdToInvite(code, role) ) // Create a builder user with specific app access const builderUser = await config.createUser({ ...structures.users.user(), builder: { global: false, apps: ["app_allowed_123"], // Different app than the one being tested }, admin: { global: false }, }) await config.login(builderUser) await config.withUser(builderUser, () => config.withApp(appId, () => config.api.users.removeWorkspaceIdFromInvite(code, 403) ) ) }) it("should allow builders to delete invites for apps they have access to", async () => { const { code } = await config.api.users.sendUserInvite( sendMailMock, structures.users.newEmail() ) const appId = "app_allowed_456" const role = "BASIC" // First add the workspace as admin await config.withApp(appId, () => config.api.users.addWorkspaceIdToInvite(code, role) ) // Create a builder user with access to the specific app const builderUser = await config.createUser({ ...structures.users.user(), builder: { global: false, apps: [appId], // Has access to this specific app }, admin: { global: false }, }) await config.login(builderUser) const res = await config.withUser(builderUser, () => config.withApp(appId, () => config.api.users.removeWorkspaceIdFromInvite(code, 200) ) ) expect(res.body.info.apps[appId]).toBeUndefined() }) }) describe("POST /api/global/users/multi/invite", () => { it("should be able to generate an invitation", async () => { const newUserInvite = () => ({ email: structures.users.newEmail(), userInfo: {}, }) const request = [newUserInvite(), newUserInvite()] const res = await config.api.users.sendMultiUserInvite(request) const body = res.body as InviteUsersResponse expect(body.successful.length).toBe(2) expect(body.unsuccessful.length).toBe(0) expect(sendMailMock).toHaveBeenCalledTimes(2) expect(events.user.invited).toHaveBeenCalledTimes(2) }) it("should not be able to generate an invitation for existing user", async () => { const request = [{ email: config.user!.email, userInfo: {} }] const res = await config.api.users.sendMultiUserInvite(request) const body = res.body as InviteUsersResponse expect(body.successful.length).toBe(0) expect(body.unsuccessful.length).toBe(1) expect(body.unsuccessful[0].reason).toBe("Unavailable") expect(sendMailMock).toHaveBeenCalledTimes(0) expect(events.user.invited).toHaveBeenCalledTimes(0) }) it("should not be able to generate an invitation for user that has already been invited", async () => { const email = structures.users.newEmail() await config.api.users.sendUserInvite(sendMailMock, email) jest.clearAllMocks() const request = [{ email: email, userInfo: {} }] const res = await config.api.users.sendMultiUserInvite(request) const body = res.body as InviteUsersResponse expect(body.successful.length).toBe(0) expect(body.unsuccessful.length).toBe(1) expect(body.unsuccessful[0].reason).toBe("Unavailable") expect(sendMailMock).toHaveBeenCalledTimes(0) expect(events.user.invited).toHaveBeenCalledTimes(0) }) it("should not allow creator users to access multi-invite endpoint", async () => { const user = await createBuilderUser() const request = [ { email: structures.users.newEmail(), userInfo: { admin: { global: true } }, }, ] const res = await config.withUser(user, () => config.api.users.sendMultiUserInvite(request, 403) ) expect(res.body.message).toBe("Admin user only endpoint.") }) }) describe("POST /api/global/users/bulk", () => { it("should ignore users existing in the same tenant", async () => { const user = await config.createUser() jest.clearAllMocks() const response = await config.api.users.bulkCreateUsers([user]) expect(response.created?.successful.length).toBe(0) expect(response.created?.unsuccessful.length).toBe(1) expect(response.created?.unsuccessful[0].email).toBe(user.email) expect(events.user.created).toHaveBeenCalledTimes(0) }) it("should ignore users existing in other tenants", async () => { const user = await config.createUser() jest.clearAllMocks() await tenancy.doInTenant(config.getTenantId(), async () => { const response = await config.api.users.bulkCreateUsers([user]) expect(response.created?.successful.length).toBe(0) expect(response.created?.unsuccessful.length).toBe(1) expect(response.created?.unsuccessful[0].email).toBe(user.email) expect(events.user.created).toHaveBeenCalledTimes(0) }) }) it("should ignore accounts using the same email", async () => { const account = structures.accounts.account() const resp = await config.api.accounts.saveMetadata(account) const user = structures.users.user({ email: resp.email }) jest.clearAllMocks() const response = await config.api.users.bulkCreateUsers([user]) expect(response.created?.successful.length).toBe(0) expect(response.created?.unsuccessful.length).toBe(1) expect(response.created?.unsuccessful[0].email).toBe(user.email) expect(events.user.created).toHaveBeenCalledTimes(0) }) it("should be able to bulk create users", async () => { const builder = structures.users.builderUser() const admin = structures.users.adminUser() const user = structures.users.user() const response = await config.api.users.bulkCreateUsers([ builder, admin, user, ]) expect(response.created?.successful.length).toBe(3) expect(response.created?.successful[0].email).toBe(builder.email) expect(response.created?.successful[1].email).toBe(admin.email) expect(response.created?.successful[2].email).toBe(user.email) expect(response.created?.unsuccessful.length).toBe(0) expect(events.user.created).toHaveBeenCalledTimes(3) expect(events.user.permissionAdminAssigned).toHaveBeenCalledTimes(1) expect(events.user.permissionBuilderAssigned).toHaveBeenCalledTimes(2) }) }) describe("POST /api/global/users", () => { it("should be able to create a basic user", async () => { const user = structures.users.user() await config.api.users.saveUser(user) expect(events.user.created).toHaveBeenCalledTimes(1) expect(events.user.updated).not.toHaveBeenCalled() expect(events.user.permissionBuilderAssigned).not.toHaveBeenCalled() expect(events.user.permissionAdminAssigned).not.toHaveBeenCalled() }) it("should be able to create an admin user", async () => { const user = structures.users.adminUser() await config.api.users.saveUser(user) expect(events.user.created).toHaveBeenCalledTimes(1) expect(events.user.updated).not.toHaveBeenCalled() expect(events.user.permissionBuilderAssigned).toHaveBeenCalledTimes(1) expect(events.user.permissionAdminAssigned).toHaveBeenCalledTimes(1) }) it("should be able to create a builder user", async () => { const user = structures.users.builderUser() await config.api.users.saveUser(user) expect(events.user.created).toHaveBeenCalledTimes(1) expect(events.user.updated).not.toHaveBeenCalled() expect(events.user.permissionBuilderAssigned).toHaveBeenCalledTimes(1) expect(events.user.permissionAdminAssigned).not.toHaveBeenCalled() }) it("should be able to assign app roles", async () => { const user = structures.users.user() user.roles = { app_123: "role1", app_456: "role2", } await config.api.users.saveUser(user) const savedUser = await config.getUser(user.email) expect(events.user.created).toHaveBeenCalledTimes(1) expect(events.user.updated).not.toHaveBeenCalled() expect(events.role.assigned).toHaveBeenCalledTimes(2) expect(events.role.assigned).toHaveBeenCalledWith(savedUser, "role1") expect(events.role.assigned).toHaveBeenCalledWith(savedUser, "role2") }) it("should not be able to create user that exists in same tenant", async () => { const user = await config.createUser() jest.clearAllMocks() delete user._id delete user._rev const response = await config.api.users.saveUser(user, 400) expect(response.body.message).toBe( `Email already in use: '${user.email}'` ) expect(events.user.created).toHaveBeenCalledTimes(0) }) it("should not be able to create user that exists in other tenant", async () => { const user = await config.createUser() jest.clearAllMocks() await tenancy.doInTenant(config.getTenantId(), async () => { delete user._id const response = await config.api.users.saveUser(user, 400) expect(response.body.message).toBe( `Email already in use: '${user.email}'` ) expect(events.user.created).toHaveBeenCalledTimes(0) }) }) it("should not be able to create user with the same email as an account", async () => { const user = structures.users.user() const account = structures.accounts.cloudAccount() accounts.getAccount.mockReturnValueOnce(Promise.resolve(account)) const response = await config.api.users.saveUser(user, 400) expect(response.body.message).toBe( `Email already in use: '${user.email}'` ) expect(events.user.created).toHaveBeenCalledTimes(0) }) it("should not be able to create a user with the same email and different casing", async () => { const user = structures.users.user() await config.api.users.saveUser(user) user.email = user.email.toUpperCase() await config.api.users.saveUser(user, 400) expect(events.user.created).toHaveBeenCalledTimes(1) }) it("should not be able to bulk create a user with the same email and different casing", async () => { const user = structures.users.user() await config.api.users.saveUser(user) user.email = user.email.toUpperCase() await config.api.users.bulkCreateUsers([user]) expect(events.user.created).toHaveBeenCalledTimes(1) }) it("should not allow a non-admin user to create a new user", async () => { const nonAdmin = await config.createUser(structures.users.builderUser()) await config.createSession(nonAdmin) const newUser = structures.users.user() await config.api.users.saveUser( newUser, 403, config.authHeaders(nonAdmin) ) }) }) describe("POST /api/global/users (update)", () => { it("should be able to update a basic user", async () => { const user = await config.createUser() jest.clearAllMocks() await config.api.users.saveUser(user) expect(events.user.created).not.toHaveBeenCalled() expect(events.user.updated).toHaveBeenCalledTimes(1) expect(events.user.permissionBuilderAssigned).not.toHaveBeenCalled() expect(events.user.permissionAdminAssigned).not.toHaveBeenCalled() expect(events.user.passwordForceReset).not.toHaveBeenCalled() }) it("should not allow a user to update their own admin/builder status", async () => { const user = (await config.api.users.getUser(config.user!._id!)) .body as User await config.api.users.saveUser({ ...user, admin: { global: false, }, builder: { global: false, }, }) const userOut = (await config.api.users.getUser(user._id!)).body expect(userOut.admin.global).toBe(true) expect(userOut.builder.global).toBe(true) }) it("should be able to force reset password", async () => { const user = await config.createUser() jest.clearAllMocks() user.forceResetPassword = true user.password = "tempPassword" await config.api.users.saveUser(user) expect(events.user.created).not.toHaveBeenCalled() expect(events.user.updated).toHaveBeenCalledTimes(1) expect(events.user.permissionBuilderAssigned).not.toHaveBeenCalled() expect(events.user.permissionAdminAssigned).not.toHaveBeenCalled() expect(events.user.passwordForceReset).toHaveBeenCalledTimes(1) }) it("should be able to update a basic user to an admin user", async () => { const user = await config.createUser() jest.clearAllMocks() await config.api.users.saveUser(structures.users.adminUser(user)) expect(events.user.created).not.toHaveBeenCalled() expect(events.user.updated).toHaveBeenCalledTimes(1) expect(events.user.permissionBuilderAssigned).toHaveBeenCalledTimes(1) expect(events.user.permissionAdminAssigned).toHaveBeenCalledTimes(1) }) it("should be able to update a basic user to a builder user", async () => { const user = await config.createUser() jest.clearAllMocks() await config.api.users.saveUser(structures.users.builderUser(user)) expect(events.user.created).not.toHaveBeenCalled() expect(events.user.updated).toHaveBeenCalledTimes(1) expect(events.user.permissionBuilderAssigned).toHaveBeenCalledTimes(1) expect(events.user.permissionAdminAssigned).not.toHaveBeenCalled() }) it("should be able to update an admin user to a basic user", async () => { const user = await config.createUser(structures.users.adminUser()) jest.clearAllMocks() user.admin!.global = false user.builder!.global = false await config.api.users.saveUser(user) expect(events.user.created).not.toHaveBeenCalled() expect(events.user.updated).toHaveBeenCalledTimes(1) expect(events.user.permissionAdminRemoved).toHaveBeenCalledTimes(1) expect(events.user.permissionBuilderRemoved).toHaveBeenCalledTimes(1) }) it("should be able to update an builder user to a basic user", async () => { const user = await config.createUser(structures.users.builderUser()) jest.clearAllMocks() user.builder!.global = false await config.api.users.saveUser(user) expect(events.user.created).not.toHaveBeenCalled() expect(events.user.updated).toHaveBeenCalledTimes(1) expect(events.user.permissionBuilderRemoved).toHaveBeenCalledTimes(1) expect(events.user.permissionAdminRemoved).not.toHaveBeenCalled() }) it("should be able to assign app roles", async () => { const user = await config.createUser() jest.clearAllMocks() user.roles = { app_123: "role1", app_456: "role2", } await config.api.users.saveUser(user) const savedUser = await config.getUser(user.email) expect(events.user.created).not.toHaveBeenCalled() expect(events.user.updated).toHaveBeenCalledTimes(1) expect(events.role.assigned).toHaveBeenCalledTimes(2) expect(events.role.assigned).toHaveBeenCalledWith(savedUser, "role1") expect(events.role.assigned).toHaveBeenCalledWith(savedUser, "role2") }) it("should be able to unassign app roles", async () => { let user = structures.users.user() user.roles = { app_123: "role1", app_456: "role2", } user = await config.createUser(user) jest.clearAllMocks() user.roles = {} await config.api.users.saveUser(user) const savedUser = await config.getUser(user.email) expect(events.user.created).not.toHaveBeenCalled() expect(events.user.updated).toHaveBeenCalledTimes(1) expect(events.role.unassigned).toHaveBeenCalledTimes(2) expect(events.role.unassigned).toHaveBeenCalledWith(savedUser, "role1") expect(events.role.unassigned).toHaveBeenCalledWith(savedUser, "role2") }) it("should be able to update existing app roles", async () => { let user = structures.users.user() user.roles = { app_123: "role1", app_456: "role2", } user = await config.createUser(user) jest.clearAllMocks() user.roles = { app_123: "role1", app_456: "role2-edit", } await config.api.users.saveUser(user) const savedUser = await config.getUser(user.email) expect(events.user.created).not.toHaveBeenCalled() expect(events.user.updated).toHaveBeenCalledTimes(1) expect(events.role.unassigned).toHaveBeenCalledTimes(1) expect(events.role.unassigned).toHaveBeenCalledWith(savedUser, "role2") expect(events.role.assigned).toHaveBeenCalledTimes(1) expect(events.role.assigned).toHaveBeenCalledWith(savedUser, "role2-edit") }) it("should not be able to update email address", async () => { const email = structures.email() const user = await config.createUser(structures.users.user({ email })) user.email = "new@example.com" const response = await config.api.users.saveUser(user, 400) const dbUser = await config.getUser(email) user.email = email expect(user).toStrictEqual(dbUser) expect(response.body.message).toBe("Email address cannot be changed") }) it("should not allow a builder users to update an existing user", async () => { const existingUser = await config.createUser(structures.users.user()) const builderUser = await config.createUser( structures.users.builderUser() ) await config.createSession(builderUser) await config.api.users.saveUser( existingUser, 403, config.authHeaders(builderUser) ) }) describe("sso users", () => { function createSSOUser() { return config.doInTenant(() => { const user = structures.users.ssoUser() return userSdk.db.save(user, { requirePassword: false }) }) } function createPasswordUser() { return config.doInTenant(() => { const user = structures.users.user() return userSdk.db.save(user) }) } it("should be able to update an sso user that has no password", async () => { const user = await createSSOUser() await config.api.users.saveUser(user) }) it("sso support couldn't be used by admin. It is cloud restricted and needs internal key", async () => { const user = await config.createUser() const ssoId = "fake-ssoId" await config.api.users .addSsoSupportDefaultAuth(ssoId, user.email) .expect("Content-Type", /json/) .expect(403) }) it("if user email doesn't exist, SSO support couldn't be added. Not found error returned", async () => { const ssoId = "fake-ssoId" const email = "fake-email@budibase.com" await config.api.users .addSsoSupportInternalAPIAuth(ssoId, email) .expect("Content-Type", /json/) .expect(404) }) it("if user email exist, SSO support is added", async () => { const user = await createPasswordUser() const ssoId = "fakessoId" await config.api.users .addSsoSupportInternalAPIAuth(ssoId, user.email) .expect(200) }) it("if user ssoId is already assigned, no change will be applied", async () => { const user = await createSSOUser() user.ssoId = "testssoId" await config.api.users .addSsoSupportInternalAPIAuth(user.ssoId, user.email) .expect(200) }) }) }) describe("POST /api/global/users/bulk (delete)", () => { it("should not be able to bulk delete current user", async () => { const user = config.user! const response = await config.api.users.bulkDeleteUsers( [ { userId: user._id!, email: "test@example.com", }, ], 400 ) expect(response.message).toBe("Unable to delete self.") expect(events.user.deleted).not.toHaveBeenCalled() }) }) describe("POST /api/global/users/search", () => { it("should be able to search by email", async () => { const user = await config.createUser() const response = await config.api.users.searchUsers({ query: { string: { email: user.email } }, }) expect(response.body.data.length).toBe(1) expect(response.body.data[0].email).toBe(user.email) }) it("should support fuzzy email fragments", async () => { const email = structures.users.newEmail() await config.createUser({ email }) const fragment = email.slice(3, 12) const response = await config.api.users.searchUsers({ query: { fuzzy: { email: fragment } }, }) expect(response.body.data.length).toBe(1) expect(response.body.data[0].email).toBe(email) }) it("should be able to search by email with numeric prefixing", async () => { const user = await config.createUser() const response = await config.api.users.searchUsers({ query: { string: { ["999:email"]: user.email } }, }) expect(response.body.data.length).toBe(1) expect(response.body.data[0].email).toBe(user.email) }) it("should be able to search by _id", async () => { const user = await config.createUser() const response = await config.api.users.searchUsers({ query: { equal: { _id: user._id } }, }) expect(response.body.data.length).toBe(1) expect(response.body.data[0]._id).toBe(user._id) }) it("should be able to search by oneOf _id", async () => { const [user, user2, user3] = await Promise.all([ config.createUser(), config.createUser(), config.createUser(), ]) const response = await config.api.users.searchUsers({ query: { oneOf: { _id: [user._id, user2._id] } }, }) expect(response.body.data.length).toBe(2) const foundUserIds = response.body.data.map((user: User) => user._id) expect(foundUserIds).toContain(user._id) expect(foundUserIds).toContain(user2._id) expect( response.body.data.find((user: User) => user._id === user3._id) ).toBeUndefined() }) it("should be able to search by _id with numeric prefixing", async () => { const user = await config.createUser() const response = await config.api.users.searchUsers({ query: { equal: { ["1:_id"]: user._id } }, }) expect(response.body.data.length).toBe(1) expect(response.body.data[0]._id).toBe(user._id) }) it("should throw an error when using multiple filters on the same field", async () => { const user = await config.createUser() await config.api.users.searchUsers( { query: { string: { ["1:email"]: user.email, ["2:email"]: "something else", }, }, }, { status: 400 } ) }) it("should throw an error when using multiple filters on the same field without prefixes", async () => { const user = await config.createUser() await config.api.users.searchUsers( { query: { string: { ["_id"]: user.email, ["999:_id"]: "something else", }, }, }, { status: 400 } ) }) it("should throw an error when unimplemented options used", async () => { const user = await config.createUser() await config.api.users.searchUsers( { query: { equal: { firstName: user.firstName } }, }, { status: 400 } ) }) it("should throw an error if public query performed", async () => { await config.api.users.searchUsers({}, { status: 403, noHeaders: true }) }) it("should be able to search using logical conditions", async () => { const user = await config.createUser() const response = await config.api.users.searchUsers({ query: { $and: { conditions: [ { $and: { conditions: [{ string: { email: user.email } }], }, }, ], }, }, }) expect(response.body.data.length).toBe(1) expect(response.body.data[0].email).toBe(user.email) }) it("should strip users if accessing as an end user", async () => { const user = await config.createUser({ admin: { global: false }, builder: { global: false }, }) const response = await config.api.users.searchUsers( { query: {}, }, { useHeaders: await config.login(user) } ) for (let user of response.body.data) { expect(user.roles).toBeUndefined() expect(user.builder).toBeUndefined() expect(user.admin).toBeUndefined() } }) }) describe("POST /api/global/users/:userId/permission/:role", () => { it("should fail to assign CREATOR role when feature is not enabled", async () => { const user = await config.createUser() const workspaceId = "app_123456789" const res = await config.withApp(workspaceId, () => config.api.users.addUserToWorkspace( user._id!, user._rev!, "CREATOR", 400 ) ) expect(res.body.message).toBe("Feature not enabled, please check license") }) it("should assign CREATOR role and set builder properties", async () => { featureMocks.licenses.useAppBuilders() const user = await config.createUser() const workspaceId = "app_123456789" await config.withApp(workspaceId, () => config.api.users.addUserToWorkspace(user._id!, user._rev!, "CREATOR") ) const updatedUser = await config.getUser(user.email) expect(updatedUser.roles[workspaceId]).toBe("CREATOR") expect(updatedUser.builder?.creator).toBe(true) expect(updatedUser.builder?.apps).toEqual([workspaceId]) }) it("should keep builder creator flag when assigning non-CREATOR roles", async () => { const builderUser = await config.createUser({ builder: { creator: true, }, }) const workspaceId = "app_creator_preserve" const res = await config.withApp(workspaceId, () => config.api.users.addUserToWorkspace( builderUser._id!, builderUser._rev!, "BASIC" ) ) builderUser._rev = res.body._rev const updatedUser = await config.getUser(builderUser.email) expect(updatedUser.roles[workspaceId]).toBe("BASIC") expect(updatedUser.builder?.creator).toBe(true) }) it("should keep builder creator flag when assigning CREATOR roles", async () => { featureMocks.licenses.useAppBuilders() const builderUser = await config.createUser({ builder: { creator: true, }, }) const workspaceId = "app_creator_preserve" const res = await config.withApp(workspaceId, () => config.api.users.addUserToWorkspace( builderUser._id!, builderUser._rev!, "CREATOR" ) ) builderUser._rev = res.body._rev const updatedUser = await config.getUser(builderUser.email) expect(updatedUser.roles[workspaceId]).toBe("CREATOR") expect(updatedUser.builder?.creator).toBe(true) }) it("should maintain builder properties when user has multiple CREATOR roles", async () => { mocks.licenses.useAppBuilders() const user = await config.createUser() const workspaceId1 = "app_111111111" const workspaceId2 = "app_222222222" // Assign CREATOR role to two workspaces let res = await config.withApp(workspaceId1, () => config.api.users.addUserToWorkspace(user._id!, user._rev!, "CREATOR") ) user._rev = res.body._rev res = await config.withApp(workspaceId2, () => config.api.users.addUserToWorkspace(user._id!, user._rev!, "CREATOR") ) user._rev = res.body._rev let updatedUser = await config.getUser(user.email) expect(updatedUser.roles[workspaceId1]).toBe("CREATOR") expect(updatedUser.roles[workspaceId2]).toBe("CREATOR") expect(updatedUser.builder?.creator).toBe(true) expect(updatedUser.builder?.apps).toEqual( expect.arrayContaining([workspaceId1, workspaceId2]) ) // Remove from one workspace await config.withApp(workspaceId1, () => config.api.users.removeUserFromWorkspace(user._id!, user._rev!) ) updatedUser = await config.getUser(user.email) expect(updatedUser.roles[workspaceId1]).toBeUndefined() expect(updatedUser.roles[workspaceId2]).toBe("CREATOR") expect(updatedUser.builder?.creator).toBe(true) expect(updatedUser.builder?.apps).toEqual([workspaceId2]) }) it("should handle non-CREATOR role assignments without affecting builder properties", async () => { const user = await config.createUser() const workspaceId = "app_123456789" const res = await config.withApp(workspaceId, () => config.api.users.addUserToWorkspace(user._id!, user._rev!, "BASIC") ) expect(res.body._id).toBe(user._id) const updatedUser = await config.getUser(user.email) expect(updatedUser.roles[workspaceId]).toBe("BASIC") expect(updatedUser.builder?.creator).toBeUndefined() expect(updatedUser.builder?.apps).toBeUndefined() }) it("should not allow non-admin users to modify workspace permissions", async () => { const regularUser = await config.createUser() const targetUser = await config.createUser() const workspaceId = "app_123456789" await config.login(regularUser) const res = await config.withUser(regularUser, () => config.withApp(workspaceId, () => config.api.users.addUserToWorkspace( targetUser._id!, targetUser._rev!, "CREATOR", 403 ) ) ) expect(res.body.message).toBe( "Workspace Admin/Builder user only endpoint." ) }) }) describe("DELETE /api/global/users/:userId", () => { it("should be able to destroy a basic user", async () => { const user = await config.createUser() jest.clearAllMocks() await config.api.users.deleteUser(user._id!) expect(events.user.deleted).toHaveBeenCalledTimes(1) expect(events.user.permissionBuilderRemoved).not.toHaveBeenCalled() expect(events.user.permissionAdminRemoved).not.toHaveBeenCalled() }) it("should be able to destroy an admin user", async () => { const user = await config.createUser(structures.users.adminUser()) jest.clearAllMocks() await config.api.users.deleteUser(user._id!) expect(events.user.deleted).toHaveBeenCalledTimes(1) expect(events.user.permissionBuilderRemoved).toHaveBeenCalledTimes(1) expect(events.user.permissionAdminRemoved).toHaveBeenCalledTimes(1) }) it("should be able to destroy a builder user", async () => { const user = await config.createUser(structures.users.builderUser()) jest.clearAllMocks() await config.api.users.deleteUser(user._id!) expect(events.user.deleted).toHaveBeenCalledTimes(1) expect(events.user.permissionBuilderRemoved).toHaveBeenCalledTimes(1) expect(events.user.permissionAdminRemoved).not.toHaveBeenCalled() }) it("should not be able to destroy account owner", async () => { const user = await config.createUser() const account = structures.accounts.cloudAccount() accounts.getAccount.mockReturnValueOnce(Promise.resolve(account)) const response = await config.api.users.deleteUser(user._id!, 400) expect(response.body.message).toBe("Account holder cannot be deleted") }) it("should not be able to destroy account owner as account owner", async () => { const user = await config.user! const account = structures.accounts.cloudAccount() account.email = user.email accounts.getAccount.mockReturnValueOnce(Promise.resolve(account)) const response = await config.api.users.deleteUser(user._id!, 400) expect(response.body.message).toBe("Unable to delete self.") }) }) describe("POST /api/global/users/onboard", () => { it("should successfully onboard a user", async () => { const response = await config.api.users.onboardUser([ { email: structures.users.newEmail(), userInfo: {} }, ]) expect(response.successful.length).toBe(1) expect(response.unsuccessful.length).toBe(0) }) it("should not onboard a user who has been invited", async () => { const email = structures.users.newEmail() await config.api.users.sendUserInvite(sendMailMock, email) const response = await config.api.users.onboardUser([ { email, userInfo: {} }, ]) expect(response.successful.length).toBe(0) expect(response.unsuccessful.length).toBe(1) }) }) describe("PUT /api/global/users/tenant/owner", () => { it("should successfully change tenant owner email for existing user", async () => { const originalEmail = `original-${structures.uuid()}@example.com` const newEmail = `new-${structures.uuid()}@example.com` const tenantId = config.getTenantId() const user = await config.doInTenant(async () => { return await userSdk.db.save( { email: originalEmail, tenantId, } as any, { requirePassword: false, isAccountHolder: true } ) }) await config.api.users.changeTenantOwnerEmail(newEmail, originalEmail, [ tenantId, ]) const updatedUser = await config.doInTenant(async () => { return await userSdk.db.getUser(user._id!) }) expect(updatedUser).toBeDefined() expect(updatedUser!.email).toBe(newEmail) }) it("should handle multiple tenants", async () => { const originalEmail = `original-${structures.uuid()}@example.com` const newEmail = `new-${structures.uuid()}@example.com` const tenant1 = config.getTenantId() const tenant2 = structures.tenant.id() const user1 = await config.doInTenant(async () => { return await userSdk.db.save( { email: originalEmail, tenantId: tenant1, } as any, { requirePassword: false, isAccountHolder: true } ) }) const user2 = await config.doInSpecificTenant(tenant2, async () => { return await userSdk.db.save( { email: originalEmail, tenantId: tenant2, } as any, { requirePassword: false, isAccountHolder: true } ) }) await config.api.users.changeTenantOwnerEmail(newEmail, originalEmail, [ tenant1, tenant2, ]) const updatedUser1 = await config.doInTenant(async () => { return await userSdk.db.getUser(user1._id!) }) const updatedUser2 = await config.doInSpecificTenant( tenant2, async () => { return await userSdk.db.getUser(user2._id!) } ) expect(updatedUser1).toBeDefined() expect(updatedUser1!.email).toBe(newEmail) expect(updatedUser2).toBeDefined() expect(updatedUser2!.email).toBe(newEmail) }) it("should not fail if user doesn't exist in tenant", async () => { const originalEmail = `nonexistent-${structures.uuid()}@example.com` const newEmail = `new-${structures.uuid()}@example.com` const tenantId = config.getTenantId() await config.api.users.changeTenantOwnerEmail(newEmail, originalEmail, [ tenantId, ]) const user = await config.doInTenant(async () => { return await userSdk.db.getUserByEmail(newEmail) }) expect(user).toBeUndefined() }) it("should handle empty tenant list", async () => { const originalEmail = `original-${structures.uuid()}@example.com` const newEmail = `new-${structures.uuid()}@example.com` await config.api.users.changeTenantOwnerEmail(newEmail, originalEmail, []) }) it("should clear all OIDC-related fields", async () => { const originalEmail = `original-${structures.uuid()}@example.com` const newEmail = `new-${structures.uuid()}@example.com` const tenantId = config.getTenantId() const profile = {} const provider = "oidc" const providerType = "oidc" const thirdPartyProfile = {} const oauth2 = {} await config.doInTenant(async () => { await userSdk.db.save( { email: originalEmail, tenantId, profile, provider, providerType, thirdPartyProfile, oauth2, } as any, { requirePassword: false, isAccountHolder: true } ) }) await config.api.users.changeTenantOwnerEmail(newEmail, originalEmail, [ tenantId, ]) const updatedUser = (await config.doInTenant(async () => { return await userSdk.db.getUserByEmail(newEmail) })) as OIDCUser expect(updatedUser).toBeDefined() expect(updatedUser!.email).toBe(newEmail) expect(updatedUser.profile).toBe(undefined) expect(updatedUser.provider).toBe(undefined) expect(updatedUser.providerType).toBe(undefined) expect(updatedUser.thirdPartyProfile).toBe(undefined) expect(updatedUser.oauth2).toBe(undefined) }) it("should require internal API headers", async () => { const originalEmail = `original-${structures.uuid()}@example.com` const newEmail = `new-${structures.uuid()}@example.com` const tenantId = config.getTenantId() await config.request .put(`/api/global/users/tenant/owner`) .send({ newAccountEmail: newEmail, originalEmail, tenantIds: [tenantId], }) .set(config.defaultHeaders()) .expect(403) }) }) })