forked from tsai/budibase
committed by
GitHub
78 changed files with 2154 additions and 1156 deletions
@ -0,0 +1,7 @@ |
|||||
|
export const getAccount = jest.fn() |
||||
|
export const getAccountByTenantId = jest.fn() |
||||
|
|
||||
|
jest.mock("../../../src/cloud/accounts", () => ({ |
||||
|
getAccount, |
||||
|
getAccountByTenantId, |
||||
|
})) |
||||
@ -1,2 +0,0 @@ |
|||||
exports.MOCK_DATE = new Date("2020-01-01T00:00:00.000Z") |
|
||||
exports.MOCK_DATE_TIMESTAMP = 1577836800000 |
|
||||
@ -0,0 +1,2 @@ |
|||||
|
export const MOCK_DATE = new Date("2020-01-01T00:00:00.000Z") |
||||
|
export const MOCK_DATE_TIMESTAMP = 1577836800000 |
||||
@ -1,9 +0,0 @@ |
|||||
const posthog = require("./posthog") |
|
||||
const events = require("./events") |
|
||||
const date = require("./date") |
|
||||
|
|
||||
module.exports = { |
|
||||
posthog, |
|
||||
date, |
|
||||
events, |
|
||||
} |
|
||||
@ -0,0 +1,4 @@ |
|||||
|
import "./posthog" |
||||
|
import "./events" |
||||
|
export * as accounts from "./accounts" |
||||
|
export * as date from "./date" |
||||
@ -0,0 +1,73 @@ |
|||||
|
<script> |
||||
|
import { Body, ModalContent, Table } from "@budibase/bbui" |
||||
|
import { onMount } from "svelte" |
||||
|
|
||||
|
export let userData |
||||
|
export let deleteUsersResponse |
||||
|
|
||||
|
let successCount |
||||
|
let failureCount |
||||
|
let title |
||||
|
let unsuccessfulUsers |
||||
|
let message |
||||
|
|
||||
|
const setTitle = () => { |
||||
|
if (successCount) { |
||||
|
title = `${successCount} users deleted` |
||||
|
} else { |
||||
|
title = "Oops!" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const setMessage = () => { |
||||
|
if (successCount) { |
||||
|
message = "However there was a problem deleting some users." |
||||
|
} else { |
||||
|
message = "There was a problem deleting some users." |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const setUsers = () => { |
||||
|
unsuccessfulUsers = deleteUsersResponse.unsuccessful.map(user => { |
||||
|
return { |
||||
|
email: user.email, |
||||
|
reason: user.reason, |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
onMount(() => { |
||||
|
successCount = deleteUsersResponse.successful.length |
||||
|
failureCount = deleteUsersResponse.unsuccessful.length |
||||
|
setTitle() |
||||
|
setMessage() |
||||
|
setUsers() |
||||
|
}) |
||||
|
|
||||
|
const schema = { |
||||
|
email: {}, |
||||
|
reason: {}, |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<ModalContent |
||||
|
size="M" |
||||
|
{title} |
||||
|
confirmText="Close" |
||||
|
showCloseIcon={false} |
||||
|
showCancelButton={false} |
||||
|
> |
||||
|
<Body size="XS"> |
||||
|
{message} |
||||
|
</Body> |
||||
|
<Table |
||||
|
{schema} |
||||
|
data={unsuccessfulUsers} |
||||
|
allowEditColumns={false} |
||||
|
allowEditRows={false} |
||||
|
allowSelectRows={false} |
||||
|
/> |
||||
|
</ModalContent> |
||||
|
|
||||
|
<style> |
||||
|
</style> |
||||
@ -0,0 +1,5 @@ |
|||||
|
export interface APIError { |
||||
|
message: string |
||||
|
status: number |
||||
|
error?: any |
||||
|
} |
||||
@ -1 +1,3 @@ |
|||||
export * from "./analytics" |
export * from "./analytics" |
||||
|
export * from "./user" |
||||
|
export * from "./errors" |
||||
|
|||||
@ -0,0 +1,31 @@ |
|||||
|
import { User } from "../../documents" |
||||
|
|
||||
|
export interface CreateUserResponse { |
||||
|
_id: string |
||||
|
_rev: string |
||||
|
email: string |
||||
|
} |
||||
|
|
||||
|
export interface BulkCreateUsersRequest { |
||||
|
users: User[] |
||||
|
groups: any[] |
||||
|
} |
||||
|
|
||||
|
export interface UserDetails { |
||||
|
_id: string |
||||
|
email: string |
||||
|
} |
||||
|
|
||||
|
export interface BulkCreateUsersResponse { |
||||
|
successful: UserDetails[] |
||||
|
unsuccessful: { email: string; reason: string }[] |
||||
|
} |
||||
|
|
||||
|
export interface BulkDeleteUsersRequest { |
||||
|
userIds: string[] |
||||
|
} |
||||
|
|
||||
|
export interface BulkDeleteUsersResponse { |
||||
|
successful: UserDetails[] |
||||
|
unsuccessful: { _id: string; email: string; reason: string }[] |
||||
|
} |
||||
@ -1,19 +1,20 @@ |
|||||
import { Document } from "../document" |
import { Document } from "../document" |
||||
import { User } from "./user" |
|
||||
export interface UserGroup extends Document { |
export interface UserGroup extends Document { |
||||
name: string |
name: string |
||||
icon: string |
icon: string |
||||
color: string |
color: string |
||||
users: groupUser[] |
users: GroupUser[] |
||||
apps: string[] |
apps: string[] |
||||
roles: UserGroupRoles |
roles: UserGroupRoles |
||||
createdAt?: number |
createdAt?: number |
||||
} |
} |
||||
|
|
||||
export interface groupUser { |
export interface GroupUser { |
||||
_id: string |
_id: string |
||||
email: string[] |
email: string |
||||
} |
} |
||||
|
|
||||
export interface UserGroupRoles { |
export interface UserGroupRoles { |
||||
[key: string]: string |
[key: string]: string |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,5 @@ |
|||||
|
import { Document } from "../document" |
||||
|
|
||||
|
export interface AccountMetadata extends Document { |
||||
|
email: string |
||||
|
} |
||||
@ -1 +1,3 @@ |
|||||
export * from "./info" |
export * from "./info" |
||||
|
export * from "./users" |
||||
|
export * from "./accounts" |
||||
|
|||||
@ -0,0 +1,9 @@ |
|||||
|
import { Document } from "../document" |
||||
|
|
||||
|
/** |
||||
|
* doc id is user email |
||||
|
*/ |
||||
|
export interface PlatformUserByEmail extends Document { |
||||
|
tenantId: string |
||||
|
userId: string |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
export interface RowValue { |
||||
|
rev: string |
||||
|
deleted: boolean |
||||
|
} |
||||
|
|
||||
|
export interface RowResponse<T> { |
||||
|
id: string |
||||
|
key: string |
||||
|
error: string |
||||
|
value: RowValue |
||||
|
doc: T |
||||
|
} |
||||
|
|
||||
|
export interface AllDocsResponse<T> { |
||||
|
offset: number |
||||
|
total_rows: number |
||||
|
rows: RowResponse<T>[] |
||||
|
} |
||||
|
|
||||
|
export type BulkDocsResponse = BulkDocResponse[] |
||||
|
|
||||
|
interface BulkDocResponse { |
||||
|
ok: boolean |
||||
|
id: string |
||||
|
rev: string |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
export interface AuthToken { |
||||
|
userId: string |
||||
|
tenantId: string |
||||
|
sessionId: string |
||||
|
} |
||||
@ -1,97 +0,0 @@ |
|||||
// get the JWT secret etc
|
|
||||
require("../../src/environment") |
|
||||
require("@budibase/backend-core").init() |
|
||||
const { |
|
||||
getProdAppID, |
|
||||
generateGlobalUserID, |
|
||||
} = require("@budibase/backend-core/db") |
|
||||
const { doInTenant, getGlobalDB } = require("@budibase/backend-core/tenancy") |
|
||||
const users = require("../../src/sdk/users") |
|
||||
const { publicApiUserFix } = require("../../src/utilities/users") |
|
||||
const { hash } = require("@budibase/backend-core/utils") |
|
||||
|
|
||||
const USER_LOAD_NUMBER = 10000 |
|
||||
const BATCH_SIZE = 200 |
|
||||
const PASSWORD = "test" |
|
||||
const TENANT_ID = "default" |
|
||||
|
|
||||
const APP_ID = process.argv[2] |
|
||||
|
|
||||
const words = [ |
|
||||
"test", |
|
||||
"testing", |
|
||||
"budi", |
|
||||
"mail", |
|
||||
"age", |
|
||||
"risk", |
|
||||
"load", |
|
||||
"uno", |
|
||||
"arm", |
|
||||
"leg", |
|
||||
"pen", |
|
||||
"glass", |
|
||||
"box", |
|
||||
"chicken", |
|
||||
"bottle", |
|
||||
] |
|
||||
|
|
||||
if (!APP_ID) { |
|
||||
console.error("Must supply app ID as first CLI option!") |
|
||||
process.exit(-1) |
|
||||
} |
|
||||
|
|
||||
const WORD_1 = words[Math.floor(Math.random() * words.length)] |
|
||||
const WORD_2 = words[Math.floor(Math.random() * words.length)] |
|
||||
let HASHED_PASSWORD |
|
||||
|
|
||||
function generateUser(count) { |
|
||||
return { |
|
||||
_id: generateGlobalUserID(), |
|
||||
password: HASHED_PASSWORD, |
|
||||
email: `${WORD_1}${count}@${WORD_2}.com`, |
|
||||
roles: { |
|
||||
[getProdAppID(APP_ID)]: "BASIC", |
|
||||
}, |
|
||||
status: "active", |
|
||||
forceResetPassword: false, |
|
||||
firstName: "John", |
|
||||
lastName: "Smith", |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async function run() { |
|
||||
HASHED_PASSWORD = await hash(PASSWORD) |
|
||||
return doInTenant(TENANT_ID, async () => { |
|
||||
const db = getGlobalDB() |
|
||||
for (let i = 0; i < USER_LOAD_NUMBER; i += BATCH_SIZE) { |
|
||||
let userSavePromises = [] |
|
||||
for (let j = 0; j < BATCH_SIZE; j++) { |
|
||||
// like the public API
|
|
||||
const ctx = publicApiUserFix({ |
|
||||
request: { |
|
||||
body: generateUser(i + j), |
|
||||
}, |
|
||||
}) |
|
||||
userSavePromises.push( |
|
||||
users.save(ctx.request.body, { |
|
||||
hashPassword: false, |
|
||||
requirePassword: true, |
|
||||
bulkCreate: true, |
|
||||
}) |
|
||||
) |
|
||||
} |
|
||||
const allUsers = await Promise.all(userSavePromises) |
|
||||
await db.bulkDocs(allUsers) |
|
||||
console.log(`${i + BATCH_SIZE} users have been created.`) |
|
||||
} |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
run() |
|
||||
.then(() => { |
|
||||
console.log(`Generated ${USER_LOAD_NUMBER} users!`) |
|
||||
}) |
|
||||
.catch(err => { |
|
||||
console.error("Failed for reason: ", err) |
|
||||
process.exit(-1) |
|
||||
}) |
|
||||
@ -0,0 +1,21 @@ |
|||||
|
import { Account, AccountMetadata } from "@budibase/types" |
||||
|
import { accounts } from "../../../sdk" |
||||
|
|
||||
|
export const save = async (ctx: any) => { |
||||
|
const account = ctx.request.body as Account |
||||
|
let metadata: AccountMetadata = { |
||||
|
_id: accounts.formatAccountMetadataId(account.accountId), |
||||
|
email: account.email, |
||||
|
} |
||||
|
|
||||
|
metadata = await accounts.saveMetadata(metadata) |
||||
|
|
||||
|
ctx.body = metadata |
||||
|
ctx.status = 200 |
||||
|
} |
||||
|
|
||||
|
export const destroy = async (ctx: any) => { |
||||
|
const accountId = accounts.formatAccountMetadataId(ctx.params.accountId) |
||||
|
await accounts.destroyMetadata(accountId) |
||||
|
ctx.status = 204 |
||||
|
} |
||||
@ -0,0 +1,464 @@ |
|||||
|
jest.mock("nodemailer") |
||||
|
import { |
||||
|
TestConfiguration, |
||||
|
mocks, |
||||
|
structures, |
||||
|
TENANT_1, |
||||
|
API, |
||||
|
} from "../../../../tests" |
||||
|
const sendMailMock = mocks.email.mock() |
||||
|
import { events, tenancy } from "@budibase/backend-core" |
||||
|
|
||||
|
describe("/api/global/users", () => { |
||||
|
const config = new TestConfiguration() |
||||
|
const api = new API(config) |
||||
|
|
||||
|
beforeAll(async () => { |
||||
|
await config.beforeAll() |
||||
|
}) |
||||
|
|
||||
|
afterAll(async () => { |
||||
|
await config.afterAll() |
||||
|
}) |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
jest.clearAllMocks() |
||||
|
}) |
||||
|
|
||||
|
describe("invite", () => { |
||||
|
it("should be able to generate an invitation", async () => { |
||||
|
const { code, res } = await api.users.sendUserInvite(sendMailMock) |
||||
|
|
||||
|
expect(res.body).toEqual({ message: "Invitation has been sent." }) |
||||
|
expect(sendMailMock).toHaveBeenCalled() |
||||
|
expect(code).toBeDefined() |
||||
|
expect(events.user.invited).toBeCalledTimes(1) |
||||
|
}) |
||||
|
|
||||
|
it("should be able to create new user from invite", async () => { |
||||
|
const { code } = await api.users.sendUserInvite(sendMailMock) |
||||
|
|
||||
|
const res = await api.users.acceptInvite(code) |
||||
|
|
||||
|
expect(res.body._id).toBeDefined() |
||||
|
const user = await config.getUser("invite@test.com") |
||||
|
expect(user).toBeDefined() |
||||
|
expect(user._id).toEqual(res.body._id) |
||||
|
expect(events.user.inviteAccepted).toBeCalledTimes(1) |
||||
|
expect(events.user.inviteAccepted).toBeCalledWith(user) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
describe("bulkCreate", () => { |
||||
|
it("should ignore users existing in the same tenant", async () => { |
||||
|
const user = await config.createUser() |
||||
|
jest.clearAllMocks() |
||||
|
|
||||
|
const response = await api.users.bulkCreateUsers([user]) |
||||
|
|
||||
|
expect(response.successful.length).toBe(0) |
||||
|
expect(response.unsuccessful.length).toBe(1) |
||||
|
expect(response.unsuccessful[0].email).toBe(user.email) |
||||
|
expect(events.user.created).toBeCalledTimes(0) |
||||
|
}) |
||||
|
|
||||
|
it("should ignore users existing in other tenants", async () => { |
||||
|
const user = await config.createUser() |
||||
|
jest.resetAllMocks() |
||||
|
|
||||
|
await tenancy.doInTenant(TENANT_1, async () => { |
||||
|
const response = await api.users.bulkCreateUsers([user]) |
||||
|
|
||||
|
expect(response.successful.length).toBe(0) |
||||
|
expect(response.unsuccessful.length).toBe(1) |
||||
|
expect(response.unsuccessful[0].email).toBe(user.email) |
||||
|
expect(events.user.created).toBeCalledTimes(0) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should ignore accounts using the same email", async () => { |
||||
|
const account = structures.accounts.account() |
||||
|
const resp = await api.accounts.saveMetadata(account) |
||||
|
const user = structures.users.user({ email: resp.email }) |
||||
|
jest.clearAllMocks() |
||||
|
|
||||
|
const response = await api.users.bulkCreateUsers([user]) |
||||
|
|
||||
|
expect(response.successful.length).toBe(0) |
||||
|
expect(response.unsuccessful.length).toBe(1) |
||||
|
expect(response.unsuccessful[0].email).toBe(user.email) |
||||
|
expect(events.user.created).toBeCalledTimes(0) |
||||
|
}) |
||||
|
|
||||
|
it("should be able to bulkCreate users", async () => { |
||||
|
const builder = structures.users.builderUser() |
||||
|
const admin = structures.users.adminUser() |
||||
|
const user = structures.users.user() |
||||
|
|
||||
|
const response = await api.users.bulkCreateUsers([builder, admin, user]) |
||||
|
|
||||
|
expect(response.successful.length).toBe(3) |
||||
|
expect(response.successful[0].email).toBe(builder.email) |
||||
|
expect(response.successful[1].email).toBe(admin.email) |
||||
|
expect(response.successful[2].email).toBe(user.email) |
||||
|
expect(response.unsuccessful.length).toBe(0) |
||||
|
expect(events.user.created).toBeCalledTimes(3) |
||||
|
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(2) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
describe("create", () => { |
||||
|
it("should be able to create a basic user", async () => { |
||||
|
const user = structures.users.user() |
||||
|
|
||||
|
await api.users.saveUser(user) |
||||
|
|
||||
|
expect(events.user.created).toBeCalledTimes(1) |
||||
|
expect(events.user.updated).not.toBeCalled() |
||||
|
expect(events.user.permissionBuilderAssigned).not.toBeCalled() |
||||
|
expect(events.user.permissionAdminAssigned).not.toBeCalled() |
||||
|
}) |
||||
|
|
||||
|
it("should be able to create an admin user", async () => { |
||||
|
const user = structures.users.adminUser() |
||||
|
|
||||
|
await api.users.saveUser(user) |
||||
|
|
||||
|
expect(events.user.created).toBeCalledTimes(1) |
||||
|
expect(events.user.updated).not.toBeCalled() |
||||
|
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1) |
||||
|
}) |
||||
|
|
||||
|
it("should be able to create a builder user", async () => { |
||||
|
const user = structures.users.builderUser() |
||||
|
|
||||
|
await api.users.saveUser(user) |
||||
|
|
||||
|
expect(events.user.created).toBeCalledTimes(1) |
||||
|
expect(events.user.updated).not.toBeCalled() |
||||
|
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionAdminAssigned).not.toBeCalled() |
||||
|
}) |
||||
|
|
||||
|
it("should be able to assign app roles", async () => { |
||||
|
const user = structures.users.user() |
||||
|
user.roles = { |
||||
|
app_123: "role1", |
||||
|
app_456: "role2", |
||||
|
} |
||||
|
|
||||
|
await api.users.saveUser(user) |
||||
|
|
||||
|
const savedUser = await config.getUser(user.email) |
||||
|
expect(events.user.created).toBeCalledTimes(1) |
||||
|
expect(events.user.updated).not.toBeCalled() |
||||
|
expect(events.role.assigned).toBeCalledTimes(2) |
||||
|
expect(events.role.assigned).toBeCalledWith(savedUser, "role1") |
||||
|
expect(events.role.assigned).toBeCalledWith(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 api.users.saveUser(user, 400) |
||||
|
|
||||
|
expect(response.body.message).toBe(`Unavailable`) |
||||
|
expect(events.user.created).toBeCalledTimes(0) |
||||
|
}) |
||||
|
|
||||
|
it("should not be able to create user that exists in other tenant", async () => { |
||||
|
const user = await config.createUser() |
||||
|
jest.resetAllMocks() |
||||
|
|
||||
|
await tenancy.doInTenant(TENANT_1, async () => { |
||||
|
delete user._id |
||||
|
const response = await api.users.saveUser(user, 400) |
||||
|
|
||||
|
expect(response.body.message).toBe(`Unavailable`) |
||||
|
expect(events.user.created).toBeCalledTimes(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() |
||||
|
mocks.accounts.getAccount.mockReturnValueOnce(account) |
||||
|
|
||||
|
const response = await api.users.saveUser(user, 400) |
||||
|
|
||||
|
expect(response.body.message).toBe(`Unavailable`) |
||||
|
expect(events.user.created).toBeCalledTimes(0) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
describe("update", () => { |
||||
|
it("should be able to update a basic user", async () => { |
||||
|
const user = await config.createUser() |
||||
|
jest.clearAllMocks() |
||||
|
|
||||
|
await api.users.saveUser(user) |
||||
|
|
||||
|
expect(events.user.created).not.toBeCalled() |
||||
|
expect(events.user.updated).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionBuilderAssigned).not.toBeCalled() |
||||
|
expect(events.user.permissionAdminAssigned).not.toBeCalled() |
||||
|
expect(events.user.passwordForceReset).not.toBeCalled() |
||||
|
}) |
||||
|
|
||||
|
it("should be able to force reset password", async () => { |
||||
|
const user = await config.createUser() |
||||
|
jest.clearAllMocks() |
||||
|
|
||||
|
user.forceResetPassword = true |
||||
|
user.password = "tempPassword" |
||||
|
await api.users.saveUser(user) |
||||
|
|
||||
|
expect(events.user.created).not.toBeCalled() |
||||
|
expect(events.user.updated).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionBuilderAssigned).not.toBeCalled() |
||||
|
expect(events.user.permissionAdminAssigned).not.toBeCalled() |
||||
|
expect(events.user.passwordForceReset).toBeCalledTimes(1) |
||||
|
}) |
||||
|
|
||||
|
it("should be able to update a basic user to an admin user", async () => { |
||||
|
const user = await config.createUser() |
||||
|
jest.clearAllMocks() |
||||
|
|
||||
|
await api.users.saveUser(structures.users.adminUser(user)) |
||||
|
|
||||
|
expect(events.user.created).not.toBeCalled() |
||||
|
expect(events.user.updated).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1) |
||||
|
}) |
||||
|
|
||||
|
it("should be able to update a basic user to a builder user", async () => { |
||||
|
const user = await config.createUser() |
||||
|
jest.clearAllMocks() |
||||
|
|
||||
|
await api.users.saveUser(structures.users.builderUser(user)) |
||||
|
|
||||
|
expect(events.user.created).not.toBeCalled() |
||||
|
expect(events.user.updated).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionAdminAssigned).not.toBeCalled() |
||||
|
}) |
||||
|
|
||||
|
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 api.users.saveUser(user) |
||||
|
|
||||
|
expect(events.user.created).not.toBeCalled() |
||||
|
expect(events.user.updated).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(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 api.users.saveUser(user) |
||||
|
|
||||
|
expect(events.user.created).not.toBeCalled() |
||||
|
expect(events.user.updated).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionAdminRemoved).not.toBeCalled() |
||||
|
}) |
||||
|
|
||||
|
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 api.users.saveUser(user) |
||||
|
|
||||
|
const savedUser = await config.getUser(user.email) |
||||
|
expect(events.user.created).not.toBeCalled() |
||||
|
expect(events.user.updated).toBeCalledTimes(1) |
||||
|
expect(events.role.assigned).toBeCalledTimes(2) |
||||
|
expect(events.role.assigned).toBeCalledWith(savedUser, "role1") |
||||
|
expect(events.role.assigned).toBeCalledWith(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 api.users.saveUser(user) |
||||
|
|
||||
|
const savedUser = await config.getUser(user.email) |
||||
|
expect(events.user.created).not.toBeCalled() |
||||
|
expect(events.user.updated).toBeCalledTimes(1) |
||||
|
expect(events.role.unassigned).toBeCalledTimes(2) |
||||
|
expect(events.role.unassigned).toBeCalledWith(savedUser, "role1") |
||||
|
expect(events.role.unassigned).toBeCalledWith(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 api.users.saveUser(user) |
||||
|
|
||||
|
const savedUser = await config.getUser(user.email) |
||||
|
expect(events.user.created).not.toBeCalled() |
||||
|
expect(events.user.updated).toBeCalledTimes(1) |
||||
|
expect(events.role.unassigned).toBeCalledTimes(1) |
||||
|
expect(events.role.unassigned).toBeCalledWith(savedUser, "role2") |
||||
|
expect(events.role.assigned).toBeCalledTimes(1) |
||||
|
expect(events.role.assigned).toBeCalledWith(savedUser, "role2-edit") |
||||
|
}) |
||||
|
|
||||
|
it("should not be able to update email address", async () => { |
||||
|
const email = "email@test.com" |
||||
|
const user = await config.createUser(structures.users.user({ email })) |
||||
|
user.email = "new@test.com" |
||||
|
|
||||
|
const response = await 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") |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
describe("bulkDelete", () => { |
||||
|
it("should not be able to bulkDelete current user", async () => { |
||||
|
const user = await config.defaultUser! |
||||
|
const request = { userIds: [user._id!] } |
||||
|
|
||||
|
const response = await api.users.bulkDeleteUsers(request, 400) |
||||
|
|
||||
|
expect(response.body.message).toBe("Unable to delete self.") |
||||
|
expect(events.user.deleted).not.toBeCalled() |
||||
|
}) |
||||
|
|
||||
|
it("should not be able to bulkDelete account owner", async () => { |
||||
|
const user = await config.createUser() |
||||
|
const account = structures.accounts.cloudAccount() |
||||
|
account.budibaseUserId = user._id! |
||||
|
mocks.accounts.getAccountByTenantId.mockReturnValue(account) |
||||
|
|
||||
|
const request = { userIds: [user._id!] } |
||||
|
|
||||
|
const response = await api.users.bulkDeleteUsers(request) |
||||
|
|
||||
|
expect(response.body.successful.length).toBe(0) |
||||
|
expect(response.body.unsuccessful.length).toBe(1) |
||||
|
expect(response.body.unsuccessful[0].reason).toBe( |
||||
|
"Account holder cannot be deleted" |
||||
|
) |
||||
|
expect(response.body.unsuccessful[0]._id).toBe(user._id) |
||||
|
expect(events.user.deleted).not.toBeCalled() |
||||
|
}) |
||||
|
|
||||
|
it("should be able to bulk delete users", async () => { |
||||
|
const account = structures.accounts.cloudAccount() |
||||
|
mocks.accounts.getAccountByTenantId.mockReturnValue(account) |
||||
|
|
||||
|
const builder = structures.users.builderUser() |
||||
|
const admin = structures.users.adminUser() |
||||
|
const user = structures.users.user() |
||||
|
const createdUsers = await api.users.bulkCreateUsers([ |
||||
|
builder, |
||||
|
admin, |
||||
|
user, |
||||
|
]) |
||||
|
const request = { userIds: createdUsers.successful.map(u => u._id!) } |
||||
|
|
||||
|
const response = await api.users.bulkDeleteUsers(request) |
||||
|
|
||||
|
expect(response.body.successful.length).toBe(3) |
||||
|
expect(response.body.unsuccessful.length).toBe(0) |
||||
|
expect(events.user.deleted).toBeCalledTimes(3) |
||||
|
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(2) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
describe("destroy", () => { |
||||
|
it("should be able to destroy a basic user", async () => { |
||||
|
const user = await config.createUser() |
||||
|
jest.clearAllMocks() |
||||
|
|
||||
|
await api.users.deleteUser(user._id!) |
||||
|
|
||||
|
expect(events.user.deleted).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionBuilderRemoved).not.toBeCalled() |
||||
|
expect(events.user.permissionAdminRemoved).not.toBeCalled() |
||||
|
}) |
||||
|
|
||||
|
it("should be able to destroy an admin user", async () => { |
||||
|
const user = await config.createUser(structures.users.adminUser()) |
||||
|
jest.clearAllMocks() |
||||
|
|
||||
|
await api.users.deleteUser(user._id!) |
||||
|
|
||||
|
expect(events.user.deleted).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1) |
||||
|
}) |
||||
|
|
||||
|
it("should be able to destroy a builder user", async () => { |
||||
|
const user = await config.createUser(structures.users.builderUser()) |
||||
|
jest.clearAllMocks() |
||||
|
|
||||
|
await api.users.deleteUser(user._id!) |
||||
|
|
||||
|
expect(events.user.deleted).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1) |
||||
|
expect(events.user.permissionAdminRemoved).not.toBeCalled() |
||||
|
}) |
||||
|
|
||||
|
it("should not be able to destroy account owner", async () => { |
||||
|
const user = await config.createUser() |
||||
|
const account = structures.accounts.cloudAccount() |
||||
|
mocks.accounts.getAccount.mockReturnValueOnce(account) |
||||
|
|
||||
|
const response = await 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.defaultUser! |
||||
|
const account = structures.accounts.cloudAccount() |
||||
|
account.email = user.email |
||||
|
mocks.accounts.getAccount.mockReturnValueOnce(account) |
||||
|
|
||||
|
const response = await api.users.deleteUser(user._id!, 400) |
||||
|
|
||||
|
expect(response.body.message).toBe("Unable to delete self.") |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,19 @@ |
|||||
|
import Router from "@koa/router" |
||||
|
import * as controller from "../../controllers/system/accounts" |
||||
|
import { middleware } from "@budibase/backend-core" |
||||
|
|
||||
|
const router = new Router() |
||||
|
|
||||
|
router |
||||
|
.put( |
||||
|
"/api/system/accounts/:accountId/metadata", |
||||
|
middleware.internalApi, |
||||
|
controller.save |
||||
|
) |
||||
|
.delete( |
||||
|
"/api/system/accounts/:accountId/metadata", |
||||
|
middleware.internalApi, |
||||
|
controller.destroy |
||||
|
) |
||||
|
|
||||
|
export = router |
||||
@ -0,0 +1,57 @@ |
|||||
|
import { accounts } from "../../../../sdk" |
||||
|
import { TestConfiguration, structures, API } from "../../../../tests" |
||||
|
import { v4 as uuid } from "uuid" |
||||
|
|
||||
|
describe("accounts", () => { |
||||
|
const config = new TestConfiguration() |
||||
|
const api = new API(config) |
||||
|
|
||||
|
beforeAll(async () => { |
||||
|
await config.beforeAll() |
||||
|
}) |
||||
|
|
||||
|
afterAll(async () => { |
||||
|
await config.afterAll() |
||||
|
}) |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
jest.clearAllMocks() |
||||
|
}) |
||||
|
|
||||
|
describe("metadata", () => { |
||||
|
describe("saveMetadata", () => { |
||||
|
it("saves account metadata", async () => { |
||||
|
let account = structures.accounts.account() |
||||
|
|
||||
|
const response = await api.accounts.saveMetadata(account) |
||||
|
|
||||
|
const id = accounts.formatAccountMetadataId(account.accountId) |
||||
|
const metadata = await accounts.getMetadata(id) |
||||
|
expect(response).toStrictEqual(metadata) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
describe("destroyMetadata", () => { |
||||
|
it("destroys account metadata", async () => { |
||||
|
const account = structures.accounts.account() |
||||
|
await api.accounts.saveMetadata(account) |
||||
|
|
||||
|
await api.accounts.destroyMetadata(account.accountId) |
||||
|
|
||||
|
const deleted = await accounts.getMetadata(account.accountId) |
||||
|
expect(deleted).toBe(undefined) |
||||
|
}) |
||||
|
|
||||
|
it("destroys account metadata that does not exist", async () => { |
||||
|
const id = uuid() |
||||
|
|
||||
|
const response = await api.accounts.destroyMetadata(id) |
||||
|
|
||||
|
expect(response.status).toBe(404) |
||||
|
expect(response.body.message).toBe( |
||||
|
`id=${accounts.formatAccountMetadataId(id)} does not exist` |
||||
|
) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -1,390 +0,0 @@ |
|||||
jest.mock("nodemailer") |
|
||||
const { config, request, mocks, structures } = require("../../../tests") |
|
||||
const sendMailMock = mocks.email.mock() |
|
||||
const { events } = require("@budibase/backend-core") |
|
||||
describe("/api/global/users", () => { |
|
||||
|
|
||||
beforeAll(async () => { |
|
||||
await config.beforeAll() |
|
||||
}) |
|
||||
|
|
||||
afterAll(async () => { |
|
||||
await config.afterAll() |
|
||||
}) |
|
||||
|
|
||||
const sendUserInvite = async () => { |
|
||||
await config.saveSmtpConfig() |
|
||||
await config.saveSettingsConfig() |
|
||||
const res = await request |
|
||||
.post(`/api/global/users/invite`) |
|
||||
.send({ |
|
||||
email: "invite@test.com", |
|
||||
}) |
|
||||
.set(config.defaultHeaders()) |
|
||||
.expect("Content-Type", /json/) |
|
||||
.expect(200) |
|
||||
|
|
||||
const emailCall = sendMailMock.mock.calls[0][0] |
|
||||
// after this URL there should be a code
|
|
||||
const parts = emailCall.html.split("http://localhost:10000/builder/invite?code=") |
|
||||
const code = parts[1].split("\"")[0].split("&")[0] |
|
||||
return { code, res } |
|
||||
} |
|
||||
|
|
||||
it("should be able to generate an invitation", async () => { |
|
||||
const { code, res } = await sendUserInvite() |
|
||||
|
|
||||
expect(res.body).toEqual({ message: "Invitation has been sent." }) |
|
||||
expect(sendMailMock).toHaveBeenCalled() |
|
||||
expect(code).toBeDefined() |
|
||||
expect(events.user.invited).toBeCalledTimes(1) |
|
||||
}) |
|
||||
|
|
||||
it("should be able to create new user from invite", async () => { |
|
||||
const { code } = await sendUserInvite() |
|
||||
|
|
||||
const res = await request |
|
||||
.post(`/api/global/users/invite/accept`) |
|
||||
.send({ |
|
||||
password: "newpassword", |
|
||||
inviteCode: code, |
|
||||
}) |
|
||||
.expect("Content-Type", /json/) |
|
||||
.expect(200) |
|
||||
expect(res.body._id).toBeDefined() |
|
||||
const user = await config.getUser("invite@test.com") |
|
||||
expect(user).toBeDefined() |
|
||||
expect(user._id).toEqual(res.body._id) |
|
||||
expect(events.user.inviteAccepted).toBeCalledTimes(1) |
|
||||
expect(events.user.inviteAccepted).toBeCalledWith(user) |
|
||||
}) |
|
||||
|
|
||||
const createUser = async (user) => { |
|
||||
const existing = await config.getUser(user.email) |
|
||||
if (existing) { |
|
||||
await deleteUser(existing._id) |
|
||||
} |
|
||||
return saveUser(user) |
|
||||
} |
|
||||
|
|
||||
const updateUser = async (user) => { |
|
||||
const existing = await config.getUser(user.email) |
|
||||
user._id = existing._id |
|
||||
return saveUser(user) |
|
||||
} |
|
||||
|
|
||||
const saveUser = async (user) => { |
|
||||
const res = await request |
|
||||
.post(`/api/global/users`) |
|
||||
.send(user) |
|
||||
.set(config.defaultHeaders()) |
|
||||
.expect("Content-Type", /json/) |
|
||||
.expect(200) |
|
||||
return res.body |
|
||||
} |
|
||||
|
|
||||
|
|
||||
const bulkCreateUsers = async (users) => { |
|
||||
const res = await request |
|
||||
.post(`/api/global/users/bulkCreate`) |
|
||||
.send(users) |
|
||||
.set(config.defaultHeaders()) |
|
||||
.expect("Content-Type", /json/) |
|
||||
.expect(200) |
|
||||
return res.body |
|
||||
} |
|
||||
|
|
||||
const bulkDeleteUsers = async (users) => { |
|
||||
const res = await request |
|
||||
.post(`/api/global/users/bulkDelete`) |
|
||||
.send(users) |
|
||||
.set(config.defaultHeaders()) |
|
||||
.expect("Content-Type", /json/) |
|
||||
.expect(200) |
|
||||
return res.body |
|
||||
} |
|
||||
|
|
||||
|
|
||||
|
|
||||
const deleteUser = async (email) => { |
|
||||
const user = await config.getUser(email) |
|
||||
if (user) { |
|
||||
await request |
|
||||
.delete(`/api/global/users/${user._id}`) |
|
||||
.set(config.defaultHeaders()) |
|
||||
.expect("Content-Type", /json/) |
|
||||
.expect(200) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
describe("create", () => { |
|
||||
it("should be able to create a basic user", async () => { |
|
||||
jest.clearAllMocks() |
|
||||
const user = structures.users.user({ email: "basic@test.com" }) |
|
||||
await createUser(user) |
|
||||
|
|
||||
expect(events.user.created).toBeCalledTimes(1) |
|
||||
expect(events.user.updated).not.toBeCalled() |
|
||||
expect(events.user.permissionBuilderAssigned).not.toBeCalled() |
|
||||
expect(events.user.permissionAdminAssigned).not.toBeCalled() |
|
||||
}) |
|
||||
|
|
||||
it("should be able to bulkCreate users with different permissions", async () => { |
|
||||
jest.clearAllMocks() |
|
||||
const builder = structures.users.builderUser({ email: "bulkbasic@test.com" }) |
|
||||
const admin = structures.users.adminUser({ email: "bulkadmin@test.com" }) |
|
||||
const user = structures.users.user({ email: "bulkuser@test.com" }) |
|
||||
|
|
||||
let toCreate = { users: [builder, admin, user], groups: [] } |
|
||||
await bulkCreateUsers(toCreate) |
|
||||
|
|
||||
expect(events.user.created).toBeCalledTimes(3) |
|
||||
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1) |
|
||||
}) |
|
||||
|
|
||||
|
|
||||
it("should be able to create an admin user", async () => { |
|
||||
jest.clearAllMocks() |
|
||||
const user = structures.users.adminUser({ email: "admin@test.com" }) |
|
||||
await createUser(user) |
|
||||
|
|
||||
expect(events.user.created).toBeCalledTimes(1) |
|
||||
expect(events.user.updated).not.toBeCalled() |
|
||||
expect(events.user.permissionBuilderAssigned).not.toBeCalled() |
|
||||
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1) |
|
||||
}) |
|
||||
|
|
||||
it("should be able to create a builder user", async () => { |
|
||||
jest.clearAllMocks() |
|
||||
const user = structures.users.builderUser({ email: "builder@test.com" }) |
|
||||
await createUser(user) |
|
||||
|
|
||||
expect(events.user.created).toBeCalledTimes(1) |
|
||||
expect(events.user.updated).not.toBeCalled() |
|
||||
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionAdminAssigned).not.toBeCalled() |
|
||||
}) |
|
||||
|
|
||||
it("should be able to assign app roles", async () => { |
|
||||
jest.clearAllMocks() |
|
||||
const user = structures.users.user({ email: "assign-roles@test.com" }) |
|
||||
user.roles = { |
|
||||
"app_123": "role1", |
|
||||
"app_456": "role2", |
|
||||
} |
|
||||
|
|
||||
await createUser(user) |
|
||||
const savedUser = await config.getUser(user.email) |
|
||||
|
|
||||
expect(events.user.created).toBeCalledTimes(1) |
|
||||
expect(events.user.updated).not.toBeCalled() |
|
||||
expect(events.role.assigned).toBeCalledTimes(2) |
|
||||
expect(events.role.assigned).toBeCalledWith(savedUser, "role1") |
|
||||
expect(events.role.assigned).toBeCalledWith(savedUser, "role2") |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
describe("update", () => { |
|
||||
it("should be able to update a basic user", async () => { |
|
||||
let user = structures.users.user({ email: "basic-update@test.com" }) |
|
||||
await createUser(user) |
|
||||
jest.clearAllMocks() |
|
||||
|
|
||||
await updateUser(user) |
|
||||
|
|
||||
expect(events.user.created).not.toBeCalled() |
|
||||
expect(events.user.updated).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionBuilderAssigned).not.toBeCalled() |
|
||||
expect(events.user.permissionAdminAssigned).not.toBeCalled() |
|
||||
expect(events.user.passwordForceReset).not.toBeCalled() |
|
||||
}) |
|
||||
|
|
||||
it("should be able to force reset password", async () => { |
|
||||
let user = structures.users.user({ email: "basic-password-update@test.com" }) |
|
||||
await createUser(user) |
|
||||
jest.clearAllMocks() |
|
||||
|
|
||||
user.forceResetPassword = true |
|
||||
user.password = "tempPassword" |
|
||||
await updateUser(user) |
|
||||
|
|
||||
expect(events.user.created).not.toBeCalled() |
|
||||
expect(events.user.updated).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionBuilderAssigned).not.toBeCalled() |
|
||||
expect(events.user.permissionAdminAssigned).not.toBeCalled() |
|
||||
expect(events.user.passwordForceReset).toBeCalledTimes(1) |
|
||||
}) |
|
||||
|
|
||||
it("should be able to update a basic user to an admin user", async () => { |
|
||||
let user = structures.users.user({ email: "basic-update-admin@test.com" }) |
|
||||
await createUser(user) |
|
||||
jest.clearAllMocks() |
|
||||
|
|
||||
await updateUser(structures.users.adminUser(user)) |
|
||||
|
|
||||
expect(events.user.created).not.toBeCalled() |
|
||||
expect(events.user.updated).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionBuilderAssigned).not.toBeCalled() |
|
||||
expect(events.user.permissionAdminAssigned).toBeCalledTimes(1) |
|
||||
}) |
|
||||
|
|
||||
it("should be able to update a basic user to a builder user", async () => { |
|
||||
let user = structures.users.user({ email: "basic-update-builder@test.com" }) |
|
||||
await createUser(user) |
|
||||
jest.clearAllMocks() |
|
||||
|
|
||||
await updateUser(structures.users.builderUser(user)) |
|
||||
|
|
||||
expect(events.user.created).not.toBeCalled() |
|
||||
expect(events.user.updated).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionAdminAssigned).not.toBeCalled() |
|
||||
}) |
|
||||
|
|
||||
it("should be able to update an admin user to a basic user", async () => { |
|
||||
let user = structures.users.adminUser({ email: "admin-update-basic@test.com" }) |
|
||||
await createUser(user) |
|
||||
jest.clearAllMocks() |
|
||||
|
|
||||
user.admin.global = false |
|
||||
await updateUser(user) |
|
||||
|
|
||||
expect(events.user.created).not.toBeCalled() |
|
||||
expect(events.user.updated).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionBuilderRemoved).not.toBeCalled() |
|
||||
}) |
|
||||
|
|
||||
it("should be able to update an builder user to a basic user", async () => { |
|
||||
let user = structures.users.builderUser({ email: "builder-update-basic@test.com" }) |
|
||||
await createUser(user) |
|
||||
jest.clearAllMocks() |
|
||||
|
|
||||
user.builder.global = false |
|
||||
await updateUser(user) |
|
||||
|
|
||||
expect(events.user.created).not.toBeCalled() |
|
||||
expect(events.user.updated).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionAdminRemoved).not.toBeCalled() |
|
||||
}) |
|
||||
|
|
||||
it("should be able to assign app roles", async () => { |
|
||||
const user = structures.users.user({ email: "assign-roles-update@test.com" }) |
|
||||
await createUser(user) |
|
||||
jest.clearAllMocks() |
|
||||
|
|
||||
user.roles = { |
|
||||
"app_123": "role1", |
|
||||
"app_456": "role2", |
|
||||
} |
|
||||
await updateUser(user) |
|
||||
const savedUser = await config.getUser(user.email) |
|
||||
|
|
||||
expect(events.user.created).not.toBeCalled() |
|
||||
expect(events.user.updated).toBeCalledTimes(1) |
|
||||
expect(events.role.assigned).toBeCalledTimes(2) |
|
||||
expect(events.role.assigned).toBeCalledWith(savedUser, "role1") |
|
||||
expect(events.role.assigned).toBeCalledWith(savedUser, "role2") |
|
||||
}) |
|
||||
|
|
||||
it("should be able to unassign app roles", async () => { |
|
||||
const user = structures.users.user({ email: "unassign-roles@test.com" }) |
|
||||
user.roles = { |
|
||||
"app_123": "role1", |
|
||||
"app_456": "role2", |
|
||||
} |
|
||||
await createUser(user) |
|
||||
jest.clearAllMocks() |
|
||||
|
|
||||
user.roles = {} |
|
||||
await updateUser(user) |
|
||||
const savedUser = await config.getUser(user.email) |
|
||||
|
|
||||
expect(events.user.created).not.toBeCalled() |
|
||||
expect(events.user.updated).toBeCalledTimes(1) |
|
||||
expect(events.role.unassigned).toBeCalledTimes(2) |
|
||||
expect(events.role.unassigned).toBeCalledWith(savedUser, "role1") |
|
||||
expect(events.role.unassigned).toBeCalledWith(savedUser, "role2") |
|
||||
}) |
|
||||
|
|
||||
it("should be able to update existing app roles", async () => { |
|
||||
const user = structures.users.user({ email: "update-roles@test.com" }) |
|
||||
user.roles = { |
|
||||
"app_123": "role1", |
|
||||
"app_456": "role2", |
|
||||
} |
|
||||
await createUser(user) |
|
||||
jest.clearAllMocks() |
|
||||
|
|
||||
user.roles = { |
|
||||
"app_123": "role1", |
|
||||
"app_456": "role2-edit", |
|
||||
} |
|
||||
await updateUser(user) |
|
||||
const savedUser = await config.getUser(user.email) |
|
||||
|
|
||||
expect(events.user.created).not.toBeCalled() |
|
||||
expect(events.user.updated).toBeCalledTimes(1) |
|
||||
expect(events.role.unassigned).toBeCalledTimes(1) |
|
||||
expect(events.role.unassigned).toBeCalledWith(savedUser, "role2") |
|
||||
expect(events.role.assigned).toBeCalledTimes(1) |
|
||||
expect(events.role.assigned).toBeCalledWith(savedUser, "role2-edit") |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
describe("destroy", () => { |
|
||||
it("should be able to destroy a basic user", async () => { |
|
||||
let user = structures.users.user({ email: "destroy@test.com" }) |
|
||||
await createUser(user) |
|
||||
jest.clearAllMocks() |
|
||||
|
|
||||
await deleteUser(user.email) |
|
||||
|
|
||||
expect(events.user.deleted).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionBuilderRemoved).not.toBeCalled() |
|
||||
expect(events.user.permissionAdminRemoved).not.toBeCalled() |
|
||||
}) |
|
||||
|
|
||||
it("should be able to destroy an admin user", async () => { |
|
||||
let user = structures.users.adminUser({ email: "destroy-admin@test.com" }) |
|
||||
await createUser(user) |
|
||||
jest.clearAllMocks() |
|
||||
|
|
||||
await deleteUser(user.email) |
|
||||
|
|
||||
expect(events.user.deleted).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionBuilderRemoved).not.toBeCalled() |
|
||||
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1) |
|
||||
}) |
|
||||
|
|
||||
it("should be able to destroy a builder user", async () => { |
|
||||
let user = structures.users.builderUser({ email: "destroy-admin@test.com" }) |
|
||||
await createUser(user) |
|
||||
jest.clearAllMocks() |
|
||||
|
|
||||
await deleteUser(user.email) |
|
||||
|
|
||||
expect(events.user.deleted).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionAdminRemoved).not.toBeCalled() |
|
||||
}) |
|
||||
|
|
||||
it("should be able to bulk delete users with different permissions", async () => { |
|
||||
jest.clearAllMocks() |
|
||||
const builder = structures.users.builderUser({ email: "basic@test.com" }) |
|
||||
const admin = structures.users.adminUser({ email: "admin@test.com" }) |
|
||||
const user = structures.users.user({ email: "user@test.com" }) |
|
||||
|
|
||||
let toCreate = { users: [builder, admin, user], groups: [] } |
|
||||
let createdUsers = await bulkCreateUsers(toCreate) |
|
||||
await bulkDeleteUsers({ userIds: [createdUsers[0]._id, createdUsers[1]._id, createdUsers[2]._id] }) |
|
||||
expect(events.user.deleted).toBeCalledTimes(3) |
|
||||
expect(events.user.permissionAdminRemoved).toBeCalledTimes(1) |
|
||||
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1) |
|
||||
|
|
||||
}) |
|
||||
|
|
||||
}) |
|
||||
}) |
|
||||
@ -0,0 +1,53 @@ |
|||||
|
import { AccountMetadata } from "@budibase/types" |
||||
|
import { |
||||
|
db, |
||||
|
StaticDatabases, |
||||
|
HTTPError, |
||||
|
DocumentType, |
||||
|
SEPARATOR, |
||||
|
} from "@budibase/backend-core" |
||||
|
|
||||
|
export const formatAccountMetadataId = (accountId: string) => { |
||||
|
return `${DocumentType.ACCOUNT_METADATA}${SEPARATOR}${accountId}` |
||||
|
} |
||||
|
|
||||
|
export const saveMetadata = async ( |
||||
|
metadata: AccountMetadata |
||||
|
): Promise<AccountMetadata> => { |
||||
|
return db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => { |
||||
|
const existing = await getMetadata(metadata._id!) |
||||
|
if (existing) { |
||||
|
metadata._rev = existing._rev |
||||
|
} |
||||
|
const res = await db.put(metadata) |
||||
|
metadata._rev = res.rev |
||||
|
return metadata |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
export const getMetadata = async ( |
||||
|
accountId: string |
||||
|
): Promise<AccountMetadata | undefined> => { |
||||
|
return db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => { |
||||
|
try { |
||||
|
return await db.get(accountId) |
||||
|
} catch (e: any) { |
||||
|
if (e.status === 404) { |
||||
|
// do nothing
|
||||
|
return |
||||
|
} else { |
||||
|
throw e |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
export const destroyMetadata = async (accountId: string) => { |
||||
|
await db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => { |
||||
|
const metadata = await getMetadata(accountId) |
||||
|
if (!metadata) { |
||||
|
throw new HTTPError(`id=${accountId} does not exist`, 404) |
||||
|
} |
||||
|
await db.remove(accountId, metadata._rev) |
||||
|
}) |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
export * from "./accounts" |
||||
@ -1 +1,2 @@ |
|||||
export * as users from "./users" |
export * as users from "./users" |
||||
|
export * as accounts from "./accounts" |
||||
|
|||||
@ -1,231 +0,0 @@ |
|||||
require("./mocks") |
|
||||
require("../db").init() |
|
||||
const env = require("../environment") |
|
||||
const controllers = require("./controllers") |
|
||||
const supertest = require("supertest") |
|
||||
const { jwt } = require("@budibase/backend-core/auth") |
|
||||
const { Cookies, Headers } = require("@budibase/backend-core/constants") |
|
||||
const { Configs } = require("../constants") |
|
||||
const { users } = require("@budibase/backend-core") |
|
||||
const { createASession } = require("@budibase/backend-core/sessions") |
|
||||
const { TENANT_ID, CSRF_TOKEN } = require("./structures") |
|
||||
const structures = require("./structures") |
|
||||
const { doInTenant } = require("@budibase/backend-core/tenancy") |
|
||||
const { groups } = require("@budibase/pro") |
|
||||
class TestConfiguration { |
|
||||
constructor(openServer = true) { |
|
||||
if (openServer) { |
|
||||
env.PORT = "0" // random port
|
|
||||
this.server = require("../index") |
|
||||
// we need the request for logging in, involves cookies, hard to fake
|
|
||||
this.request = supertest(this.server) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
getRequest() { |
|
||||
return this.request |
|
||||
} |
|
||||
|
|
||||
// UTILS
|
|
||||
|
|
||||
async _req(config, params, controlFunc) { |
|
||||
const request = {} |
|
||||
// fake cookies, we don't need them
|
|
||||
request.cookies = { set: () => {}, get: () => {} } |
|
||||
request.config = { jwtSecret: env.JWT_SECRET } |
|
||||
request.appId = this.appId |
|
||||
request.user = { appId: this.appId, tenantId: TENANT_ID } |
|
||||
request.query = {} |
|
||||
request.request = { |
|
||||
body: config, |
|
||||
} |
|
||||
request.throw = (status, err) => { |
|
||||
throw { status, message: err } |
|
||||
} |
|
||||
if (params) { |
|
||||
request.params = params |
|
||||
} |
|
||||
await doInTenant(TENANT_ID, () => { |
|
||||
return controlFunc(request) |
|
||||
}) |
|
||||
return request.body |
|
||||
} |
|
||||
|
|
||||
// SETUP / TEARDOWN
|
|
||||
|
|
||||
async beforeAll() { |
|
||||
await this.login() |
|
||||
} |
|
||||
|
|
||||
async afterAll() { |
|
||||
if (this.server) { |
|
||||
await this.server.close() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// USER / AUTH
|
|
||||
|
|
||||
async login() { |
|
||||
// create a test user
|
|
||||
await this._req( |
|
||||
{ |
|
||||
email: "test@test.com", |
|
||||
password: "test", |
|
||||
_id: "us_uuid1", |
|
||||
builder: { |
|
||||
global: true, |
|
||||
}, |
|
||||
admin: { |
|
||||
global: true, |
|
||||
}, |
|
||||
}, |
|
||||
null, |
|
||||
controllers.users.save |
|
||||
) |
|
||||
await createASession("us_uuid1", { |
|
||||
sessionId: "sessionid", |
|
||||
tenantId: TENANT_ID, |
|
||||
csrfToken: CSRF_TOKEN, |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
cookieHeader(cookies) { |
|
||||
return { |
|
||||
Cookie: [cookies], |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
defaultHeaders() { |
|
||||
const user = { |
|
||||
_id: "us_uuid1", |
|
||||
userId: "us_uuid1", |
|
||||
sessionId: "sessionid", |
|
||||
tenantId: TENANT_ID, |
|
||||
} |
|
||||
const authToken = jwt.sign(user, env.JWT_SECRET) |
|
||||
return { |
|
||||
Accept: "application/json", |
|
||||
...this.cookieHeader([`${Cookies.Auth}=${authToken}`]), |
|
||||
[Headers.CSRF_TOKEN]: CSRF_TOKEN, |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async getUser(email) { |
|
||||
return doInTenant(TENANT_ID, () => { |
|
||||
return users.getGlobalUserByEmail(email) |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
async getGroup(id) { |
|
||||
return doInTenant(TENANT_ID, () => { |
|
||||
return groups.get(id) |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
async saveGroup(group) { |
|
||||
const res = await this.getRequest() |
|
||||
.post(`/api/global/groups`) |
|
||||
.send(group) |
|
||||
.set(this.defaultHeaders()) |
|
||||
.expect("Content-Type", /json/) |
|
||||
.expect(200) |
|
||||
return res.body |
|
||||
} |
|
||||
|
|
||||
async createUser(email, password) { |
|
||||
const user = await this.getUser(structures.users.email) |
|
||||
if (user) { |
|
||||
return user |
|
||||
} |
|
||||
await this._req( |
|
||||
structures.users.user({ email, password }), |
|
||||
null, |
|
||||
controllers.users.save |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
async saveAdminUser() { |
|
||||
await this._req( |
|
||||
structures.users.user({ tenantId: TENANT_ID }), |
|
||||
null, |
|
||||
controllers.users.adminUser |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
// CONFIGS
|
|
||||
|
|
||||
async deleteConfig(type) { |
|
||||
try { |
|
||||
const cfg = await this._req( |
|
||||
null, |
|
||||
{ |
|
||||
type, |
|
||||
}, |
|
||||
controllers.config.find |
|
||||
) |
|
||||
if (cfg) { |
|
||||
await this._req( |
|
||||
null, |
|
||||
{ |
|
||||
id: cfg._id, |
|
||||
rev: cfg._rev, |
|
||||
}, |
|
||||
controllers.config.destroy |
|
||||
) |
|
||||
} |
|
||||
} catch (err) { |
|
||||
// don't need to handle error
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// CONFIGS - SETTINGS
|
|
||||
|
|
||||
async saveSettingsConfig() { |
|
||||
await this.deleteConfig(Configs.SETTINGS) |
|
||||
await this._req( |
|
||||
structures.configs.settings(), |
|
||||
null, |
|
||||
controllers.config.save |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
// CONFIGS - GOOGLE
|
|
||||
|
|
||||
async saveGoogleConfig() { |
|
||||
await this.deleteConfig(Configs.GOOGLE) |
|
||||
await this._req(structures.configs.google(), null, controllers.config.save) |
|
||||
} |
|
||||
|
|
||||
// CONFIGS - OIDC
|
|
||||
|
|
||||
getOIDConfigCookie(configId) { |
|
||||
const token = jwt.sign(configId, env.JWT_SECRET) |
|
||||
return this.cookieHeader([[`${Cookies.OIDC_CONFIG}=${token}`]]) |
|
||||
} |
|
||||
|
|
||||
async saveOIDCConfig() { |
|
||||
await this.deleteConfig(Configs.OIDC) |
|
||||
const config = structures.configs.oidc() |
|
||||
|
|
||||
await this._req(config, null, controllers.config.save) |
|
||||
return config |
|
||||
} |
|
||||
|
|
||||
// CONFIGS - SMTP
|
|
||||
|
|
||||
async saveSmtpConfig() { |
|
||||
await this.deleteConfig(Configs.SMTP) |
|
||||
await this._req(structures.configs.smtp(), null, controllers.config.save) |
|
||||
} |
|
||||
|
|
||||
async saveEtherealSmtpConfig() { |
|
||||
await this.deleteConfig(Configs.SMTP) |
|
||||
await this._req( |
|
||||
structures.configs.smtpEthereal(), |
|
||||
null, |
|
||||
controllers.config.save |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
module.exports = TestConfiguration |
|
||||
@ -0,0 +1,273 @@ |
|||||
|
import "./mocks" |
||||
|
import dbConfig from "../db" |
||||
|
dbConfig.init() |
||||
|
import env from "../environment" |
||||
|
import controllers from "./controllers" |
||||
|
const supertest = require("supertest") |
||||
|
import { Configs } from "../constants" |
||||
|
import { |
||||
|
users, |
||||
|
tenancy, |
||||
|
Cookies, |
||||
|
Headers, |
||||
|
sessions, |
||||
|
auth, |
||||
|
} from "@budibase/backend-core" |
||||
|
import { TENANT_ID, TENANT_1, CSRF_TOKEN } from "./structures" |
||||
|
import structures from "./structures" |
||||
|
import { CreateUserResponse, User, AuthToken } from "@budibase/types" |
||||
|
|
||||
|
enum Mode { |
||||
|
ACCOUNT = "account", |
||||
|
SELF = "self", |
||||
|
} |
||||
|
|
||||
|
class TestConfiguration { |
||||
|
server: any |
||||
|
request: any |
||||
|
defaultUser?: User |
||||
|
tenant1User?: User |
||||
|
|
||||
|
constructor( |
||||
|
opts: { openServer: boolean; mode: Mode } = { |
||||
|
openServer: true, |
||||
|
mode: Mode.ACCOUNT, |
||||
|
} |
||||
|
) { |
||||
|
if (opts.mode === Mode.ACCOUNT) { |
||||
|
this.modeAccount() |
||||
|
} else if (opts.mode === Mode.SELF) { |
||||
|
this.modeSelf() |
||||
|
} |
||||
|
|
||||
|
if (opts.openServer) { |
||||
|
env.PORT = "0" // random port
|
||||
|
this.server = require("../index") |
||||
|
// we need the request for logging in, involves cookies, hard to fake
|
||||
|
this.request = supertest(this.server) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
getRequest() { |
||||
|
return this.request |
||||
|
} |
||||
|
|
||||
|
// MODES
|
||||
|
|
||||
|
modeAccount = () => { |
||||
|
env.SELF_HOSTED = false |
||||
|
// @ts-ignore
|
||||
|
env.MULTI_TENANCY = true |
||||
|
// @ts-ignore
|
||||
|
env.DISABLE_ACCOUNT_PORTAL = false |
||||
|
} |
||||
|
|
||||
|
modeSelf = () => { |
||||
|
env.SELF_HOSTED = true |
||||
|
// @ts-ignore
|
||||
|
env.MULTI_TENANCY = false |
||||
|
// @ts-ignore
|
||||
|
env.DISABLE_ACCOUNT_PORTAL = true |
||||
|
} |
||||
|
|
||||
|
// UTILS
|
||||
|
|
||||
|
async _req(config: any, params: any, controlFunc: any) { |
||||
|
const request: any = {} |
||||
|
// fake cookies, we don't need them
|
||||
|
request.cookies = { set: () => {}, get: () => {} } |
||||
|
request.config = { jwtSecret: env.JWT_SECRET } |
||||
|
request.user = { tenantId: this.getTenantId() } |
||||
|
request.query = {} |
||||
|
request.request = { |
||||
|
body: config, |
||||
|
} |
||||
|
request.throw = (status: any, err: any) => { |
||||
|
throw { status, message: err } |
||||
|
} |
||||
|
if (params) { |
||||
|
request.params = params |
||||
|
} |
||||
|
await tenancy.doInTenant(this.getTenantId(), () => { |
||||
|
return controlFunc(request) |
||||
|
}) |
||||
|
return request.body |
||||
|
} |
||||
|
|
||||
|
// SETUP / TEARDOWN
|
||||
|
|
||||
|
async beforeAll() { |
||||
|
await this.createDefaultUser() |
||||
|
await this.createSession(this.defaultUser!) |
||||
|
|
||||
|
await tenancy.doInTenant(TENANT_1, async () => { |
||||
|
await this.createTenant1User() |
||||
|
await this.createSession(this.tenant1User!) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
async afterAll() { |
||||
|
if (this.server) { |
||||
|
await this.server.close() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// TENANCY
|
||||
|
|
||||
|
getTenantId() { |
||||
|
try { |
||||
|
return tenancy.getTenantId() |
||||
|
} catch (e: any) { |
||||
|
return TENANT_ID |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// USER / AUTH
|
||||
|
|
||||
|
async createDefaultUser() { |
||||
|
const user = structures.users.adminUser({ |
||||
|
email: "test@test.com", |
||||
|
password: "test", |
||||
|
}) |
||||
|
this.defaultUser = await this.createUser(user) |
||||
|
} |
||||
|
|
||||
|
async createTenant1User() { |
||||
|
const user = structures.users.adminUser({ |
||||
|
email: "tenant1@test.com", |
||||
|
password: "test", |
||||
|
}) |
||||
|
this.tenant1User = await this.createUser(user) |
||||
|
} |
||||
|
|
||||
|
async createSession(user: User) { |
||||
|
await sessions.createASession(user._id!, { |
||||
|
sessionId: "sessionid", |
||||
|
tenantId: user.tenantId, |
||||
|
csrfToken: CSRF_TOKEN, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
cookieHeader(cookies: any) { |
||||
|
return { |
||||
|
Cookie: [cookies], |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
authHeaders(user: User) { |
||||
|
const authToken: AuthToken = { |
||||
|
userId: user._id!, |
||||
|
sessionId: "sessionid", |
||||
|
tenantId: user.tenantId, |
||||
|
} |
||||
|
const authCookie = auth.jwt.sign(authToken, env.JWT_SECRET) |
||||
|
return { |
||||
|
Accept: "application/json", |
||||
|
...this.cookieHeader([`${Cookies.Auth}=${authCookie}`]), |
||||
|
[Headers.CSRF_TOKEN]: CSRF_TOKEN, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
defaultHeaders() { |
||||
|
const tenantId = this.getTenantId() |
||||
|
if (tenantId === TENANT_ID) { |
||||
|
return this.authHeaders(this.defaultUser!) |
||||
|
} else if (tenantId === TENANT_1) { |
||||
|
return this.authHeaders(this.tenant1User!) |
||||
|
} else { |
||||
|
throw new Error("could not determine auth headers to use") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async getUser(email: string): Promise<User> { |
||||
|
return tenancy.doInTenant(this.getTenantId(), () => { |
||||
|
return users.getGlobalUserByEmail(email) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
async createUser(user?: User) { |
||||
|
if (!user) { |
||||
|
user = structures.users.user() |
||||
|
} |
||||
|
const response = await this._req(user, null, controllers.users.save) |
||||
|
const body = response as CreateUserResponse |
||||
|
return this.getUser(body.email) |
||||
|
} |
||||
|
|
||||
|
// CONFIGS
|
||||
|
|
||||
|
async deleteConfig(type: any) { |
||||
|
try { |
||||
|
const cfg = await this._req( |
||||
|
null, |
||||
|
{ |
||||
|
type, |
||||
|
}, |
||||
|
controllers.config.find |
||||
|
) |
||||
|
if (cfg) { |
||||
|
await this._req( |
||||
|
null, |
||||
|
{ |
||||
|
id: cfg._id, |
||||
|
rev: cfg._rev, |
||||
|
}, |
||||
|
controllers.config.destroy |
||||
|
) |
||||
|
} |
||||
|
} catch (err) { |
||||
|
// don't need to handle error
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// CONFIGS - SETTINGS
|
||||
|
|
||||
|
async saveSettingsConfig() { |
||||
|
await this.deleteConfig(Configs.SETTINGS) |
||||
|
await this._req( |
||||
|
structures.configs.settings(), |
||||
|
null, |
||||
|
controllers.config.save |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// CONFIGS - GOOGLE
|
||||
|
|
||||
|
async saveGoogleConfig() { |
||||
|
await this.deleteConfig(Configs.GOOGLE) |
||||
|
await this._req(structures.configs.google(), null, controllers.config.save) |
||||
|
} |
||||
|
|
||||
|
// CONFIGS - OIDC
|
||||
|
|
||||
|
getOIDConfigCookie(configId: string) { |
||||
|
const token = auth.jwt.sign(configId, env.JWT_SECRET) |
||||
|
return this.cookieHeader([[`${Cookies.OIDC_CONFIG}=${token}`]]) |
||||
|
} |
||||
|
|
||||
|
async saveOIDCConfig() { |
||||
|
await this.deleteConfig(Configs.OIDC) |
||||
|
const config = structures.configs.oidc() |
||||
|
|
||||
|
await this._req(config, null, controllers.config.save) |
||||
|
return config |
||||
|
} |
||||
|
|
||||
|
// CONFIGS - SMTP
|
||||
|
|
||||
|
async saveSmtpConfig() { |
||||
|
await this.deleteConfig(Configs.SMTP) |
||||
|
await this._req(structures.configs.smtp(), null, controllers.config.save) |
||||
|
} |
||||
|
|
||||
|
async saveEtherealSmtpConfig() { |
||||
|
await this.deleteConfig(Configs.SMTP) |
||||
|
await this._req( |
||||
|
structures.configs.smtpEthereal(), |
||||
|
null, |
||||
|
controllers.config.save |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export = TestConfiguration |
||||
@ -0,0 +1,28 @@ |
|||||
|
import { Account, AccountMetadata } from "@budibase/types" |
||||
|
import TestConfiguration from "../TestConfiguration" |
||||
|
|
||||
|
export class AccountAPI { |
||||
|
config: TestConfiguration |
||||
|
request: any |
||||
|
|
||||
|
constructor(config: TestConfiguration) { |
||||
|
this.config = config |
||||
|
this.request = config.request |
||||
|
} |
||||
|
|
||||
|
saveMetadata = async (account: Account) => { |
||||
|
const res = await this.request |
||||
|
.put(`/api/system/accounts/${account.accountId}/metadata`) |
||||
|
.send(account) |
||||
|
.set(this.config.defaultHeaders()) |
||||
|
.expect("Content-Type", /json/) |
||||
|
.expect(200) |
||||
|
return res.body as AccountMetadata |
||||
|
} |
||||
|
|
||||
|
destroyMetadata = (accountId: string) => { |
||||
|
return this.request |
||||
|
.del(`/api/system/accounts/${accountId}/metadata`) |
||||
|
.set(this.config.defaultHeaders()) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
import TestConfiguration from "../TestConfiguration" |
||||
|
|
||||
|
export class AuthAPI { |
||||
|
config: TestConfiguration |
||||
|
request: any |
||||
|
|
||||
|
constructor(config: TestConfiguration) { |
||||
|
this.config = config |
||||
|
this.request = config.request |
||||
|
} |
||||
|
|
||||
|
updatePassword = (code: string) => { |
||||
|
return this.request |
||||
|
.post(`/api/global/auth/${this.config.getTenantId()}/reset/update`) |
||||
|
.send({ |
||||
|
password: "newpassword", |
||||
|
resetCode: code, |
||||
|
}) |
||||
|
.expect("Content-Type", /json/) |
||||
|
.expect(200) |
||||
|
} |
||||
|
|
||||
|
logout = () => { |
||||
|
return this.request |
||||
|
.post("/api/global/auth/logout") |
||||
|
.set(this.config.defaultHeaders()) |
||||
|
.expect(200) |
||||
|
} |
||||
|
|
||||
|
requestPasswordReset = async (sendMailMock: any) => { |
||||
|
await this.config.saveSmtpConfig() |
||||
|
await this.config.saveSettingsConfig() |
||||
|
await this.config.createUser() |
||||
|
const res = await this.request |
||||
|
.post(`/api/global/auth/${this.config.getTenantId()}/reset`) |
||||
|
.send({ |
||||
|
email: "test@test.com", |
||||
|
}) |
||||
|
.expect("Content-Type", /json/) |
||||
|
.expect(200) |
||||
|
const emailCall = sendMailMock.mock.calls[0][0] |
||||
|
const parts = emailCall.html.split( |
||||
|
`http://localhost:10000/builder/auth/reset?code=` |
||||
|
) |
||||
|
const code = parts[1].split('"')[0].split("&")[0] |
||||
|
return { code, res } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
import TestConfiguration from "../TestConfiguration" |
||||
|
|
||||
|
export class ConfigAPI { |
||||
|
config: TestConfiguration |
||||
|
request: any |
||||
|
|
||||
|
constructor(config: TestConfiguration) { |
||||
|
this.config = config |
||||
|
this.request = config.request |
||||
|
} |
||||
|
|
||||
|
getConfigChecklist = () => { |
||||
|
return this.request |
||||
|
.get(`/api/global/configs/checklist`) |
||||
|
.set(this.config.defaultHeaders()) |
||||
|
.expect("Content-Type", /json/) |
||||
|
.expect(200) |
||||
|
} |
||||
|
|
||||
|
saveConfig = (data: any) => { |
||||
|
return this.request |
||||
|
.post(`/api/global/configs`) |
||||
|
.send(data) |
||||
|
.set(this.config.defaultHeaders()) |
||||
|
.expect("Content-Type", /json/) |
||||
|
.expect(200) |
||||
|
} |
||||
|
|
||||
|
OIDCCallback = (configId: string) => { |
||||
|
return this.request |
||||
|
.get(`/api/global/auth/${this.config.getTenantId()}/oidc/callback`) |
||||
|
.set(this.config.getOIDConfigCookie(configId)) |
||||
|
} |
||||
|
|
||||
|
getOIDCConfig = (configId: string) => { |
||||
|
return this.request.get( |
||||
|
`/api/global/auth/${this.config.getTenantId()}/oidc/configs/${configId}` |
||||
|
) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
import TestConfiguration from "../TestConfiguration" |
||||
|
|
||||
|
export class EmailAPI { |
||||
|
config: TestConfiguration |
||||
|
request: any |
||||
|
|
||||
|
constructor(config: TestConfiguration) { |
||||
|
this.config = config |
||||
|
this.request = config.request |
||||
|
} |
||||
|
|
||||
|
sendEmail = (purpose: string) => { |
||||
|
return this.request |
||||
|
.post(`/api/global/email/send`) |
||||
|
.send({ |
||||
|
email: "test@test.com", |
||||
|
purpose, |
||||
|
tenantId: this.config.getTenantId(), |
||||
|
}) |
||||
|
.set(this.config.defaultHeaders()) |
||||
|
.expect("Content-Type", /json/) |
||||
|
.expect(200) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
import TestConfiguration from "../TestConfiguration" |
||||
|
import { AccountAPI } from "./accounts" |
||||
|
import { AuthAPI } from "./auth" |
||||
|
import { ConfigAPI } from "./configs" |
||||
|
import { EmailAPI } from "./email" |
||||
|
import { SelfAPI } from "./self" |
||||
|
import { UserAPI } from "./users" |
||||
|
|
||||
|
export default class API { |
||||
|
accounts: AccountAPI |
||||
|
auth: AuthAPI |
||||
|
configs: ConfigAPI |
||||
|
emails: EmailAPI |
||||
|
self: SelfAPI |
||||
|
users: UserAPI |
||||
|
|
||||
|
constructor(config: TestConfiguration) { |
||||
|
this.accounts = new AccountAPI(config) |
||||
|
this.auth = new AuthAPI(config) |
||||
|
this.configs = new ConfigAPI(config) |
||||
|
this.emails = new EmailAPI(config) |
||||
|
this.self = new SelfAPI(config) |
||||
|
this.users = new UserAPI(config) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
import TestConfiguration from "../TestConfiguration" |
||||
|
import { User } from "@budibase/types" |
||||
|
|
||||
|
export class SelfAPI { |
||||
|
config: TestConfiguration |
||||
|
request: any |
||||
|
|
||||
|
constructor(config: TestConfiguration) { |
||||
|
this.config = config |
||||
|
this.request = config.request |
||||
|
} |
||||
|
|
||||
|
updateSelf = (user: User) => { |
||||
|
return this.request |
||||
|
.post(`/api/global/self`) |
||||
|
.send(user) |
||||
|
.set(this.config.authHeaders(user)) |
||||
|
.expect("Content-Type", /json/) |
||||
|
.expect(200) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
import { |
||||
|
BulkCreateUsersRequest, |
||||
|
BulkCreateUsersResponse, |
||||
|
BulkDeleteUsersRequest, |
||||
|
CreateUserResponse, |
||||
|
User, |
||||
|
UserDetails, |
||||
|
} from "@budibase/types" |
||||
|
import TestConfiguration from "../TestConfiguration" |
||||
|
|
||||
|
export class UserAPI { |
||||
|
config: TestConfiguration |
||||
|
request: any |
||||
|
|
||||
|
constructor(config: TestConfiguration) { |
||||
|
this.config = config |
||||
|
this.request = config.request |
||||
|
} |
||||
|
|
||||
|
// INVITE
|
||||
|
|
||||
|
sendUserInvite = async (sendMailMock: any) => { |
||||
|
await this.config.saveSmtpConfig() |
||||
|
await this.config.saveSettingsConfig() |
||||
|
const res = await this.request |
||||
|
.post(`/api/global/users/invite`) |
||||
|
.send({ |
||||
|
email: "invite@test.com", |
||||
|
}) |
||||
|
.set(this.config.defaultHeaders()) |
||||
|
.expect("Content-Type", /json/) |
||||
|
.expect(200) |
||||
|
|
||||
|
const emailCall = sendMailMock.mock.calls[0][0] |
||||
|
// after this URL there should be a code
|
||||
|
const parts = emailCall.html.split( |
||||
|
"http://localhost:10000/builder/invite?code=" |
||||
|
) |
||||
|
const code = parts[1].split('"')[0].split("&")[0] |
||||
|
return { code, res } |
||||
|
} |
||||
|
|
||||
|
acceptInvite = (code: string) => { |
||||
|
return this.request |
||||
|
.post(`/api/global/users/invite/accept`) |
||||
|
.send({ |
||||
|
password: "newpassword", |
||||
|
inviteCode: code, |
||||
|
}) |
||||
|
.expect("Content-Type", /json/) |
||||
|
.expect(200) |
||||
|
} |
||||
|
|
||||
|
// BULK
|
||||
|
|
||||
|
bulkCreateUsers = async (users: User[], groups: any[] = []) => { |
||||
|
const body: BulkCreateUsersRequest = { users, groups } |
||||
|
const res = await this.request |
||||
|
.post(`/api/global/users/bulkCreate`) |
||||
|
.send(body) |
||||
|
.set(this.config.defaultHeaders()) |
||||
|
.expect("Content-Type", /json/) |
||||
|
.expect(200) |
||||
|
|
||||
|
return res.body as BulkCreateUsersResponse |
||||
|
} |
||||
|
|
||||
|
bulkDeleteUsers = (body: BulkDeleteUsersRequest, status?: number) => { |
||||
|
return this.request |
||||
|
.post(`/api/global/users/bulkDelete`) |
||||
|
.send(body) |
||||
|
.set(this.config.defaultHeaders()) |
||||
|
.expect("Content-Type", /json/) |
||||
|
.expect(status ? status : 200) |
||||
|
} |
||||
|
|
||||
|
// USER
|
||||
|
|
||||
|
saveUser = (user: User, status?: number) => { |
||||
|
return this.request |
||||
|
.post(`/api/global/users`) |
||||
|
.send(user) |
||||
|
.set(this.config.defaultHeaders()) |
||||
|
.expect("Content-Type", /json/) |
||||
|
.expect(status ? status : 200) |
||||
|
} |
||||
|
|
||||
|
deleteUser = (userId: string, status?: number) => { |
||||
|
return this.request |
||||
|
.delete(`/api/global/users/${userId}`) |
||||
|
.set(this.config.defaultHeaders()) |
||||
|
.expect("Content-Type", /json/) |
||||
|
.expect(status ? status : 200) |
||||
|
} |
||||
|
} |
||||
@ -1,12 +0,0 @@ |
|||||
const TestConfiguration = require("./TestConfiguration") |
|
||||
const structures = require("./structures") |
|
||||
const mocks = require("./mocks") |
|
||||
const config = new TestConfiguration() |
|
||||
const request = config.getRequest() |
|
||||
|
|
||||
module.exports = { |
|
||||
structures, |
|
||||
mocks, |
|
||||
config, |
|
||||
request, |
|
||||
} |
|
||||
@ -0,0 +1,14 @@ |
|||||
|
import TestConfiguration from "./TestConfiguration" |
||||
|
import structures from "./structures" |
||||
|
import mocks from "./mocks" |
||||
|
import API from "./api" |
||||
|
|
||||
|
const pkg = { |
||||
|
structures, |
||||
|
TENANT_1: structures.TENANT_1, |
||||
|
mocks, |
||||
|
TestConfiguration, |
||||
|
API, |
||||
|
} |
||||
|
|
||||
|
export = pkg |
||||
@ -1,5 +0,0 @@ |
|||||
const email = require("./email") |
|
||||
|
|
||||
module.exports = { |
|
||||
email, |
|
||||
} |
|
||||
@ -0,0 +1,7 @@ |
|||||
|
const email = require("./email") |
||||
|
import { mocks as coreMocks } from "@budibase/backend-core/tests" |
||||
|
|
||||
|
export = { |
||||
|
email, |
||||
|
...coreMocks, |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
import { Account, AuthType, Hosting, CloudAccount } from "@budibase/types" |
||||
|
import { v4 as uuid } from "uuid" |
||||
|
import { utils } from "@budibase/backend-core" |
||||
|
|
||||
|
export const account = (): Account => { |
||||
|
return { |
||||
|
email: `${uuid()}@test.com`, |
||||
|
tenantId: utils.newid(), |
||||
|
hosting: Hosting.SELF, |
||||
|
authType: AuthType.SSO, |
||||
|
accountId: uuid(), |
||||
|
createdAt: Date.now(), |
||||
|
verified: true, |
||||
|
verificationSent: true, |
||||
|
tier: "FREE", |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const cloudAccount = (): CloudAccount => { |
||||
|
return { |
||||
|
...account(), |
||||
|
budibaseUserId: uuid(), |
||||
|
} |
||||
|
} |
||||
@ -1,14 +0,0 @@ |
|||||
const configs = require("./configs") |
|
||||
const users = require("./users") |
|
||||
const groups = require("./groups") |
|
||||
|
|
||||
const TENANT_ID = "default" |
|
||||
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306" |
|
||||
|
|
||||
module.exports = { |
|
||||
configs, |
|
||||
users, |
|
||||
TENANT_ID, |
|
||||
CSRF_TOKEN, |
|
||||
groups, |
|
||||
} |
|
||||
@ -0,0 +1,20 @@ |
|||||
|
import configs from "./configs" |
||||
|
import * as users from "./users" |
||||
|
import * as groups from "./groups" |
||||
|
import * as accounts from "./accounts" |
||||
|
|
||||
|
const TENANT_ID = "default" |
||||
|
const TENANT_1 = "tenant1" |
||||
|
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306" |
||||
|
|
||||
|
const pkg = { |
||||
|
configs, |
||||
|
users, |
||||
|
accounts, |
||||
|
TENANT_ID, |
||||
|
TENANT_1, |
||||
|
CSRF_TOKEN, |
||||
|
groups, |
||||
|
} |
||||
|
|
||||
|
export = pkg |
||||
Loading…
Reference in new issue