1
0
Fork 0
mirror of synced 2024-04-30 02:22:32 +12:00

remove QA core

This commit is contained in:
Martin McKeaveney 2024-04-02 17:24:30 +01:00
parent 89e081271f
commit 6b1829a7fc
118 changed files with 4 additions and 10943 deletions

View file

@ -34,7 +34,6 @@
},
{
"files": ["**/*.ts"],
"excludedFiles": ["qa-core/**"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended"],
@ -49,7 +48,6 @@
},
{
"files": ["**/*.spec.ts"],
"excludedFiles": ["qa-core/**"],
"parser": "@typescript-eslint/parser",
"plugins": ["jest", "@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:jest/recommended"],

View file

@ -175,35 +175,6 @@ jobs:
yarn test --scope=@budibase/server
fi
integration-test:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
- name: Use Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: yarn
- run: yarn --frozen-lockfile
- name: Build packages
run: yarn build --scope @budibase/server --scope @budibase/worker
- name: Build backend-core for OSS contributor (required for pro)
if: ${{ env.IS_OSS_CONTRIBUTOR == 'true' }}
run: yarn build --scope @budibase/backend-core
- name: Run tests
run: |
cd qa-core
yarn setup
yarn serve:test:self:ci
env:
BB_ADMIN_USER_EMAIL: admin
BB_ADMIN_USER_PASSWORD: admin
check-pro-submodule:
runs-on: ubuntu-latest
if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase')

1
.gitignore vendored
View file

@ -69,7 +69,6 @@ typings/
# dotenv environment variables file
.env
!qa-core/.env
!hosting/.env
# parcel-bundler cache (https://parceljs.org/)

View file

@ -58,11 +58,11 @@
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
"dev:docker": "yarn build --scope @budibase/server --scope @budibase/worker && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
"test": "lerna run --stream test --stream",
"lint:eslint": "eslint packages qa-core --max-warnings=0",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
"lint:eslint": "eslint packages --max-warnings=0",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
"lint": "yarn run lint:eslint && yarn run lint:prettier",
"lint:fix:eslint": "eslint --fix --max-warnings=0 packages qa-core",
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
"lint:fix:eslint": "eslint --fix --max-warnings=0 packages",
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
"lint:fix": "yarn run lint:fix:eslint && yarn run lint:fix:prettier",
"build:specs": "lerna run --stream specs",
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",

5
qa-core/.gitignore vendored
View file

@ -1,5 +0,0 @@
node_modules/
.env
watchtower-hook.json
dist/
testResults.json

View file

@ -1,28 +0,0 @@
# QA Core API Tests
The QA Core API tests are a jest suite that run directly against the budibase backend APIs.
## Auto Setup
You can run the whole test suite with one command, that spins up the budibase server and runs the jest tests:
`yarn test:ci`
## Setup Server
You can run the local development stack by following the instructions on the main readme.
## Run Tests
If you configured the server using the previous command, you can run the whole test suite by using:
`yarn test`
for watch mode, where the tests will run on every change:
`yarn test:watch`
To run tests locally against a cloud service you can update the configuration inside the `.env` file and run:
`yarn test`

View file

@ -1,21 +0,0 @@
import { Config } from "@jest/types"
const config: Config.InitialOptions = {
preset: "ts-jest",
setupFiles: ["./src/jest/jestSetup.ts"],
setupFilesAfterEnv: ["./src/jest/jest.extends.ts"],
testEnvironment: "node",
transform: {
"^.+\\.ts?$": "@swc/jest",
},
globalSetup: "./src/jest/globalSetup.ts",
globalTeardown: "./src/jest/globalTeardown.ts",
moduleNameMapper: {
"@budibase/types": "<rootDir>/../packages/types/src",
"@budibase/server": "<rootDir>/../packages/server/src",
"@budibase/backend-core": "<rootDir>/../packages/backend-core/src",
"@budibase/backend-core/(.*)": "<rootDir>/../packages/backend-core/$1",
},
}
export default config

View file

@ -1,49 +0,0 @@
{
"name": "@budibase/qa-core",
"email": "hi@budibase.com",
"version": "0.0.1",
"main": "index.js",
"description": "Budibase Integration Test Suite",
"repository": {
"type": "git",
"url": "https://github.com/Budibase/budibase.git"
},
"scripts": {
"setup": "yarn && node scripts/createEnv.js",
"user": "yarn && node scripts/createEnv.js && node scripts/createUser.js",
"test": "jest --runInBand --json --outputFile=testResults.json --forceExit",
"test:watch": "yarn run test --watch",
"test:debug": "DEBUG=1 yarn run test",
"test:notify": "node scripts/testResultsWebhook",
"test:cloud:prod": "yarn run test --testPathIgnorePatterns=\\.integration\\.",
"test:cloud:qa": "yarn run test",
"test:self:ci": "yarn run test --testPathIgnorePatterns=\\.integration\\. \\.cloud\\. \\.licensing\\.",
"serve:test:self:ci": "start-server-and-test dev:built http://localhost:4001/health test:self:ci",
"serve": "start-server-and-test dev:built http://localhost:4001/health",
"dev:built": "cd ../ && DISABLE_RATE_LIMITING=1 yarn dev:built"
},
"devDependencies": {
"@budibase/types": "^2.3.17",
"@swc/core": "1.3.71",
"@swc/jest": "0.2.27",
"@trendyol/jest-testcontainers": "2.1.1",
"@types/jest": "29.5.3",
"@types/node-fetch": "2.6.4",
"chance": "1.1.8",
"dotenv": "16.0.1",
"jest": "29.7.0",
"prettier": "2.7.1",
"start-server-and-test": "1.14.0",
"timekeeper": "2.2.0",
"ts-jest": "29.1.1",
"ts-node": "10.8.1",
"tsconfig-paths": "4.0.0",
"typescript": "5.2.2"
},
"dependencies": {
"@budibase/backend-core": "^2.3.17",
"form-data": "^4.0.0",
"node-fetch": "2.6.7",
"stripe": "^14.11.0"
}
}

View file

@ -1,26 +0,0 @@
#!/usr/bin/env node
const path = require("path")
const fs = require("fs")
function init() {
const envFilePath = path.join(process.cwd(), ".env")
if (!fs.existsSync(envFilePath)) {
const envFileJson = {
BUDIBASE_URL: "http://localhost:10000",
ACCOUNT_PORTAL_URL: "http://localhost:10001",
ACCOUNT_PORTAL_API_KEY: "budibase",
BB_ADMIN_USER_EMAIL: "admin",
BB_ADMIN_USER_PASSWORD: "admin",
LOG_LEVEL: "info",
JEST_TIMEOUT: "60000",
DISABLE_PINO_LOGGER: "1",
}
let envFile = ""
Object.keys(envFileJson).forEach(key => {
envFile += `${key}=${envFileJson[key]}\n`
})
fs.writeFileSync(envFilePath, envFile)
}
}
init()

View file

@ -1,49 +0,0 @@
const dotenv = require("dotenv")
const { join } = require("path")
const fs = require("fs")
const fetch = require("node-fetch")
function getVarFromDotEnv(path, varName) {
const parsed = dotenv.parse(fs.readFileSync(path))
return parsed[varName]
}
async function createUser() {
const serverPath = join(__dirname, "..", "..", "packages", "server", ".env")
const qaCorePath = join(__dirname, "..", ".env")
const apiKey = getVarFromDotEnv(serverPath, "INTERNAL_API_KEY")
const username = getVarFromDotEnv(qaCorePath, "BB_ADMIN_USER_EMAIL")
const password = getVarFromDotEnv(qaCorePath, "BB_ADMIN_USER_PASSWORD")
const url = getVarFromDotEnv(qaCorePath, "BUDIBASE_URL")
const resp = await fetch(`${url}/api/public/v1/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-budibase-api-key": apiKey,
},
body: JSON.stringify({
email: username,
password,
builder: {
global: true,
},
admin: {
global: true,
},
roles: {},
}),
})
if (resp.status !== 200) {
throw new Error(await resp.text())
} else {
return await resp.json()
}
}
createUser()
.then(() => {
console.log("User created - ready to use")
})
.catch(err => {
console.error("Failed to create user - ", err)
})

View file

@ -1,130 +0,0 @@
#!/usr/bin/env node
const fetch = require("node-fetch")
const path = require("path")
const fs = require("fs")
const WEBHOOK_URL = process.env.WEBHOOK_URL
const GIT_SHA = process.env.GITHUB_SHA
const GITHUB_ACTIONS_RUN_URL = process.env.GITHUB_ACTIONS_RUN_URL
async function generateReport() {
// read the report file
const REPORT_PATH = path.resolve(__dirname, "..", "testResults.json")
const report = fs.readFileSync(REPORT_PATH, "utf-8")
return JSON.parse(report)
}
const env = process.argv.slice(2)[0]
if (!env) {
throw new Error("environment argument is required")
}
async function discordResultsNotification(report) {
const {
numTotalTestSuites,
numTotalTests,
numPassedTests,
numPendingTests,
numFailedTests,
success,
startTime,
endTime,
} = report
const OUTCOME = success ? "success" : "failure"
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
content: `**Tests Status**: ${OUTCOME}`,
embeds: [
{
title: `Budi QA Bot - ${env}`,
description: `API Integration Tests`,
url: GITHUB_ACTIONS_RUN_URL,
color: OUTCOME === "success" ? 3066993 : 15548997,
timestamp: new Date(),
footer: {
icon_url: "http://bbui.budibase.com/budibase-logo.png",
text: "Budibase QA Bot",
},
thumbnail: {
url: "http://bbui.budibase.com/budibase-logo.png",
},
author: {
name: "Budibase QA Bot",
url: "https://discordapp.com",
icon_url: "http://bbui.budibase.com/budibase-logo.png",
},
fields: [
{
name: "Commit",
value: `https://github.com/Budibase/budibase/commit/${GIT_SHA}`,
},
{
name: "Github Actions Run URL",
value: GITHUB_ACTIONS_RUN_URL || "None Supplied",
},
{
name: "Test Suites",
value: numTotalTestSuites,
},
{
name: "Tests",
value: numTotalTests,
},
{
name: "Passed",
value: numPassedTests,
},
{
name: "Pending",
value: numPendingTests,
},
{
name: "Failures",
value: numFailedTests,
},
{
name: "Duration",
value: endTime
? `${(endTime - startTime) / 1000} Seconds`
: "DNF",
},
{
name: "Pass Percentage",
value: Math.floor((numPassedTests / numTotalTests) * 100),
},
],
},
],
}),
}
// Only post in discord when tests fail
if (success) {
return
}
const response = await fetch(WEBHOOK_URL, options)
if (response.status >= 201) {
const text = await response.text()
console.error(
`Error sending discord webhook. \nStatus: ${response.status}. \nResponse Body: ${text}. \nRequest Body: ${options.body}`
)
}
}
async function run() {
const report = await generateReport()
await discordResultsNotification(report)
}
run()

View file

@ -1,20 +0,0 @@
import AccountInternalAPIClient from "./AccountInternalAPIClient"
import { AccountAPI, LicenseAPI, AuthAPI, StripeAPI } from "./apis"
import { State } from "../../types"
export default class AccountInternalAPI {
client: AccountInternalAPIClient
auth: AuthAPI
accounts: AccountAPI
licenses: LicenseAPI
stripe: StripeAPI
constructor(state: State) {
this.client = new AccountInternalAPIClient(state)
this.auth = new AuthAPI(this.client)
this.accounts = new AccountAPI(this.client)
this.licenses = new LicenseAPI(this.client)
this.stripe = new StripeAPI(this.client)
}
}

View file

@ -1,89 +0,0 @@
import fetch, { Response, HeadersInit } from "node-fetch"
import env from "../../environment"
import { State } from "../../types"
import { Header } from "@budibase/backend-core"
type APIMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
interface ApiOptions {
method?: APIMethod
body?: object
headers?: HeadersInit | undefined
internal?: boolean
}
export default class AccountInternalAPIClient {
state: State
host: string
constructor(state: State) {
if (!env.ACCOUNT_PORTAL_URL) {
throw new Error("Must set ACCOUNT_PORTAL_URL env var")
}
if (!env.ACCOUNT_PORTAL_API_KEY) {
throw new Error("Must set ACCOUNT_PORTAL_API_KEY env var")
}
this.host = `${env.ACCOUNT_PORTAL_URL}`
this.state = state
}
apiCall =
(method: APIMethod) =>
async (url = "", options: ApiOptions = {}): Promise<[Response, any]> => {
const requestOptions = {
method,
body: JSON.stringify(options.body),
headers: {
"Content-Type": "application/json",
Accept: "application/json",
cookie: this.state.cookie,
redirect: "follow",
follow: 20,
...options.headers,
},
credentials: "include",
}
if (options.internal) {
requestOptions.headers = {
...requestOptions.headers,
...{ [Header.API_KEY]: env.ACCOUNT_PORTAL_API_KEY },
cookie: "",
}
}
// @ts-ignore
const response = await fetch(`${this.host}${url}`, requestOptions)
let body: any
const contentType = response.headers.get("content-type")
if (contentType && contentType.includes("application/json")) {
body = await response.json()
} else {
body = await response.text()
}
const data = {
request: requestOptions.body,
response: body,
}
const message = `${method} ${url} - ${response.status}`
const isDebug = process.env.LOG_LEVEL === "debug"
if (response.status > 499) {
console.error(message, data)
} else if (response.status >= 400) {
console.warn(message, data)
} else if (isDebug) {
console.debug(message, data)
}
return [response, body]
}
post = this.apiCall("POST")
get = this.apiCall("GET")
patch = this.apiCall("PATCH")
del = this.apiCall("DELETE")
put = this.apiCall("PUT")
}

View file

@ -1,123 +0,0 @@
import { Response } from "node-fetch"
import {
Account,
CreateAccountRequest,
SearchAccountsRequest,
SearchAccountsResponse,
} from "@budibase/types"
import AccountInternalAPIClient from "../AccountInternalAPIClient"
import { APIRequestOpts } from "../../../types"
import { Header } from "@budibase/backend-core"
import BaseAPI from "./BaseAPI"
export default class AccountAPI extends BaseAPI {
client: AccountInternalAPIClient
constructor(client: AccountInternalAPIClient) {
super()
this.client = client
}
async validateEmail(email: string, opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.post(`/api/accounts/validate/email`, {
body: { email },
})
}, opts)
}
async validateTenantId(
tenantId: string,
opts: APIRequestOpts = { status: 200 }
) {
return this.doRequest(() => {
return this.client.post(`/api/accounts/validate/tenantId`, {
body: { tenantId },
})
}, opts)
}
async create(
body: CreateAccountRequest,
opts: APIRequestOpts & { autoVerify: boolean } = {
status: 201,
autoVerify: false,
}
): Promise<[Response, Account]> {
return this.doRequest(() => {
const headers = {
"no-verify": opts.autoVerify ? "1" : "0",
}
return this.client.post(`/api/accounts`, {
body,
headers,
})
}, opts)
}
async delete(accountID: string, opts: APIRequestOpts = { status: 204 }) {
return this.doRequest(() => {
return this.client.del(`/api/accounts/${accountID}`, {
internal: true,
})
}, opts)
}
async deleteCurrentAccount(opts: APIRequestOpts = { status: 204 }) {
return this.doRequest(() => {
return this.client.del(`/api/accounts`)
}, opts)
}
async verifyAccount(
verificationCode: string,
opts: APIRequestOpts = { status: 200 }
) {
return this.doRequest(() => {
return this.client.post(`/api/accounts/verify`, {
body: { verificationCode },
})
}, opts)
}
async sendVerificationEmail(
email: string,
opts: APIRequestOpts = { status: 200 }
): Promise<[Response, string]> {
return this.doRequest(async () => {
const [response] = await this.client.post(`/api/accounts/verify/send`, {
body: { email },
headers: {
[Header.RETURN_VERIFICATION_CODE]: "1",
},
})
const code = response.headers.get(Header.VERIFICATION_CODE)
return [response, code]
}, opts)
}
async search(
searchType: string,
search: "email" | "tenantId",
opts: APIRequestOpts = { status: 200 }
): Promise<[Response, SearchAccountsResponse]> {
return this.doRequest(() => {
let body: SearchAccountsRequest = {}
if (search === "email") {
body.email = searchType
} else if (search === "tenantId") {
body.tenantId = searchType
}
return this.client.post(`/api/accounts/search`, {
body,
internal: true,
})
}, opts)
}
async self(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.get(`/api/auth/self`)
}, opts)
}
}

View file

@ -1,68 +0,0 @@
import { Response } from "node-fetch"
import AccountInternalAPIClient from "../AccountInternalAPIClient"
import { APIRequestOpts } from "../../../types"
import BaseAPI from "./BaseAPI"
import { Header } from "@budibase/backend-core"
export default class AuthAPI extends BaseAPI {
client: AccountInternalAPIClient
constructor(client: AccountInternalAPIClient) {
super()
this.client = client
}
async login(
email: string,
password: string,
opts: APIRequestOpts = { doExpect: true, status: 200 }
): Promise<[Response, string]> {
return this.doRequest(async () => {
const [res] = await this.client.post(`/api/auth/login`, {
body: {
email: email,
password: password,
},
})
const cookie = res.headers.get("set-cookie")
return [res, cookie]
}, opts)
}
async logout(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.post(`/api/auth/logout`)
}, opts)
}
async resetPassword(
email: string,
opts: APIRequestOpts = { status: 200 }
): Promise<[Response, string]> {
return this.doRequest(async () => {
const [response] = await this.client.post(`/api/auth/reset`, {
body: { email },
headers: {
[Header.RETURN_RESET_PASSWORD_CODE]: "1",
},
})
const code = response.headers.get(Header.RESET_PASSWORD_CODE)
return [response, code]
}, opts)
}
async resetPasswordUpdate(
resetCode: string,
password: string,
opts: APIRequestOpts = { status: 200 }
) {
return this.doRequest(() => {
return this.client.post(`/api/auth/reset/update`, {
body: {
resetCode: resetCode,
password: password,
},
})
}, opts)
}
}

View file

@ -1,20 +0,0 @@
import { Response } from "node-fetch"
import { APIRequestOpts } from "../../../types"
export default class BaseAPI {
async doRequest(
request: () => Promise<[Response, any]>,
opts: APIRequestOpts
): Promise<[Response, any]> {
const [response, body] = await request()
// do expect on by default
if (opts.doExpect === undefined) {
opts.doExpect = true
}
if (opts.doExpect && opts.status) {
expect(response).toHaveStatusCode(opts.status)
}
return [response, body]
}
}

View file

@ -1,140 +0,0 @@
import AccountInternalAPIClient from "../AccountInternalAPIClient"
import {
Account,
CreateOfflineLicenseRequest,
GetLicenseKeyResponse,
GetOfflineLicenseResponse,
UpdateLicenseRequest,
} from "@budibase/types"
import { Response } from "node-fetch"
import BaseAPI from "./BaseAPI"
import { APIRequestOpts } from "../../../types"
export default class LicenseAPI extends BaseAPI {
client: AccountInternalAPIClient
constructor(client: AccountInternalAPIClient) {
super()
this.client = client
}
async updateLicense(
accountId: string,
body: UpdateLicenseRequest,
opts: APIRequestOpts = { status: 200 }
): Promise<[Response, Account]> {
return this.doRequest(() => {
return this.client.put(`/api/accounts/${accountId}/license`, {
body,
internal: true,
})
}, opts)
}
// TODO: Better approach for setting tenant id header
async createOfflineLicense(
accountId: string,
tenantId: string,
body: CreateOfflineLicenseRequest,
opts: { status?: number } = {}
): Promise<Response> {
const [response, json] = await this.client.post(
`/api/internal/accounts/${accountId}/license/offline`,
{
body,
internal: true,
headers: {
"x-budibase-tenant-id": tenantId,
},
}
)
expect(response.status).toBe(opts.status ? opts.status : 201)
return response
}
async getOfflineLicense(
accountId: string,
tenantId: string,
opts: { status?: number } = {}
): Promise<[Response, GetOfflineLicenseResponse]> {
const [response, json] = await this.client.get(
`/api/internal/accounts/${accountId}/license/offline`,
{
internal: true,
headers: {
"x-budibase-tenant-id": tenantId,
},
}
)
expect(response.status).toBe(opts.status ? opts.status : 200)
return [response, json]
}
async getLicenseKey(
opts: { status?: number } = {}
): Promise<[Response, GetLicenseKeyResponse]> {
const [response, json] = await this.client.get(`/api/license/key`)
expect(response.status).toBe(opts.status || 200)
return [response, json]
}
async activateLicense(
apiKey: string,
tenantId: string,
licenseKey: string,
opts: APIRequestOpts = { status: 200 }
) {
return this.doRequest(() => {
return this.client.post(`/api/license/activate`, {
body: {
apiKey: apiKey,
tenantId: tenantId,
licenseKey: licenseKey,
},
})
}, opts)
}
async regenerateLicenseKey(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.post(`/api/license/key/regenerate`, {})
}, opts)
}
async getPlans(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.get(`/api/plans`)
}, opts)
}
async updatePlan(priceId: string, opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.put(`/api/license/plan`, {
body: { priceId },
})
}, opts)
}
async refreshAccountLicense(
accountId: string,
opts: { status?: number } = {}
): Promise<Response> {
const [response, json] = await this.client.post(
`/api/accounts/${accountId}/license/refresh`,
{
internal: true,
}
)
expect(response.status).toBe(opts.status ? opts.status : 201)
return response
}
async getLicenseUsage(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.get(`/api/license/usage`)
}, opts)
}
async licenseUsageTriggered(
opts: { status?: number } = {}
): Promise<Response> {
const [response, json] = await this.client.post(
`/api/license/usage/triggered`
)
expect(response.status).toBe(opts.status ? opts.status : 201)
return response
}
}

View file

@ -1,74 +0,0 @@
import AccountInternalAPIClient from "../AccountInternalAPIClient"
import BaseAPI from "./BaseAPI"
import { APIRequestOpts } from "../../../types"
export default class StripeAPI extends BaseAPI {
client: AccountInternalAPIClient
constructor(client: AccountInternalAPIClient) {
super()
this.client = client
}
async createCheckoutSession(
price: object,
opts: APIRequestOpts = { status: 200 }
) {
return this.doRequest(() => {
return this.client.post(`/api/stripe/checkout-session`, {
body: { prices: [price] },
})
}, opts)
}
async checkoutSuccess(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.post(`/api/stripe/checkout-success`)
}, opts)
}
async createPortalSession(
stripeCustomerId: string,
opts: APIRequestOpts = { status: 200 }
) {
return this.doRequest(() => {
return this.client.post(`/api/stripe/portal-session`, {
body: { stripeCustomerId },
})
}, opts)
}
async linkStripeCustomer(
accountId: string,
stripeCustomerId: string,
opts: APIRequestOpts = { status: 200 }
) {
return this.doRequest(() => {
return this.client.post(`/api/stripe/link`, {
body: {
accountId,
stripeCustomerId,
},
internal: true,
})
}, opts)
}
async getInvoices(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.get(`/api/stripe/invoices`)
}, opts)
}
async getUpcomingInvoice(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.get(`/api/stripe/upcoming-invoice`)
}, opts)
}
async getStripeCustomers(opts: APIRequestOpts = { status: 200 }) {
return this.doRequest(() => {
return this.client.get(`/api/stripe/customers`)
}, opts)
}
}

View file

@ -1,4 +0,0 @@
export { default as AuthAPI } from "./AuthAPI"
export { default as AccountAPI } from "./AccountAPI"
export { default as LicenseAPI } from "./LicenseAPI"
export { default as StripeAPI } from "./StripeAPI"

View file

@ -1 +0,0 @@
export { default as AccountInternalAPI } from "./AccountInternalAPI"

View file

@ -1,29 +0,0 @@
import { AccountInternalAPI } from "../api"
import { BudibaseTestConfiguration } from "../../shared"
export default class TestConfiguration<T> extends BudibaseTestConfiguration {
// apis
api: AccountInternalAPI
context: T
constructor() {
super()
this.api = new AccountInternalAPI(this.state)
this.context = <T>{}
}
async beforeAll() {
await super.beforeAll()
await this.setApiKey()
}
async afterAll() {
await super.afterAll()
}
async setApiKey() {
const apiKeyResponse = await this.internalApi.self.getApiKey()
this.state.apiKey = apiKeyResponse.apiKey
}
}

View file

@ -1,24 +0,0 @@
import { generator } from "../../shared"
import { Hosting, CreateAccountRequest } from "@budibase/types"
// TODO: Refactor me to central location
export const generateAccount = (
partial: Partial<CreateAccountRequest>
): CreateAccountRequest => {
const uuid = generator.guid()
const email = `${uuid}@budibase.com`
const tenant = `tenant${uuid.replace(/-/g, "")}`
return {
email,
hosting: Hosting.CLOUD,
name: email,
password: uuid,
profession: "software_engineer",
size: "10+",
tenantId: tenant,
tenantName: tenant,
...partial,
}
}

View file

@ -1 +0,0 @@
export * as accounts from "./accounts"

View file

@ -1 +0,0 @@
export * from "./api"

View file

@ -1,32 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
import { generator } from "../../../shared"
import { Hosting } from "@budibase/types"
describe("Account Internal Operations", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("performs account deletion by ID", async () => {
// Deleting by unknown id doesn't work
const accountId = generator.guid()
await config.api.accounts.delete(accountId, { status: 404 })
// Create new account
const [_, account] = await config.api.accounts.create({
...fixtures.accounts.generateAccount({
hosting: Hosting.CLOUD,
}),
})
// New account can be deleted
await config.api.accounts.delete(account.accountId)
})
})

View file

@ -1,102 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
import { generator } from "../../../shared"
import { Hosting } from "@budibase/types"
describe("Accounts", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("performs signup and deletion flow", async () => {
await config.doInNewState(async () => {
// Create account
const createAccountRequest = fixtures.accounts.generateAccount({
hosting: Hosting.CLOUD,
})
const email = createAccountRequest.email
const tenantId = createAccountRequest.tenantId
// Validation - email and tenant ID allowed
await config.api.accounts.validateEmail(email)
await config.api.accounts.validateTenantId(tenantId)
// Create unverified account
await config.api.accounts.create(createAccountRequest)
// Validation - email and tenant ID no longer valid
await config.api.accounts.validateEmail(email, { status: 400 })
await config.api.accounts.validateTenantId(tenantId, { status: 400 })
// Attempt to log in using unverified account
await config.loginAsAccount(createAccountRequest, { status: 400 })
// Re-send verification email to get access to code
const [_, code] = await config.accountsApi.accounts.sendVerificationEmail(
email
)
// Send the verification request
await config.accountsApi.accounts.verifyAccount(code!)
// Verify self response is unauthorized
await config.api.accounts.self({ status: 403 })
// Can now log in to the account
await config.loginAsAccount(createAccountRequest)
// Verify self response matches account
const [selfRes, selfBody] = await config.api.accounts.self()
expect(selfBody.email).toBe(email)
// Delete account
await config.api.accounts.deleteCurrentAccount()
// Can't log in
await config.loginAsAccount(createAccountRequest, { status: 403 })
})
})
describe("Searching accounts", () => {
it("search by tenant ID", async () => {
const tenantId = generator.string()
// Empty result
const [_, emptyBody] = await config.api.accounts.search(
tenantId,
"tenantId"
)
expect(emptyBody.length).toBe(0)
// Hit result
const [hitRes, hitBody] = await config.api.accounts.search(
config.state.tenantId!,
"tenantId"
)
expect(hitBody.length).toBe(1)
expect(hitBody[0].tenantId).toBe(config.state.tenantId)
})
it("searches by email", async () => {
const email = generator.email({ domain: "example.com" })
// Empty result
const [_, emptyBody] = await config.api.accounts.search(email, "email")
expect(emptyBody.length).toBe(0)
// Hit result
const [hitRes, hitBody] = await config.api.accounts.search(
config.state.email!,
"email"
)
expect(hitBody.length).toBe(1)
expect(hitBody[0].email).toBe(config.state.email)
})
})
})

View file

@ -1,46 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
import { generator } from "../../../shared"
import { Hosting } from "@budibase/types"
describe("Password Management", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("performs password reset flow", async () => {
// Create account
const createAccountRequest = fixtures.accounts.generateAccount({
hosting: Hosting.CLOUD,
})
await config.api.accounts.create(createAccountRequest, { autoVerify: true })
// Request password reset to get code
const [_, code] = await config.api.auth.resetPassword(
createAccountRequest.email
)
// Change password using code
const password = generator.string()
await config.api.auth.resetPasswordUpdate(code, password)
// Login using the new password
await config.api.auth.login(createAccountRequest.email, password)
// Logout of account
await config.api.auth.logout()
// Cannot log in using old password
await config.api.auth.login(
createAccountRequest.email,
createAccountRequest.password,
{ status: 403 }
)
})
})

View file

@ -1,68 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixures from "../../fixtures"
import { Feature, Hosting } from "@budibase/types"
describe("license activation", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("creates, activates and deletes online license - self host", async () => {
// Remove existing license key
await config.internalApi.license.deleteLicenseKey()
// Verify license key not found
await config.internalApi.license.getLicenseKey({ status: 404 })
// Create self host account
const createAccountRequest = fixures.accounts.generateAccount({
hosting: Hosting.SELF,
})
const [createAccountRes, account] =
await config.accountsApi.accounts.create(createAccountRequest, {
autoVerify: true,
})
let licenseKey: string = " "
await config.doInNewState(async () => {
await config.loginAsAccount(createAccountRequest)
// Retrieve license key
const [res, body] = await config.accountsApi.licenses.getLicenseKey()
licenseKey = body.licenseKey
})
const accountId = account.accountId!
// Update license to have paid feature
const [res, acc] = await config.accountsApi.licenses.updateLicense(
accountId,
{
overrides: {
features: [Feature.APP_BACKUPS],
},
}
)
// Activate license key
await config.internalApi.license.activateLicenseKey({ licenseKey })
// Verify license updated with new feature
await config.doInNewState(async () => {
await config.loginAsAccount(createAccountRequest)
const [selfRes, body] = await config.api.accounts.self()
expect(body.license.features[0]).toBe("appBackups")
})
// Remove license key
await config.internalApi.license.deleteLicenseKey()
// Verify license key not found
await config.internalApi.license.getLicenseKey({ status: 404 })
})
})

View file

@ -1,116 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
import { Hosting, PlanType } from "@budibase/types"
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY)
describe("license management", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("retrieves plans, creates checkout session, and updates license", async () => {
// Create cloud account
const createAccountRequest = fixtures.accounts.generateAccount({
hosting: Hosting.CLOUD,
})
const [createAccountRes, account] =
await config.accountsApi.accounts.create(createAccountRequest, {
autoVerify: true,
})
// Self response has free license
await config.doInNewState(async () => {
await config.loginAsAccount(createAccountRequest)
const [selfRes, selfBody] = await config.api.accounts.self()
expect(selfBody.license.plan.type).toBe(PlanType.FREE)
})
// Retrieve plans
const [plansRes, planBody] = await config.api.licenses.getPlans()
// Select priceId from premium plan
let premiumPrice = null
let businessPriceId: ""
for (const plan of planBody) {
if (plan.type === PlanType.PREMIUM_PLUS) {
premiumPrice = plan.prices[0]
}
if (plan.type === PlanType.ENTERPRISE_BASIC) {
businessPriceId = plan.prices[0].priceId
}
}
// Create checkout session for price
const checkoutSessionRes = await config.api.stripe.createCheckoutSession({
id: premiumPrice.priceId,
type: premiumPrice.type,
})
const checkoutSessionUrl = checkoutSessionRes[1].url
expect(checkoutSessionUrl).toContain("checkout.stripe.com")
// Create stripe customer
const customer = await stripe.customers.create({
email: createAccountRequest.email,
})
// Create payment method
const paymentMethod = await stripe.paymentMethods.create({
type: "card",
card: {
token: "tok_visa", // Test Visa Card
},
})
// Attach payment method to customer
await stripe.paymentMethods.attach(paymentMethod.id, {
customer: customer.id,
})
// Update customer
await stripe.customers.update(customer.id, {
invoice_settings: {
default_payment_method: paymentMethod.id,
},
})
// Create subscription for premium plan
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [
{
price: premiumPrice.priceId,
quantity: 1,
},
],
default_payment_method: paymentMethod.id,
collection_method: "charge_automatically",
})
await config.doInNewState(async () => {
// License updated from Free to Premium
await config.loginAsAccount(createAccountRequest)
await config.api.stripe.linkStripeCustomer(account.accountId, customer.id)
const [_, selfBodyPremium] = await config.api.accounts.self()
expect(selfBodyPremium.license.plan.type).toBe(PlanType.PREMIUM_PLUS)
// Create portal session - Check URL
const [portalRes, portalSessionBody] =
await config.api.stripe.createPortalSession(customer.id)
expect(portalSessionBody.url).toContain("billing.stripe.com")
// Update subscription from premium to business license
await config.api.licenses.updatePlan(businessPriceId)
// License updated to Business
const [selfRes, selfBodyBusiness] = await config.api.accounts.self()
expect(selfBodyBusiness.license.plan.type).toBe(PlanType.ENTERPRISE_BASIC)
})
})
})

View file

@ -1,79 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixures from "../../fixtures"
import { Hosting, Feature } from "@budibase/types"
describe("offline", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
// TODO: Currently requires a self host install + account portal
// Ignored until we set this up
it.skip("creates, activates and deletes offline license", async () => {
// installation: Delete any token
await config.internalApi.license.deleteOfflineLicenseToken()
// installation: Assert token not found
let [getTokenRes] = await config.internalApi.license.getOfflineLicenseToken(
{ status: 404 }
)
// installation: Retrieve Identifier
const [getIdentifierRes, identifier] =
await config.internalApi.license.getOfflineIdentifier()
// account-portal: Create self-host account
const createAccountRequest = fixures.accounts.generateAccount({
hosting: Hosting.SELF,
})
const [createAccountRes, account] =
await config.accountsApi.accounts.create(createAccountRequest)
const accountId = account.accountId!
const tenantId = account.tenantId!
// account-portal: Enable feature on license
await config.accountsApi.licenses.updateLicense(accountId, {
overrides: {
features: [Feature.OFFLINE],
},
})
// account-portal: Create offline token
const expireAt = new Date()
expireAt.setDate(new Date().getDate() + 1)
await config.accountsApi.licenses.createOfflineLicense(
accountId,
tenantId,
{
expireAt: expireAt.toISOString(),
installationIdentifierBase64: identifier.identifierBase64,
}
)
// account-portal: Retrieve offline token
const [getLicenseRes, offlineLicense] =
await config.accountsApi.licenses.getOfflineLicense(accountId, tenantId)
// installation: Activate offline token
await config.internalApi.license.activateOfflineLicenseToken({
offlineLicenseToken: offlineLicense.offlineLicenseToken,
})
// installation: Assert token found
await config.internalApi.license.getOfflineLicenseToken()
// TODO: Assert on license for current user
// installation: Remove the token
await config.internalApi.license.deleteOfflineLicenseToken()
// installation: Assert token not found
await config.internalApi.license.getOfflineLicenseToken({ status: 404 })
})
})

View file

@ -1,34 +0,0 @@
import { join } from "path"
let LOADED = false
if (!LOADED) {
require("dotenv").config({
path: join(__dirname, "..", ".env"),
})
LOADED = true
}
const env = {
BUDIBASE_URL: process.env.BUDIBASE_URL,
ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL,
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY,
BB_ADMIN_USER_EMAIL: process.env.BB_ADMIN_USER_EMAIL,
BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD,
POSTGRES_HOST: process.env.POSTGRES_HOST,
POSTGRES_PORT: process.env.POSTGRES_PORT,
POSTGRES_DB: process.env.POSTGRES_DB,
POSTGRES_USER: process.env.POSTGRES_USER,
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD,
MONGODB_CONNECTION_STRING: process.env.MONGODB_CONNECTION_STRING,
MONGODB_DB: process.env.MONGODB_DB,
REST_API_BASE_URL: process.env.REST_API_BASE_URL,
REST_API_KEY: process.env.REST_API_KEY,
MARIADB_HOST: process.env.MARIADB_HOST,
MARIADB_PORT: process.env.MARIADB_PORT,
MARIADB_DB: process.env.MARIADB_DB,
MARIADB_USER: process.env.MARIADB_USER,
MARIADB_PASSWORD: process.env.MARIADB_PASSWORD,
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
}
export = env

View file

@ -1,112 +0,0 @@
import { GenericContainer, Wait } from "testcontainers"
import { Duration, TemporalUnit } from "node-duration"
import mssql from "../../../../packages/server/src/integrations/microsoftSqlServer"
jest.unmock("mssql")
describe("getExternalSchema", () => {
describe("mssql", () => {
let config: any
beforeAll(async () => {
const password = "Str0Ng_p@ssW0rd!"
const container = await new GenericContainer(
"mcr.microsoft.com/mssql/server"
)
.withExposedPorts(1433)
.withEnv("ACCEPT_EULA", "Y")
.withEnv("MSSQL_SA_PASSWORD", password)
.withEnv("MSSQL_PID", "Developer")
.withWaitStrategy(Wait.forHealthCheck())
.withHealthCheck({
test: `/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "${password}" -Q "SELECT 1" -b -o /dev/null`,
interval: new Duration(1000, TemporalUnit.MILLISECONDS),
timeout: new Duration(3, TemporalUnit.SECONDS),
retries: 20,
startPeriod: new Duration(100, TemporalUnit.MILLISECONDS),
})
.start()
const host = container.getContainerIpAddress()
const port = container.getMappedPort(1433)
config = {
user: "sa",
password,
server: host,
port: port,
database: "master",
schema: "dbo",
}
})
it("can export an empty database", async () => {
const integration = new mssql.integration(config)
const result = await integration.getExternalSchema()
expect(result).toMatchInlineSnapshot(`""`)
})
it("can export a database with tables", async () => {
const integration = new mssql.integration(config)
await integration.connect()
await integration.internalQuery({
sql: `
CREATE TABLE users (
id INT IDENTITY(1,1) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
role VARCHAR(15) NOT NULL
);
CREATE TABLE products (
id INT IDENTITY(1,1) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price DECIMAL(10, 2) NOT NULL
);
`,
})
const result = await integration.getExternalSchema()
expect(result).toMatchInlineSnapshot(`
"CREATE TABLE [products] (
id int(4) NOT NULL,
name varchar(100) NOT NULL,
price decimal(9) NOT NULL,
CONSTRAINT [PK_products] PRIMARY KEY (id)
);
CREATE TABLE [users] (
id int(4) NOT NULL,
name varchar(100) NOT NULL,
role varchar(15) NOT NULL,
CONSTRAINT [PK_users] PRIMARY KEY (id)
);"
`)
})
it("does not export a data", async () => {
const integration = new mssql.integration(config)
await integration.connect()
await integration.internalQuery({
sql: `INSERT INTO [users] ([name], [role]) VALUES ('John Doe', 'Administrator');
INSERT INTO [products] ([name], [price]) VALUES ('Book', 7.68);
`,
})
const result = await integration.getExternalSchema()
expect(result).toMatchInlineSnapshot(`
"CREATE TABLE [products] (
id int(4) NOT NULL,
name varchar(100) NOT NULL,
price decimal(9) NOT NULL,
CONSTRAINT [PK_products] PRIMARY KEY (id)
);
CREATE TABLE [users] (
id int(4) NOT NULL,
name varchar(100) NOT NULL,
role varchar(15) NOT NULL,
CONSTRAINT [PK_users] PRIMARY KEY (id)
);"
`)
})
})
})

View file

@ -1,106 +0,0 @@
import { GenericContainer } from "testcontainers"
import mysql from "../../../../packages/server/src/integrations/mysql"
describe("datasource validators", () => {
describe("mysql", () => {
let config: any
beforeAll(async () => {
const container = await new GenericContainer("mysql:8.3")
.withExposedPorts(3306)
.withEnv("MYSQL_ROOT_PASSWORD", "admin")
.withEnv("MYSQL_DATABASE", "db")
.withEnv("MYSQL_USER", "user")
.withEnv("MYSQL_PASSWORD", "password")
.start()
const host = container.getContainerIpAddress()
const port = container.getMappedPort(3306)
config = {
host,
port,
user: "user",
database: "db",
password: "password",
rejectUnauthorized: true,
}
})
it("can export an empty database", async () => {
const integration = new mysql.integration(config)
const result = await integration.getExternalSchema()
expect(result).toMatchInlineSnapshot(
`"CREATE DATABASE \`db\` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */"`
)
})
it("can export a database with tables", async () => {
const integration = new mysql.integration(config)
await integration.internalQuery({
sql: `
CREATE TABLE users (
id INT AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
role VARCHAR(15) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE products (
id INT AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
price DECIMAL,
PRIMARY KEY (id)
);
`,
})
const result = await integration.getExternalSchema()
expect(result).toMatchInlineSnapshot(`
"CREATE DATABASE \`db\` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */
CREATE TABLE \`products\` (
\`id\` int NOT NULL AUTO_INCREMENT,
\`name\` varchar(100) NOT NULL,
\`price\` decimal(10,0) DEFAULT NULL,
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
CREATE TABLE \`users\` (
\`id\` int NOT NULL AUTO_INCREMENT,
\`name\` varchar(100) NOT NULL,
\`role\` varchar(15) NOT NULL,
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci"
`)
})
it("does not export a data", async () => {
const integration = new mysql.integration(config)
await integration.internalQuery({
sql: `INSERT INTO users (name, role) VALUES ('John Doe', 'Administrator');`,
})
await integration.internalQuery({
sql: `INSERT INTO products (name, price) VALUES ('Book', 7.68);`,
})
const result = await integration.getExternalSchema()
expect(result).toMatchInlineSnapshot(`
"CREATE DATABASE \`db\` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */
CREATE TABLE \`products\` (
\`id\` int NOT NULL AUTO_INCREMENT,
\`name\` varchar(100) NOT NULL,
\`price\` decimal(10,0) DEFAULT NULL,
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
CREATE TABLE \`users\` (
\`id\` int NOT NULL AUTO_INCREMENT,
\`name\` varchar(100) NOT NULL,
\`role\` varchar(15) NOT NULL,
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci"
`)
})
})
})

View file

@ -1,376 +0,0 @@
import { GenericContainer } from "testcontainers"
import postgres from "../../../../packages/server/src/integrations/postgres"
jest.unmock("pg")
describe("getExternalSchema", () => {
describe("postgres", () => {
let config: any
// Remove versioning from the outputs to prevent failures when running different pg_dump versions
function stripResultsVersions(sql: string) {
const result = sql
.replace(/\n[^\n]+Dumped from database version[^\n]+\n/, "")
.replace(/\n[^\n]+Dumped by pg_dump version[^\n]+\n/, "")
.toString()
return result
}
beforeAll(async () => {
const container = await new GenericContainer("postgres:16.1-bullseye")
.withExposedPorts(5432)
.withEnv("POSTGRES_PASSWORD", "password")
.start()
const host = container.getContainerIpAddress()
const port = container.getMappedPort(5432)
config = {
host,
port,
database: "postgres",
user: "postgres",
password: "password",
schema: "public",
ssl: false,
rejectUnauthorized: false,
}
})
it("can export an empty database", async () => {
const integration = new postgres.integration(config)
const result = await integration.getExternalSchema()
expect(stripResultsVersions(result)).toMatchInlineSnapshot(`
"--
-- PostgreSQL database dump
--
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- PostgreSQL database dump complete
--
"
`)
})
it("can export a database with tables", async () => {
const integration = new postgres.integration(config)
await integration.internalQuery(
{
sql: `
CREATE TABLE "users" (
"id" SERIAL,
"name" VARCHAR(100) NOT NULL,
"role" VARCHAR(15) NOT NULL,
PRIMARY KEY ("id")
);
CREATE TABLE "products" (
"id" SERIAL,
"name" VARCHAR(100) NOT NULL,
"price" DECIMAL NOT NULL,
"owner" INTEGER NULL,
PRIMARY KEY ("id")
);
ALTER TABLE "products" ADD CONSTRAINT "fk_owner" FOREIGN KEY ("owner") REFERENCES "users" ("id");`,
},
false
)
const result = await integration.getExternalSchema()
expect(stripResultsVersions(result)).toMatchInlineSnapshot(`
"--
-- PostgreSQL database dump
--
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- Name: products; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.products (
id integer NOT NULL,
name character varying(100) NOT NULL,
price numeric NOT NULL,
owner integer
);
ALTER TABLE public.products OWNER TO postgres;
--
-- Name: products_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.products_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE public.products_id_seq OWNER TO postgres;
--
-- Name: products_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.products_id_seq OWNED BY public.products.id;
--
-- Name: users; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.users (
id integer NOT NULL,
name character varying(100) NOT NULL,
role character varying(15) NOT NULL
);
ALTER TABLE public.users OWNER TO postgres;
--
-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.users_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE public.users_id_seq OWNER TO postgres;
--
-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
--
-- Name: products id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.products ALTER COLUMN id SET DEFAULT nextval('public.products_id_seq'::regclass);
--
-- Name: users id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);
--
-- Name: products products_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.products
ADD CONSTRAINT products_pkey PRIMARY KEY (id);
--
-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
--
-- Name: products fk_owner; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.products
ADD CONSTRAINT fk_owner FOREIGN KEY (owner) REFERENCES public.users(id);
--
-- PostgreSQL database dump complete
--
"
`)
})
it("does not export a data", async () => {
const integration = new postgres.integration(config)
await integration.internalQuery(
{
sql: `INSERT INTO "users" ("name", "role") VALUES ('John Doe', 'Administrator');
INSERT INTO "products" ("name", "price") VALUES ('Book', 7.68);`,
},
false
)
const result = await integration.getExternalSchema()
expect(stripResultsVersions(result)).toMatchInlineSnapshot(`
"--
-- PostgreSQL database dump
--
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- Name: products; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.products (
id integer NOT NULL,
name character varying(100) NOT NULL,
price numeric NOT NULL,
owner integer
);
ALTER TABLE public.products OWNER TO postgres;
--
-- Name: products_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.products_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE public.products_id_seq OWNER TO postgres;
--
-- Name: products_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.products_id_seq OWNED BY public.products.id;
--
-- Name: users; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.users (
id integer NOT NULL,
name character varying(100) NOT NULL,
role character varying(15) NOT NULL
);
ALTER TABLE public.users OWNER TO postgres;
--
-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.users_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE public.users_id_seq OWNER TO postgres;
--
-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
--
-- Name: products id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.products ALTER COLUMN id SET DEFAULT nextval('public.products_id_seq'::regclass);
--
-- Name: users id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);
--
-- Name: products products_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.products
ADD CONSTRAINT products_pkey PRIMARY KEY (id);
--
-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
--
-- Name: products fk_owner; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.products
ADD CONSTRAINT fk_owner FOREIGN KEY (owner) REFERENCES public.users(id);
--
-- PostgreSQL database dump complete
--
"
`)
})
})
})

View file

@ -1,77 +0,0 @@
import { GenericContainer, Wait } from "testcontainers"
import arangodb from "../../../../packages/server/src/integrations/arangodb"
import { generator } from "../../shared"
jest.unmock("arangojs")
describe("datasource validators", () => {
describe("arangodb", () => {
let connectionSettings: {
user: string
password: string
url: string
}
beforeAll(async () => {
const user = "root"
const password = generator.hash()
const container = await new GenericContainer("arangodb")
.withExposedPorts(8529)
.withEnv("ARANGO_ROOT_PASSWORD", password)
.withWaitStrategy(
Wait.forLogMessage("is ready for business. Have fun!")
)
.start()
connectionSettings = {
user,
password,
url: `http://${container.getContainerIpAddress()}:${container.getMappedPort(
8529
)}`,
}
})
it("test valid connection string", async () => {
const integration = new arangodb.integration({
url: connectionSettings.url,
username: connectionSettings.user,
password: connectionSettings.password,
databaseName: "",
collection: "",
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test wrong password", async () => {
const integration = new arangodb.integration({
url: connectionSettings.url,
username: connectionSettings.user,
password: "wrong",
databaseName: "",
collection: "",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "not authorized to execute this request",
})
})
it("test wrong url", async () => {
const integration = new arangodb.integration({
url: "http://not.here",
username: connectionSettings.user,
password: connectionSettings.password,
databaseName: "",
collection: "",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "getaddrinfo ENOTFOUND not.here",
})
})
})
})

View file

@ -1,67 +0,0 @@
import { GenericContainer } from "testcontainers"
import couchdb from "../../../../packages/server/src/integrations/couchdb"
import { generator } from "../../shared"
describe("datasource validators", () => {
describe("couchdb", () => {
let url: string
beforeAll(async () => {
const user = generator.first()
const password = generator.hash()
const container = await new GenericContainer("budibase/couchdb")
.withExposedPorts(5984)
.withEnv("COUCHDB_USER", user)
.withEnv("COUCHDB_PASSWORD", password)
.start()
const host = container.getContainerIpAddress()
const port = container.getMappedPort(5984)
await container.exec([
`curl`,
`-u`,
`${user}:${password}`,
`-X`,
`PUT`,
`localhost:5984/db`,
])
url = `http://${user}:${password}@${host}:${port}`
})
it("test valid connection string", async () => {
const integration = new couchdb.integration({
url,
database: "db",
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test invalid database", async () => {
const integration = new couchdb.integration({
url,
database: "random_db",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
})
})
it("test invalid url", async () => {
const integration = new couchdb.integration({
url: "http://invalid:123",
database: "any",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error:
"request to http://invalid:123/any failed, reason: getaddrinfo ENOTFOUND invalid",
})
})
})
})

View file

@ -1,63 +0,0 @@
import { GenericContainer } from "testcontainers"
import { env } from "@budibase/backend-core"
import dynamodb from "../../../../packages/server/src/integrations/dynamodb"
import { generator } from "../../shared"
jest.unmock("aws-sdk")
describe("datasource validators", () => {
describe("dynamodb", () => {
let connectionSettings: {
user: string
password: string
url: string
}
beforeAll(async () => {
const user = "root"
const password = generator.hash()
const container = await new GenericContainer("amazon/dynamodb-local")
.withExposedPorts(8000)
.start()
connectionSettings = {
user,
password,
url: `http://${container.getContainerIpAddress()}:${container.getMappedPort(
8000
)}`,
}
env._set("AWS_ACCESS_KEY_ID", "mockedkey")
env._set("AWS_SECRET_ACCESS_KEY", "mockedsecret")
})
it("test valid connection string", async () => {
const integration = new dynamodb.integration({
endpoint: connectionSettings.url,
region: "",
accessKeyId: "",
secretAccessKey: "",
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test wrong endpoint", async () => {
const integration = new dynamodb.integration({
endpoint: "http://wrong.url:2880",
region: "",
accessKeyId: "",
secretAccessKey: "",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error:
"Inaccessible host: `wrong.url' at port `undefined'. This service may not be available in the `eu-west-1' region.",
})
})
})
})

View file

@ -1,34 +0,0 @@
import { ElasticsearchContainer } from "testcontainers"
import elastic from "../../../../packages/server/src/integrations/elasticsearch"
jest.unmock("@elastic/elasticsearch")
describe("datasource validators", () => {
describe("elastic search", () => {
let url: string
beforeAll(async () => {
const container = await new ElasticsearchContainer().start()
url = container.getHttpUrl()
})
it("test valid connection string", async () => {
const integration = new elastic.integration({
url,
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test wrong connection string", async () => {
const integration = new elastic.integration({
url: `http://localhost:5656`,
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "connect ECONNREFUSED 127.0.0.1:5656",
})
})
})
})

View file

@ -1,100 +0,0 @@
import { GenericContainer } from "testcontainers"
import mongo from "../../../../packages/server/src/integrations/mongodb"
import { generator } from "../../shared"
jest.unmock("mongodb")
describe("datasource validators", () => {
describe("mongo", () => {
let connectionSettings: {
user: string
password: string
host: string
port: number
}
function getConnectionString(
settings: Partial<typeof connectionSettings> = {}
) {
const { user, password, host, port } = {
...connectionSettings,
...settings,
}
return `mongodb://${user}:${password}@${host}:${port}`
}
beforeAll(async () => {
const user = generator.name()
const password = generator.hash()
const container = await new GenericContainer("mongo:7.0-jammy")
.withExposedPorts(27017)
.withEnv("MONGO_INITDB_ROOT_USERNAME", user)
.withEnv("MONGO_INITDB_ROOT_PASSWORD", password)
.start()
connectionSettings = {
user,
password,
host: container.getContainerIpAddress(),
port: container.getMappedPort(27017),
}
})
it("test valid connection string", async () => {
const integration = new mongo.integration({
connectionString: getConnectionString(),
db: "",
tlsCertificateFile: "",
tlsCertificateKeyFile: "",
tlsCAFile: "",
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test invalid password", async () => {
const integration = new mongo.integration({
connectionString: getConnectionString({ password: "wrong" }),
db: "",
tlsCertificateFile: "",
tlsCertificateKeyFile: "",
tlsCAFile: "",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "Authentication failed.",
})
})
it("test invalid username", async () => {
const integration = new mongo.integration({
connectionString: getConnectionString({ user: "wrong" }),
db: "",
tlsCertificateFile: "",
tlsCertificateKeyFile: "",
tlsCAFile: "",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "Authentication failed.",
})
})
it("test invalid connection", async () => {
const integration = new mongo.integration({
connectionString: getConnectionString({ host: "http://nothinghere" }),
db: "",
tlsCertificateFile: "",
tlsCertificateKeyFile: "",
tlsCAFile: "",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "getaddrinfo ENOTFOUND http",
})
})
})
})

View file

@ -1,65 +0,0 @@
import { GenericContainer, Wait } from "testcontainers"
import { Duration, TemporalUnit } from "node-duration"
import mssql from "../../../../packages/server/src/integrations/microsoftSqlServer"
jest.unmock("mssql")
describe("datasource validators", () => {
describe("mssql", () => {
let host: string, port: number
const password = "Str0Ng_p@ssW0rd!"
beforeAll(async () => {
const container = await new GenericContainer(
"mcr.microsoft.com/mssql/server:2022-latest"
)
.withExposedPorts(1433)
.withEnv("ACCEPT_EULA", "Y")
.withEnv("MSSQL_SA_PASSWORD", password)
.withEnv("MSSQL_PID", "Developer")
.withWaitStrategy(Wait.forHealthCheck())
.withHealthCheck({
test: `/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "${password}" -Q "SELECT 1" -b -o /dev/null`,
interval: new Duration(1000, TemporalUnit.MILLISECONDS),
timeout: new Duration(3, TemporalUnit.SECONDS),
retries: 20,
startPeriod: new Duration(100, TemporalUnit.MILLISECONDS),
})
.start()
host = container.getContainerIpAddress()
port = container.getMappedPort(1433)
})
it("test valid connection string", async () => {
const integration = new mssql.integration({
user: "sa",
password,
server: host,
port: port,
database: "master",
schema: "dbo",
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test invalid password", async () => {
const integration = new mssql.integration({
user: "sa",
password: "wrong_pwd",
server: host,
port: port,
database: "master",
schema: "dbo",
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "Login failed for user 'sa'.",
})
})
})
})

View file

@ -1,68 +0,0 @@
import { GenericContainer } from "testcontainers"
import mysql from "../../../../packages/server/src/integrations/mysql"
describe("datasource validators", () => {
describe("mysql", () => {
let host: string
let port: number
beforeAll(async () => {
const container = await new GenericContainer("mysql:8.3")
.withExposedPorts(3306)
.withEnv("MYSQL_ROOT_PASSWORD", "admin")
.withEnv("MYSQL_DATABASE", "db")
.withEnv("MYSQL_USER", "user")
.withEnv("MYSQL_PASSWORD", "password")
.start()
host = container.getContainerIpAddress()
port = container.getMappedPort(3306)
})
it("test valid connection string", async () => {
const integration = new mysql.integration({
host,
port,
user: "user",
database: "db",
password: "password",
rejectUnauthorized: true,
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test invalid database", async () => {
const integration = new mysql.integration({
host,
port,
user: "user",
database: "test",
password: "password",
rejectUnauthorized: true,
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: "Access denied for user 'user'@'%' to database 'test'",
})
})
it("test invalid password", async () => {
const integration = new mysql.integration({
host,
port,
user: "root",
database: "test",
password: "wrong",
rejectUnauthorized: true,
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error:
"Access denied for the specified user. User does not have the necessary privileges or the provided credentials are incorrect. Please verify the credentials, and ensure that the user has appropriate permissions.",
})
})
})
})

View file

@ -1,54 +0,0 @@
import { GenericContainer } from "testcontainers"
import postgres from "../../../../packages/server/src/integrations/postgres"
jest.unmock("pg")
describe("datasource validators", () => {
describe("postgres", () => {
let host: string
let port: number
beforeAll(async () => {
const container = await new GenericContainer("postgres:16.1-bullseye")
.withExposedPorts(5432)
.withEnv("POSTGRES_PASSWORD", "password")
.start()
host = container.getContainerIpAddress()
port = container.getMappedPort(5432)
})
it("test valid connection string", async () => {
const integration = new postgres.integration({
host,
port,
database: "postgres",
user: "postgres",
password: "password",
schema: "public",
ssl: false,
rejectUnauthorized: false,
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test invalid connection string", async () => {
const integration = new postgres.integration({
host,
port,
database: "postgres",
user: "wrong",
password: "password",
schema: "public",
ssl: false,
rejectUnauthorized: false,
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error: 'password authentication failed for user "wrong"',
})
})
})
})

View file

@ -1,72 +0,0 @@
import redis from "../../../../packages/server/src/integrations/redis"
import { GenericContainer } from "testcontainers"
import { generator } from "../../shared"
describe("datasource validators", () => {
describe("redis", () => {
describe("unsecured", () => {
let host: string
let port: number
beforeAll(async () => {
const container = await new GenericContainer("redis")
.withExposedPorts(6379)
.start()
host = container.getContainerIpAddress()
port = container.getMappedPort(6379)
})
it("test valid connection", async () => {
const integration = new redis.integration({
host,
port,
username: "",
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test invalid connection even with wrong user/password", async () => {
const integration = new redis.integration({
host,
port,
username: generator.name(),
password: generator.hash(),
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error:
"WRONGPASS invalid username-password pair or user is disabled.",
})
})
})
describe("secured", () => {
let host: string
let port: number
beforeAll(async () => {
const container = await new GenericContainer("redis")
.withExposedPorts(6379)
.withCmd(["redis-server", "--requirepass", "P@ssW0rd!"])
.start()
host = container.getContainerIpAddress()
port = container.getMappedPort(6379)
})
it("test valid connection", async () => {
const integration = new redis.integration({
host,
port,
username: "",
password: "P@ssW0rd!",
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
})
})
})

View file

@ -1,52 +0,0 @@
import s3 from "../../../../packages/server/src/integrations/s3"
import { GenericContainer } from "testcontainers"
jest.unmock("aws-sdk")
describe("datasource validators", () => {
describe("s3", () => {
let host: string
let port: number
beforeAll(async () => {
const container = await new GenericContainer("localstack/localstack")
.withExposedPorts(4566)
.withEnv("SERVICES", "s3")
.withEnv("DEFAULT_REGION", "eu-west-1")
.withEnv("AWS_ACCESS_KEY_ID", "testkey")
.withEnv("AWS_SECRET_ACCESS_KEY", "testsecret")
.start()
host = container.getContainerIpAddress()
port = container.getMappedPort(4566)
})
it("test valid connection", async () => {
const integration = new s3.integration({
region: "eu-west-1",
accessKeyId: "testkey",
secretAccessKey: "testsecret",
s3ForcePathStyle: false,
endpoint: `http://${host}:${port}`,
})
const result = await integration.testConnection()
expect(result).toEqual({ connected: true })
})
it("test wrong endpoint", async () => {
const integration = new s3.integration({
region: "eu-west-2",
accessKeyId: "testkey",
secretAccessKey: "testsecret",
s3ForcePathStyle: false,
endpoint: `http://wrong:123`,
})
const result = await integration.testConnection()
expect(result).toEqual({
connected: false,
error:
"Inaccessible host: `wrong' at port `undefined'. This service may not be available in the `eu-west-2' region.",
})
})
})
})

View file

@ -1,54 +0,0 @@
import AppAPI from "./apis/AppAPI"
import AuthAPI from "./apis/AuthAPI"
import EnvironmentAPI from "./apis/EnvironmentAPI"
import RoleAPI from "./apis/RoleAPI"
import RowAPI from "./apis/RowAPI"
import ScreenAPI from "./apis/ScreenAPI"
import SelfAPI from "./apis/SelfAPI"
import TableAPI from "./apis/TableAPI"
import UserAPI from "./apis/UserAPI"
import DatasourcesAPI from "./apis/DatasourcesAPI"
import IntegrationsAPI from "./apis/IntegrationsAPI"
import QueriesAPI from "./apis/QueriesAPI"
import PermissionsAPI from "./apis/PermissionsAPI"
import LicenseAPI from "./apis/LicenseAPI"
import BudibaseInternalAPIClient from "./BudibaseInternalAPIClient"
import { State } from "../../types"
export default class BudibaseInternalAPI {
client: BudibaseInternalAPIClient
apps: AppAPI
auth: AuthAPI
environment: EnvironmentAPI
roles: RoleAPI
rows: RowAPI
screens: ScreenAPI
self: SelfAPI
tables: TableAPI
users: UserAPI
datasources: DatasourcesAPI
integrations: IntegrationsAPI
queries: QueriesAPI
permissions: PermissionsAPI
license: LicenseAPI
constructor(state: State) {
this.client = new BudibaseInternalAPIClient(state)
this.apps = new AppAPI(this.client)
this.auth = new AuthAPI(this.client, state)
this.environment = new EnvironmentAPI(this.client)
this.roles = new RoleAPI(this.client)
this.rows = new RowAPI(this.client)
this.screens = new ScreenAPI(this.client)
this.self = new SelfAPI(this.client)
this.tables = new TableAPI(this.client)
this.users = new UserAPI(this.client)
this.datasources = new DatasourcesAPI(this.client)
this.integrations = new IntegrationsAPI(this.client)
this.queries = new QueriesAPI(this.client)
this.permissions = new PermissionsAPI(this.client)
this.license = new LicenseAPI(this.client)
}
}

View file

@ -1,80 +0,0 @@
import env from "../../environment"
import fetch, { HeadersInit } from "node-fetch"
import { State } from "../../types"
type APIMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
interface ApiOptions {
method?: APIMethod
body?: object
headers?: HeadersInit | undefined
}
class BudibaseInternalAPIClient {
host: string
state: State
constructor(state: State) {
if (!env.BUDIBASE_URL) {
throw new Error("Must set BUDIBASE_URL env var")
}
this.host = `${env.BUDIBASE_URL}/api`
this.state = state
}
apiCall =
(method: APIMethod) =>
async (url = "", options: ApiOptions = {}) => {
const requestOptions = {
method,
body: JSON.stringify(options.body),
headers: {
"x-budibase-app-id": this.state.appId,
"Content-Type": "application/json",
Accept: "application/json",
cookie: this.state.cookie,
redirect: "follow",
follow: 20,
...options.headers,
},
credentials: "include",
}
// prettier-ignore
// @ts-ignore
const response = await fetch(`${this.host}${url}`, requestOptions)
let body: any
const contentType = response.headers.get("content-type")
if (contentType && contentType.includes("application/json")) {
body = await response.json()
} else {
body = await response.text()
}
const data = {
request: requestOptions.body,
response: body,
}
const message = `${method} ${url} - ${response.status}`
const isDebug = process.env.LOG_LEVEL === "debug"
if (response.status > 499) {
console.error(message, data)
} else if (response.status >= 400) {
console.warn(message, data)
} else if (isDebug) {
console.debug(message, data)
}
return [response, body]
}
post = this.apiCall("POST")
get = this.apiCall("GET")
patch = this.apiCall("PATCH")
del = this.apiCall("DELETE")
put = this.apiCall("PUT")
}
export default BudibaseInternalAPIClient

View file

@ -1,152 +0,0 @@
import { App, CreateAppRequest } from "@budibase/types"
import { Response } from "node-fetch"
import {
RouteConfig,
AppPackageResponse,
DeployConfig,
MessageResponse,
} from "../../../types"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import BaseAPI from "./BaseAPI"
interface RenameAppBody {
name: string
}
export default class AppAPI extends BaseAPI {
constructor(client: BudibaseInternalAPIClient) {
super(client)
}
// TODO Fix the fetch apps to receive an optional number of apps and compare if the received app is more or less.
// each possible scenario should have its own method.
async fetchEmptyAppList(): Promise<[Response, App[]]> {
const [response, json] = await this.get(`/applications?status=all`)
expect(json.length).toBeGreaterThanOrEqual(0)
return [response, json]
}
async fetchAllApplications(): Promise<[Response, App[]]> {
const [response, json] = await this.get(`/applications?status=all`)
expect(json.length).toBeGreaterThanOrEqual(1)
return [response, json]
}
async canRender(): Promise<[Response, boolean]> {
const [response, json] = await this.get("/routing/client")
const publishedAppRenders = Object.keys(json.routes).length > 0
expect(publishedAppRenders).toBe(true)
return [response, publishedAppRenders]
}
async getAppPackage(appId: string): Promise<[Response, AppPackageResponse]> {
const [response, json] = await this.get(`/applications/${appId}/appPackage`)
expect(json.application.appId).toEqual(appId)
return [response, json]
}
async publish(appId: string | undefined): Promise<[Response, DeployConfig]> {
const [response, json] = await this.post(`/applications/${appId}/publish`)
return [response, json]
}
async create(body: CreateAppRequest): Promise<App> {
const [response, json] = await this.post(`/applications`, body)
expect(json._id).toBeDefined()
return json
}
async read(id: string): Promise<[Response, App]> {
const [response, json] = await this.get(`/applications/${id}`)
return [response, json.data]
}
async sync(appId: string): Promise<[Response, MessageResponse]> {
const [response, json] = await this.post(`/applications/${appId}/sync`)
return [response, json]
}
// TODO
async updateClient(appId: string, body: any): Promise<[Response, App]> {
const [response, json] = await this.put(
`/applications/${appId}/client/update`,
{ body }
)
return [response, json]
}
async revertPublished(appId: string): Promise<[Response, MessageResponse]> {
const [response, json] = await this.post(`/dev/${appId}/revert`)
expect(json).toEqual({
message: "Reverted changes successfully.",
})
return [response, json]
}
async revertUnpublished(appId: string): Promise<[Response, MessageResponse]> {
const [response, json] = await this.post(
`/dev/${appId}/revert`,
undefined,
400
)
expect(json).toEqual({
message: "App has not yet been deployed",
status: 400,
})
return [response, json]
}
async delete(appId: string): Promise<Response> {
const [response, _] = await this.del(`/applications/${appId}`)
return response
}
async rename(
appId: string,
oldName: string,
body: RenameAppBody
): Promise<[Response, App]> {
const [response, json] = await this.put(`/applications/${appId}`, body)
expect(json.name).not.toEqual(oldName)
return [response, json]
}
async getRoutes(screenExists?: boolean): Promise<[Response, RouteConfig]> {
const [response, json] = await this.get(`/routing`)
if (screenExists) {
expect(json.routes["/test"]).toBeTruthy()
} else {
expect(json.routes["/test"]).toBeUndefined()
}
return [response, json]
}
async unpublish(appId: string): Promise<[Response]> {
const [response, json] = await this.post(
`/applications/${appId}/unpublish`,
undefined,
204
)
return [response]
}
async unlock(appId: string): Promise<[Response, MessageResponse]> {
const [response, json] = await this.del(`/dev/${appId}/lock`)
expect(json.message).toEqual("Lock released successfully.")
return [response, json]
}
async updateIcon(appId: string): Promise<[Response, App]> {
const body = {
icon: {
name: "ConversionFunnel",
color: "var(--spectrum-global-color-red-400)",
},
}
const [response, json] = await this.put(`/applications/${appId}`, body)
expect(json.icon.name).toEqual(body.icon.name)
expect(json.icon.color).toEqual(body.icon.color)
return [response, json]
}
}

View file

@ -1,39 +0,0 @@
import { Response } from "node-fetch"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import { APIRequestOpts, State } from "../../../types"
export default class AuthAPI {
state: State
client: BudibaseInternalAPIClient
constructor(client: BudibaseInternalAPIClient, state: State) {
this.client = client
this.state = state
}
async login(
tenantId: string,
email: String,
password: String,
opts: APIRequestOpts = { doExpect: true }
): Promise<[Response, string]> {
const [response, json] = await this.client.post(
`/global/auth/${tenantId}/login`,
{
body: {
username: email,
password: password,
},
}
)
if (opts.doExpect) {
expect(response).toHaveStatusCode(200)
}
const cookie = response.headers.get("set-cookie")
return [response, cookie!]
}
async logout(): Promise<any> {
return this.client.post(`/global/auth/logout`)
}
}

View file

@ -1,56 +0,0 @@
import { Response } from "node-fetch"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
export default class BaseAPI {
client: BudibaseInternalAPIClient
constructor(client: BudibaseInternalAPIClient) {
this.client = client
}
async get(url: string, status?: number): Promise<[Response, any]> {
const [response, json] = await this.client.get(url)
expect(response).toHaveStatusCode(status ? status : 200)
return [response, json]
}
async post(
url: string,
body?: any,
statusCode?: number
): Promise<[Response, any]> {
const [response, json] = await this.client.post(url, { body })
expect(response).toHaveStatusCode(statusCode ? statusCode : 200)
return [response, json]
}
async put(
url: string,
body?: any,
statusCode?: number
): Promise<[Response, any]> {
const [response, json] = await this.client.put(url, { body })
expect(response).toHaveStatusCode(statusCode ? statusCode : 200)
return [response, json]
}
async patch(
url: string,
body?: any,
statusCode?: number
): Promise<[Response, any]> {
const [response, json] = await this.client.patch(url, { body })
expect(response).toHaveStatusCode(statusCode ? statusCode : 200)
return [response, json]
}
async del(
url: string,
statusCode?: number,
body?: any
): Promise<[Response, any]> {
const [response, json] = await this.client.del(url, { body })
expect(response).toHaveStatusCode(statusCode ? statusCode : 200)
return [response, json]
}
}

View file

@ -1,62 +0,0 @@
import { Response } from "node-fetch"
import {
Datasource,
CreateDatasourceResponse,
UpdateDatasourceResponse,
} from "@budibase/types"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import BaseAPI from "./BaseAPI"
import { DatasourceRequest } from "../../../types"
export default class DatasourcesAPI extends BaseAPI {
constructor(client: BudibaseInternalAPIClient) {
super(client)
}
async getIntegrations(): Promise<[Response, any]> {
const [response, json] = await this.get(`/integrations`)
const integrationsCount = Object.keys(json).length
expect(integrationsCount).toBe(16)
return [response, json]
}
async getAll(): Promise<[Response, Datasource[]]> {
const [response, json] = await this.get(`/datasources`)
expect(json.length).toBeGreaterThan(0)
return [response, json]
}
async getTable(dataSourceId: string): Promise<[Response, Datasource]> {
const [response, json] = await this.get(`/datasources/${dataSourceId}`)
expect(json._id).toEqual(dataSourceId)
return [response, json]
}
async add(
body: DatasourceRequest
): Promise<[Response, CreateDatasourceResponse]> {
const [response, json] = await this.post(`/datasources`, body)
expect(json.datasource._id).toBeDefined()
expect(json.datasource._rev).toBeDefined()
return [response, json]
}
async update(
body: Datasource
): Promise<[Response, UpdateDatasourceResponse]> {
const [response, json] = await this.put(`/datasources/${body._id}`, body)
expect(json.datasource._id).toBeDefined()
expect(json.datasource._rev).toBeDefined()
return [response, json]
}
async delete(dataSourceId: string, revId: string): Promise<Response> {
const [response, json] = await this.del(
`/datasources/${dataSourceId}/${revId}`
)
return response
}
}

View file

@ -1,21 +0,0 @@
import { GetEnvironmentResponse } from "@budibase/types"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import { APIRequestOpts } from "../../../types"
export default class EnvironmentAPI {
client: BudibaseInternalAPIClient
constructor(client: BudibaseInternalAPIClient) {
this.client = client
}
async getEnvironment(
opts: APIRequestOpts = { doExpect: true }
): Promise<GetEnvironmentResponse> {
const [response, json] = await this.client.get(`/system/environment`)
if (opts.doExpect) {
expect(response.status).toBe(200)
}
return json
}
}

View file

@ -1,16 +0,0 @@
import { Response } from "node-fetch"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import BaseAPI from "./BaseAPI"
export default class IntegrationsAPI extends BaseAPI {
constructor(client: BudibaseInternalAPIClient) {
super(client)
}
async getAll(): Promise<[Response, any]> {
const [response, json] = await this.get(`/integrations`)
const integrationsCount = Object.keys(json).length
expect(integrationsCount).toBeGreaterThan(0)
return [response, json]
}
}

View file

@ -1,63 +0,0 @@
import { Response } from "node-fetch"
import {
ActivateLicenseKeyRequest,
ActivateOfflineLicenseTokenRequest,
GetLicenseKeyResponse,
GetOfflineIdentifierResponse,
GetOfflineLicenseTokenResponse,
} from "@budibase/types"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import BaseAPI from "./BaseAPI"
import { APIRequestOpts } from "../../../types"
export default class LicenseAPI extends BaseAPI {
constructor(client: BudibaseInternalAPIClient) {
super(client)
}
async getOfflineLicenseToken(
opts: { status?: number } = {}
): Promise<[Response, GetOfflineLicenseTokenResponse]> {
const [response, body] = await this.get(
`/global/license/offline`,
opts.status
)
return [response, body]
}
async deleteOfflineLicenseToken(): Promise<[Response]> {
const [response] = await this.del(`/global/license/offline`, 204)
return [response]
}
async activateOfflineLicenseToken(
body: ActivateOfflineLicenseTokenRequest
): Promise<[Response]> {
const [response] = await this.post(`/global/license/offline`, body)
return [response]
}
async getOfflineIdentifier(): Promise<
[Response, GetOfflineIdentifierResponse]
> {
const [response, body] = await this.get(
`/global/license/offline/identifier`
)
return [response, body]
}
async getLicenseKey(
opts: { status?: number } = {}
): Promise<[Response, GetLicenseKeyResponse]> {
const [response, body] = await this.get(`/global/license/key`, opts.status)
return [response, body]
}
async activateLicenseKey(
body: ActivateLicenseKeyRequest
): Promise<[Response]> {
const [response] = await this.post(`/global/license/key`, body)
return [response]
}
async deleteLicenseKey(): Promise<[Response]> {
const [response] = await this.del(`/global/license/key`, 204)
return [response]
}
}

View file

@ -1,14 +0,0 @@
import { Response } from "node-fetch"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import BaseAPI from "./BaseAPI"
export default class PermissionsAPI extends BaseAPI {
constructor(client: BudibaseInternalAPIClient) {
super(client)
}
async getAll(id: string): Promise<[Response, any]> {
const [response, json] = await this.get(`/permissions/${id}`)
return [response, json]
}
}

View file

@ -1,25 +0,0 @@
import { Response } from "node-fetch"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import { PreviewQueryRequest, Query } from "@budibase/types"
import BaseAPI from "./BaseAPI"
export default class QueriesAPI extends BaseAPI {
constructor(client: BudibaseInternalAPIClient) {
super(client)
}
async preview(body: PreviewQueryRequest): Promise<[Response, any]> {
const [response, json] = await this.post(`/queries/preview`, body)
return [response, json]
}
async save(body: Query): Promise<[Response, any]> {
const [response, json] = await this.post(`/queries`, body)
return [response, json]
}
async getQuery(queryId: string): Promise<[Response, any]> {
const [response, json] = await this.get(`/queries/${queryId}`)
return [response, json]
}
}

View file

@ -1,20 +0,0 @@
import { Response } from "node-fetch"
import { Role, UserRoles } from "@budibase/types"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import BaseAPI from "./BaseAPI"
export default class RoleAPI extends BaseAPI {
constructor(client: BudibaseInternalAPIClient) {
super(client)
}
async getRoles(): Promise<[Response, Role[]]> {
const [response, json] = await this.get(`/roles`)
return [response, json]
}
async createRole(body: Partial<UserRoles>): Promise<[Response, UserRoles]> {
const [response, json] = await this.post(`/roles`, body)
return [response, json]
}
}

View file

@ -1,57 +0,0 @@
import { Response } from "node-fetch"
import { Row } from "@budibase/types"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import BaseAPI from "./BaseAPI"
export default class RowAPI extends BaseAPI {
rowAdded: boolean
constructor(client: BudibaseInternalAPIClient) {
super(client)
this.rowAdded = false
}
async getAll(tableId: string): Promise<[Response, Row[]]> {
const [response, json] = await this.get(`/${tableId}/rows`)
if (this.rowAdded) {
expect(json.length).toBeGreaterThanOrEqual(1)
}
return [response, json]
}
async add(tableId: string, body: Row): Promise<[Response, Row]> {
const [response, json] = await this.post(`/${tableId}/rows`, body)
expect(json._id).toBeDefined()
expect(json._rev).toBeDefined()
expect(json.tableId).toEqual(tableId)
this.rowAdded = true
return [response, json]
}
async delete(tableId: string, body: Row): Promise<[Response, Row[]]> {
const [response, json] = await this.del(
`/${tableId}/rows/`,
undefined,
body
)
return [response, json]
}
async searchNoPagination(
tableId: string,
body: string
): Promise<[Response, Row[]]> {
const [response, json] = await this.post(`/${tableId}/search`, body)
expect(json.hasNextPage).toEqual(false)
return [response, json.rows]
}
async searchWithPagination(
tableId: string,
body: string
): Promise<[Response, Row[]]> {
const [response, json] = await this.post(`/${tableId}/search`, body)
expect(json.hasNextPage).toEqual(true)
expect(json.rows.length).toEqual(10)
return [response, json.rows]
}
}

View file

@ -1,23 +0,0 @@
import { Response } from "node-fetch"
import { Screen } from "@budibase/types"
import { ScreenRequest } from "../../../types/screens"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import BaseAPI from "./BaseAPI"
export default class ScreenAPI extends BaseAPI {
constructor(client: BudibaseInternalAPIClient) {
super(client)
}
async create(body: ScreenRequest): Promise<[Response, Screen]> {
const [response, json] = await this.post(`/screens`, body)
expect(json._id).toBeDefined()
expect(json.routing.roleId).toBe(body.routing.roleId)
return [response, json]
}
async delete(screenId: string, rev: string): Promise<[Response, Screen]> {
const [response, json] = await this.del(`/screens/${screenId}/${rev}`)
return [response, json]
}
}

View file

@ -1,29 +0,0 @@
import { Response } from "node-fetch"
import { User } from "@budibase/types"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import { ApiKeyResponse } from "../../../types"
import BaseAPI from "./BaseAPI"
export default class SelfAPI extends BaseAPI {
constructor(client: BudibaseInternalAPIClient) {
super(client)
}
async getSelf(): Promise<[Response, Partial<User>]> {
const [response, json] = await this.get(`/global/self`)
return [response, json]
}
async changeSelfPassword(body: Partial<User>): Promise<[Response, User]> {
const [response, json] = await this.post(`/global/self`, body)
expect(json._id).toEqual(body._id)
expect(json._rev).not.toEqual(body._rev)
return [response, json]
}
async getApiKey(): Promise<ApiKeyResponse> {
const [response, json] = await this.get(`/global/self/api_key`)
expect(json).toHaveProperty("apiKey")
return json
}
}

View file

@ -1,48 +0,0 @@
import { Response } from "node-fetch"
import { Table } from "@budibase/types"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import { MessageResponse } from "../../../types"
import BaseAPI from "./BaseAPI"
export default class TableAPI extends BaseAPI {
constructor(client: BudibaseInternalAPIClient) {
super(client)
}
async getAll(expectedNumber: Number): Promise<[Response, Table[]]> {
const [response, json] = await this.get(`/tables`)
expect(json.length).toBe(expectedNumber)
return [response, json]
}
async getTableById(id: string): Promise<[Response, Table]> {
const [response, json] = await this.get(`/tables/${id}`)
expect(json._id).toEqual(id)
return [response, json]
}
async save(body: any, columnAdded?: boolean): Promise<[Response, Table]> {
const [response, json] = await this.post(`/tables`, body)
expect(json._id).toBeDefined()
expect(json._rev).toBeDefined()
if (columnAdded) {
expect(json.schema.TestColumn).toBeDefined()
}
return [response, json]
}
async forbiddenSave(body: any): Promise<[Response, Table]> {
const [response, json] = await this.post(`/tables`, body, 403)
return [response, json]
}
async delete(
id: string,
revId: string
): Promise<[Response, MessageResponse]> {
const [response, json] = await this.del(`/tables/${id}/${revId}`)
expect(json.message).toEqual(`Table ${id} deleted.`)
return [response, json]
}
}

View file

@ -1,107 +0,0 @@
import { Response } from "node-fetch"
import { Role, User, UserDeletedEvent, UserRoles } from "@budibase/types"
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
import { MessageResponse } from "../../../types"
import BaseAPI from "./BaseAPI"
export default class UserAPI extends BaseAPI {
constructor(client: BudibaseInternalAPIClient) {
super(client)
}
async search(): Promise<[Response, Partial<User>[]]> {
const [response, json] = await this.post(`/global/users/search`, {})
expect(json.data.length).toBeGreaterThan(0)
return [response, json]
}
async getSelf(): Promise<[Response, Partial<User>]> {
const [response, json] = await this.get(`/global/self`)
return [response, json]
}
async getAll(): Promise<[Response, Partial<User>[]]> {
const [response, json] = await this.get(`/global/users`)
expect(json.length).toBeGreaterThan(0)
return [response, json]
}
// This endpoint is used for one or more users when we want add users with passwords set.
async addMultiple(userList: Partial<User>[]): Promise<[Response, any]> {
const body = {
create: {
users: userList,
groups: [],
},
}
const [response, json] = await this.post(`/global/users/bulk`, body)
expect(json.created.unsuccessful.length).toEqual(0)
expect(json.created.successful.length).toEqual(body.create.users.length)
return [response, json]
}
async deleteMultiple(userId: string[]): Promise<[Response, MessageResponse]> {
const body = {
delete: {
userIds: [userId],
},
}
const [response, json] = await this.post(`/global/users/bulk`, body)
expect(json.deleted.successful.length).toEqual(1)
expect(json.deleted.unsuccessful.length).toEqual(0)
expect(json.deleted.successful[0].userId).toEqual(userId)
return [response, json]
}
async delete(userId: string): Promise<[Response, UserDeletedEvent]> {
const [response, json] = await this.del(`/global/users/${userId}`)
expect(json.message).toEqual(`User ${userId} deleted.`)
return [response, json]
}
async invite(body: any): Promise<[Response, MessageResponse]> {
const [response, json] = await this.post(`/global/users/multi/invite`, body)
expect(json.unsuccessful.length).toEqual(0)
expect(json.successful.length).toEqual(body.length)
return [response, json]
}
async getRoles(): Promise<[Response, Role[]]> {
const [response, json] = await this.get(`/roles`)
return [response, json]
}
async updateInfo(body: any): Promise<[Response, User]> {
const [response, json] = await this.post(`/global/users/`, body)
expect(json._id).toEqual(body._id)
expect(json._rev).not.toEqual(body._rev)
return [response, json]
}
async forcePasswordReset(body: any): Promise<[Response, User]> {
const [response, json] = await this.post(`/global/users/`, body)
expect(json._id).toEqual(body._id)
expect(json._rev).not.toEqual(body._rev)
return [response, json]
}
async getInfo(userId: string): Promise<[Response, User]> {
const [response, json] = await this.get(`/global/users/${userId}`)
return [response, json]
}
async changeSelfPassword(body: Partial<User>): Promise<[Response, User]> {
const [response, json] = await this.post(`/global/self`, body)
expect(json._id).toEqual(body._id)
expect(json._rev).not.toEqual(body._rev)
return [response, json]
}
async createRole(body: Partial<UserRoles>): Promise<[Response, UserRoles]> {
const [response, json] = await this.post(`/roles`, body)
return [response, json]
}
}

View file

@ -1 +0,0 @@
export { default as BudibaseInternalAPI } from "./BudibaseInternalAPI"

View file

@ -1,25 +0,0 @@
import { BudibaseInternalAPI } from "../api"
import { BudibaseTestConfiguration } from "../../shared"
export default class TestConfiguration<T> extends BudibaseTestConfiguration {
// apis
api: BudibaseInternalAPI
// context
context: T
constructor() {
super()
// for brevity
this.api = this.internalApi
this.context = <T>{}
}
async beforeAll() {
await super.beforeAll()
}
async afterAll() {
await super.afterAll()
}
}

View file

@ -1 +0,0 @@
export { default as TestConfiguration } from "./TestConfiguration"

View file

@ -1,20 +0,0 @@
import { generator } from "../../shared"
import { Hosting, CreateAccountRequest } from "@budibase/types"
export const generateAccount = (): CreateAccountRequest => {
const uuid = generator.guid()
const email = `qa+${uuid}@budibase.com`
const tenant = `tenant${uuid.replace(/-/g, "")}`
return {
email,
hosting: Hosting.CLOUD,
name: email,
password: uuid,
profession: "software_engineer",
size: "10+",
tenantId: tenant,
tenantName: tenant,
}
}

View file

@ -1,27 +0,0 @@
import { generator } from "../../shared"
import { CreateAppRequest } from "@budibase/types"
function uniqueWord() {
return generator.word() + generator.hash()
}
export const generateApp = (
overrides: Partial<CreateAppRequest> = {}
): CreateAppRequest => ({
name: uniqueWord(),
url: `/${uniqueWord()}`,
...overrides,
})
// Applications type doesn't work here, save to add useTemplate parameter?
export const appFromTemplate = (): CreateAppRequest => {
return {
name: uniqueWord(),
url: `/${uniqueWord()}`,
// @ts-ignore
useTemplate: "true",
templateName: "Near Miss Register",
templateKey: "app/near-miss-register",
templateFile: undefined,
}
}

View file

@ -1,122 +0,0 @@
import { Datasource } from "@budibase/types"
import { DatasourceRequest } from "../../types"
import { generator } from "../../shared"
// Add information about the data source to the fixtures file from 1password
export const mongoDB = (): DatasourceRequest => {
return {
datasource: {
name: "MongoDB",
source: "MONGODB",
type: "datasource",
config: {
connectionString: process.env.MONGODB_CONNECTION_STRING,
db: process.env.MONGODB_DB,
},
},
fetchSchema: false,
}
}
export const postgresSQL = (): DatasourceRequest => {
return {
datasource: {
name: "PostgresSQL",
plus: true,
source: "POSTGRES",
type: "datasource",
config: {
database: process.env.POSTGRES_DB,
host: process.env.POSTGRES_HOST,
password: process.env.POSTGRES_PASSWORD,
port: process.env.POSTGRES_PORT,
schema: "public",
user: process.env.POSTGRES_USER,
},
},
fetchSchema: true,
}
}
export const mariaDB = (): DatasourceRequest => {
return {
datasource: {
name: "MariaDB",
plus: true,
source: "MYSQL",
type: "datasource",
config: {
database: process.env.MARIADB_DB,
host: process.env.MARIADB_HOST,
password: process.env.MARIADB_PASSWORD,
port: process.env.MARIADB_PORT,
schema: "public",
user: process.env.MARIADB_USER,
},
},
fetchSchema: true,
}
}
export const restAPI = (): DatasourceRequest => {
return {
datasource: {
name: "RestAPI",
source: "REST",
type: "datasource",
config: {
defaultHeaders: {},
rejectUnauthorized: true,
url: process.env.REST_API_BASE_URL,
},
},
fetchSchema: false,
}
}
export const generateRelationshipForMySQL = (
updatedDataSourceJson: any
): Datasource => {
const entities = updatedDataSourceJson!.datasource!.entities!
const datasourceId = updatedDataSourceJson!.datasource!._id!
const relationShipBody = {
...updatedDataSourceJson.datasource,
entities: {
...updatedDataSourceJson.datasource.entities,
employees: {
...entities.employees,
schema: {
...entities.employees.schema,
salaries: {
tableId: `${datasourceId}__salaries`,
name: "salaries",
relationshipType: "many-to-one",
fieldName: "salary",
type: "link",
main: true,
_id: generator.string(),
foreignKey: "emp_no",
},
},
},
titles: {
...entities.titles,
schema: {
...entities.titles.schema,
employees: {
tableId: `${datasourceId}__employees`,
name: "employees",
relationshipType: "one-to-many",
fieldName: "emp_no",
type: "link",
main: true,
_id: generator.string(),
foreignKey: "emp_no",
},
},
},
},
}
return relationShipBody
}

View file

@ -1,8 +0,0 @@
export * as accounts from "./accounts"
export * as apps from "./applications"
export * as rows from "./rows"
export * as screens from "./screens"
export * as tables from "./tables"
export * as users from "./users"
export * as datasources from "./datasources"
export * as queries from "./queries"

View file

@ -1,123 +0,0 @@
import { PreviewQueryRequest } from "@budibase/types"
const query = (datasourceId: string, fields: any): any => {
return {
datasourceId: datasourceId,
fields: fields,
name: "Query 1",
parameters: {},
queryVerb: "read",
schema: {},
transformer: "return data",
}
}
export const mariaDB = (datasourceId: string): PreviewQueryRequest => {
const fields = {
sql: "SELECT * FROM employees LIMIT 10;",
}
return query(datasourceId, fields)
}
export const mongoDB = (datasourceId: string): PreviewQueryRequest => {
const fields = {
extra: {
collection: "movies",
actionType: "find",
},
json: "",
}
return query(datasourceId, fields)
}
export const postgres = (datasourceId: string): PreviewQueryRequest => {
const fields = {
sql: "SELECT * FROM customers;",
}
return query(datasourceId, fields)
}
export const expectedSchemaFields = {
mariaDB: {
birth_date: "string",
emp_no: "number",
first_name: "string",
gender: "string",
hire_date: "string",
last_name: "string",
},
mongoDB: {
directors: "array",
genres: "array",
image: "string",
plot: "string",
rank: "number",
rating: "number",
release_date: "string",
running_time_secs: "number",
title: "string",
year: "number",
_id: "string",
},
postgres: {
address: "string",
city: "string",
company_name: "string",
contact_name: "string",
contact_title: "string",
country: "string",
customer_id: "string",
fax: "string",
phone: "string",
postal_code: "string",
region: "string",
},
restAPI: {
abilities: "array",
base_experience: "number",
forms: "array",
game_indices: "array",
height: "number",
held_items: "array",
id: "number",
is_default: "string",
location_area_encounters: "string",
moves: "array",
name: "string",
order: "number",
past_types: "array",
species: "json",
sprites: "json",
stats: "array",
types: "array",
weight: "number",
},
}
const request = (datasourceId: string, fields: any, flags: any): any => {
return {
datasourceId: datasourceId,
fields: fields,
flags: flags,
name: "Query 1",
parameters: {},
queryVerb: "read",
schema: {},
transformer: "return data",
}
}
export const restAPI = (datasourceId: string): PreviewQueryRequest => {
const fields = {
authConfigId: null,
bodyType: "none",
disabledHeaders: {},
headers: {},
pagination: {},
path: `${process.env.REST_API_BASE_URL}/pokemon/ditto`,
queryString: "",
}
const flags = {
urlName: true,
}
return request(datasourceId, fields, flags)
}

View file

@ -1,32 +0,0 @@
import { Row } from "@budibase/types"
export const generateNewRowForTable = (tableId: string): Row => {
return {
TestColumn: "TestRow",
tableId: tableId,
}
}
export const searchBody = (primaryDisplay: string): any => {
return {
bookmark: null,
limit: 10,
paginate: true,
query: {
contains: {},
containsAny: {},
empty: {},
equal: {},
fuzzy: {},
notContains: {},
notEmpty: {},
notEqual: {},
oneOf: {},
range: {},
string: {},
},
sort: primaryDisplay,
sortOrder: "ascending",
sortType: "string",
}
}

View file

@ -1,33 +0,0 @@
import { generator } from "../../shared"
import { ScreenRequest } from "../../types"
const randomId = generator.guid()
export const generateScreen = (roleId: string): ScreenRequest => ({
showNavigation: true,
width: "Large",
name: randomId,
template: "createFromScratch",
props: {
_id: randomId,
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_children: [],
_instanceName: "New Screen",
direction: "column",
hAlign: "stretch",
vAlign: "top",
size: "grow",
gap: "M",
},
routing: {
route: "/test",
roleId: roleId,
homeScreen: false,
},
})

View file

@ -1,30 +0,0 @@
import { Table } from "@budibase/types"
export const generateTable = (): Table => {
return {
name: "Test Table",
schema: {},
sourceId: "bb_internal",
type: "internal",
}
}
export const generateNewColumnForTable = (tableData: any): Table => {
const newColumn = tableData
newColumn.schema = {
TestColumn: {
type: "string",
name: "TestColumn",
constraints: {
presence: { allowEmpty: false },
length: { maximum: null },
type: "string",
},
},
}
newColumn.indexes = {
0: "TestColumn",
}
newColumn.updatedAt = new Date().toISOString()
return newColumn
}

View file

@ -1,82 +0,0 @@
import { generator } from "../../shared"
import { User } from "@budibase/types"
const generateDeveloper = (): Partial<User> => {
const randomId = generator.guid()
return {
email: `${randomId}@budibase.com`,
password: randomId,
roles: {},
forceResetPassword: true,
builder: {
global: true,
},
}
}
const generateAdmin = (): Partial<User> => {
const randomId = generator.guid()
return {
email: `${randomId}@budibase.com`,
password: randomId,
roles: {},
forceResetPassword: true,
admin: {
global: true,
},
builder: {
global: true,
},
}
}
const generateAppUser = (): Partial<User> => {
const randomId = generator.guid()
return {
email: `${randomId}@budibase.com`,
password: randomId,
roles: {},
forceResetPassword: true,
admin: {
global: false,
},
builder: {
global: false,
},
}
}
export const generateInviteUser = (): Object[] => {
const randomId = generator.guid()
return [
{
email: `${randomId}@budibase.com`,
userInfo: {
userGroups: [],
},
},
]
}
export const generateUser = (
amount: number = 1,
role?: string
): Partial<User>[] => {
const userList: Partial<User>[] = []
for (let i = 0; i < amount; i++) {
switch (role) {
case "admin":
userList.push(generateAdmin())
break
case "developer":
userList.push(generateDeveloper())
break
case "appUser":
userList.push(generateAppUser())
break
default:
userList.push(generateAppUser())
break
}
}
return userList
}

View file

@ -1 +0,0 @@
export * from "./api"

View file

@ -1,106 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
import { Query } from "@budibase/types"
describe("Internal API - Data Sources: MariaDB", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Create an app with a data source - MariaDB", async () => {
// Create app
await config.createApp()
// Get all integrations
await config.api.integrations.getAll()
// Add data source
const [dataSourceResponse, dataSourceJson] =
await config.api.datasources.add(fixtures.datasources.mariaDB())
// Update data source
const newDataSourceInfo = {
...dataSourceJson.datasource,
name: "MariaDB2",
}
const [updatedDataSourceResponse, updatedDataSourceJson] =
await config.api.datasources.update(newDataSourceInfo)
// Query data source
const [queryResponse, queryJson] = await config.api.queries.preview(
fixtures.queries.mariaDB(updatedDataSourceJson.datasource._id!)
)
expect(queryJson.rows.length).toEqual(10)
expect(queryJson.schemaFields).toEqual(
fixtures.queries.expectedSchemaFields.mariaDB
)
// Save query
const datasourcetoSave: Query = {
...fixtures.queries.mariaDB(updatedDataSourceJson.datasource._id!),
parameters: [],
}
const [saveQueryResponse, saveQueryJson] = await config.api.queries.save(
datasourcetoSave
)
// Get Query
const [getQueryResponse, getQueryJson] = await config.api.queries.getQuery(
<string>saveQueryJson._id
)
// Get Query permissions
const [getQueryPermissionsResponse, getQueryPermissionsJson] =
await config.api.permissions.getAll(saveQueryJson._id!)
// Delete data source
const deleteResponse = await config.api.datasources.delete(
updatedDataSourceJson.datasource._id!,
updatedDataSourceJson.datasource._rev!
)
})
it("Create a relationship", async () => {
// Create app
await config.createApp()
// Get all integrations
await config.api.integrations.getAll()
// Add data source
const [dataSourceResponse, dataSourceJson] =
await config.api.datasources.add(fixtures.datasources.mariaDB())
// Update data source
const newDataSourceInfo = {
...dataSourceJson.datasource,
name: "MariaDB2",
}
const [updatedDataSourceResponse, updatedDataSourceJson] =
await config.api.datasources.update(newDataSourceInfo)
// Query data source
const [queryResponse, queryJson] = await config.api.queries.preview(
fixtures.queries.mariaDB(updatedDataSourceJson.datasource._id!)
)
expect(queryJson.rows.length).toBeGreaterThan(9)
expect(queryJson.schemaFields).toEqual(
fixtures.queries.expectedSchemaFields.mariaDB
)
// Add relationship
const relationShipBody = fixtures.datasources.generateRelationshipForMySQL(
updatedDataSourceJson
)
const [relationshipResponse, relationshipJson] =
await config.api.datasources.update(relationShipBody)
})
})

View file

@ -1,69 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
import { Query } from "@budibase/types"
describe.skip("Internal API - Data Sources: MongoDB", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Create an app with a data source - MongoDB", async () => {
// Create app
await config.createApp()
// Get all integrations
await config.api.integrations.getAll()
// Add data source
const [dataSourceResponse, dataSourceJson] =
await config.api.datasources.add(fixtures.datasources.mongoDB())
// Update data source
const newDataSourceInfo = {
...dataSourceJson.datasource,
name: "MongoDB2",
}
const [updatedDataSourceResponse, updatedDataSourceJson] =
await config.api.datasources.update(newDataSourceInfo)
// Query data source
const [queryResponse, queryJson] = await config.api.queries.preview(
fixtures.queries.mongoDB(updatedDataSourceJson.datasource._id!)
)
expect(queryJson.rows.length).toBeGreaterThan(10)
expect(queryJson.schemaFields).toEqual(
fixtures.queries.expectedSchemaFields.mongoDB
)
// Save query
const datasourcetoSave: Query = {
...fixtures.queries.mongoDB(updatedDataSourceJson.datasource._id!),
parameters: [],
}
const [saveQueryResponse, saveQueryJson] = await config.api.queries.save(
datasourcetoSave
)
// Get Query
const [getQueryResponse, getQueryJson] = await config.api.queries.getQuery(
<string>saveQueryJson._id
)
// Get Query permissions
const [getQueryPermissionsResponse, getQueryPermissionsJson] =
await config.api.permissions.getAll(saveQueryJson._id!)
// Delete data source
const deleteResponse = await config.api.datasources.delete(
updatedDataSourceJson.datasource._id!,
updatedDataSourceJson.datasource._rev!
)
})
})

View file

@ -1,69 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
import { Query } from "@budibase/types"
describe("Internal API - Data Sources: PostgresSQL", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Create an app with a data source - PostgresSQL", async () => {
// Create app
await config.createApp()
// Get all integrations
await config.api.integrations.getAll()
// Add data source
const [dataSourceResponse, dataSourceJson] =
await config.api.datasources.add(fixtures.datasources.postgresSQL())
// Update data source
const newDataSourceInfo = {
...dataSourceJson.datasource,
name: "PostgresSQL2",
}
const [updatedDataSourceResponse, updatedDataSourceJson] =
await config.api.datasources.update(newDataSourceInfo)
// Query data source
const [queryResponse, queryJson] = await config.api.queries.preview(
fixtures.queries.postgres(updatedDataSourceJson.datasource._id!)
)
expect(queryJson.rows.length).toBeGreaterThan(10)
expect(queryJson.schemaFields).toEqual(
fixtures.queries.expectedSchemaFields.postgres
)
// Save query
const datasourcetoSave: Query = {
...fixtures.queries.postgres(updatedDataSourceJson.datasource._id!),
parameters: [],
}
const [saveQueryResponse, saveQueryJson] = await config.api.queries.save(
datasourcetoSave
)
// Get Query
const [getQueryResponse, getQueryJson] = await config.api.queries.getQuery(
saveQueryJson._id!
)
// Get Query permissions
const [getQueryPermissionsResponse, getQueryPermissionsJson] =
await config.api.permissions.getAll(saveQueryJson._id!)
// Delete data source
const deleteResponse = await config.api.datasources.delete(
updatedDataSourceJson.datasource._id!,
updatedDataSourceJson.datasource._rev!
)
})
})

View file

@ -1,69 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import * as fixtures from "../../fixtures"
import { Query } from "@budibase/types"
describe("Internal API - Data Sources: REST API", () => {
const config = new TestConfiguration()
beforeAll(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Create an app with a data source - REST API", async () => {
// Create app
await config.createApp()
// Get all integrations
await config.api.integrations.getAll()
// Add data source
const [dataSourceResponse, dataSourceJson] =
await config.api.datasources.add(fixtures.datasources.restAPI())
// Update data source
const newDataSourceInfo = {
...dataSourceJson.datasource,
name: "RestAPI - Updated",
}
const [updatedDataSourceResponse, updatedDataSourceJson] =
await config.api.datasources.update(newDataSourceInfo)
// Query data source
const [queryResponse, queryJson] = await config.api.queries.preview(
fixtures.queries.restAPI(updatedDataSourceJson.datasource._id!)
)
expect(queryJson.rows.length).toEqual(1)
expect(queryJson.schemaFields).toEqual(
fixtures.queries.expectedSchemaFields.restAPI
)
// Save query
const datasourcetoSave: Query = {
...fixtures.queries.postgres(updatedDataSourceJson.datasource._id!),
parameters: [],
}
const [saveQueryResponse, saveQueryJson] = await config.api.queries.save(
datasourcetoSave
)
// Get Query
const [getQueryResponse, getQueryJson] = await config.api.queries.getQuery(
saveQueryJson._id!
)
// Get Query permissions
const [getQueryPermissionsResponse, getQueryPermissionsJson] =
await config.api.permissions.getAll(saveQueryJson._id!)
// Delete data source
const deleteResponse = await config.api.datasources.delete(
updatedDataSourceJson.datasource._id!,
updatedDataSourceJson.datasource._rev!
)
})
})

View file

@ -1,315 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import { App, User } from "@budibase/types"
import { db } from "@budibase/backend-core"
import * as fixtures from "./../../fixtures"
describe.skip("Internal API - App Specific Roles & Permissions", () => {
const config = new TestConfiguration()
let app: Partial<App>
// Before each test, login as admin. Some tests will require login as a different user
beforeEach(async () => {
await config.beforeAll()
app = await config.createApp()
})
afterAll(async () => {
await config.afterAll()
})
it("Custom role access for level 1 permissions", async () => {
// Set up user
const appUser = fixtures.users.generateUser()
expect(appUser[0].builder?.global).toEqual(false)
expect(appUser[0].admin?.global).toEqual(false)
const [createUserResponse, createUserJson] =
await config.api.users.addMultiple(appUser)
//Create level 1 role
const role = {
inherits: "BASIC",
permissionId: "public",
name: "level 1",
}
const [createRoleResponse, createRoleJson] =
await config.api.users.createRole(role)
// Update user roles
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
createUserJson.created.successful[0]._id
)
const prodAppId = db.getProdAppID(app.appId!)
// Roles must always be set with prod appID
const body: User = {
...userInfoJson,
roles: {
[prodAppId]: createRoleJson._id,
},
}
await config.api.users.updateInfo(body)
const [changedUserInfoResponse, changedUserInfoJson] =
await config.api.users.getInfo(createUserJson.created.successful[0]._id)
expect(changedUserInfoJson.roles[prodAppId]).toBeDefined()
expect(changedUserInfoJson.roles[prodAppId]).toEqual(createRoleJson._id)
await config.api.screens.create(fixtures.screens.generateScreen("BASIC"))
await config.api.screens.create(fixtures.screens.generateScreen("POWER"))
await config.api.screens.create(fixtures.screens.generateScreen("ADMIN"))
await config.api.apps.publish(app.appId)
const [firstappPackageResponse, firstappPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(firstappPackageJson.screens).toBeDefined()
expect(firstappPackageJson.screens.length).toEqual(3)
// login with level 1 user
await config.login(
config.state.tenantId!,
appUser[0].email!,
appUser[0].password!
)
const [selfInfoResponse, selfInfoJson] = await config.api.users.getSelf()
// fetch app package
const [appPackageResponse, appPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(appPackageJson.screens).toBeDefined()
expect(appPackageJson.screens.length).toEqual(1)
})
it("Custom role access for level 2 permissions", async () => {
// Set up user
const appUser = fixtures.users.generateUser()
expect(appUser[0].builder?.global).toEqual(false)
expect(appUser[0].admin?.global).toEqual(false)
const [createUserResponse, createUserJson] =
await config.api.users.addMultiple(appUser)
// Create App
//Create level 1 role
const role = {
inherits: "BASIC",
permissionId: "read_only",
name: "level 2",
}
const [createRoleResponse, createRoleJson] =
await config.api.users.createRole(role)
// Update user roles
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
createUserJson.created.successful[0]._id
)
const prodAppId = db.getProdAppID(app.appId!)
// Roles must always be set with prod appID
const body: User = {
...userInfoJson,
roles: {
[prodAppId]: createRoleJson._id,
},
}
await config.api.users.updateInfo(body)
const [changedUserInfoResponse, changedUserInfoJson] =
await config.api.users.getInfo(createUserJson.created.successful[0]._id)
expect(changedUserInfoJson.roles[prodAppId]).toBeDefined()
expect(changedUserInfoJson.roles[prodAppId]).toEqual(createRoleJson._id)
await config.api.screens.create(fixtures.screens.generateScreen("BASIC"))
await config.api.screens.create(fixtures.screens.generateScreen("POWER"))
await config.api.screens.create(fixtures.screens.generateScreen("ADMIN"))
await config.api.apps.publish(app.appId)
const [firstappPackageResponse, firstappPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(firstappPackageJson.screens).toBeDefined()
expect(firstappPackageJson.screens.length).toEqual(3)
// login with level 1 user
await config.login(appUser[0].email!, appUser[0].password!)
const [selfInfoResponse, selfInfoJson] = await config.api.users.getSelf()
// fetch app package
const [appPackageResponse, appPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(appPackageJson.screens).toBeDefined()
expect(appPackageJson.screens.length).toEqual(1)
})
it("Custom role access for level 3 permissions", async () => {
const appUser = fixtures.users.generateUser()
expect(appUser[0].builder?.global).toEqual(false)
expect(appUser[0].admin?.global).toEqual(false)
const [createUserResponse, createUserJson] =
await config.api.users.addMultiple(appUser)
// Create App
//Create level 1 role
const role = {
inherits: "BASIC",
permissionId: "write",
name: "level 3",
}
const [createRoleResponse, createRoleJson] =
await config.api.users.createRole(role)
// Update user roles
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
createUserJson.created.successful[0]._id
)
const prodAppId = db.getProdAppID(app.appId!)
// Roles must always be set with prod appID
const body: User = {
...userInfoJson,
roles: {
[prodAppId]: createRoleJson._id,
},
}
await config.api.users.updateInfo(body)
const [changedUserInfoResponse, changedUserInfoJson] =
await config.api.users.getInfo(createUserJson.created.successful[0]._id)
expect(changedUserInfoJson.roles[prodAppId]).toBeDefined()
expect(changedUserInfoJson.roles[prodAppId]).toEqual(createRoleJson._id)
await config.api.screens.create(fixtures.screens.generateScreen("BASIC"))
await config.api.screens.create(fixtures.screens.generateScreen("POWER"))
await config.api.screens.create(fixtures.screens.generateScreen("ADMIN"))
await config.api.apps.publish(app.appId)
const [firstappPackageResponse, firstappPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(firstappPackageJson.screens).toBeDefined()
expect(firstappPackageJson.screens.length).toEqual(3)
// login with level 1 user
await config.login(appUser[0].email!, appUser[0].password!)
const [selfInfoResponse, selfInfoJson] = await config.api.users.getSelf()
// fetch app package
const [appPackageResponse, appPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(appPackageJson.screens).toBeDefined()
expect(appPackageJson.screens.length).toEqual(1)
})
it("Custom role access for level 4 permissions", async () => {
const appUser = fixtures.users.generateUser()
expect(appUser[0].builder?.global).toEqual(false)
expect(appUser[0].admin?.global).toEqual(false)
const [createUserResponse, createUserJson] =
await config.api.users.addMultiple(appUser)
// Create App
//Create level 1 role
const role = {
inherits: "BASIC",
permissionId: "power",
name: "level 4",
}
const [createRoleResponse, createRoleJson] =
await config.api.users.createRole(role)
// Update user roles
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
createUserJson.created.successful[0]._id
)
const prodAppId = db.getProdAppID(app.appId!)
// Roles must always be set with prod appID
const body: User = {
...userInfoJson,
roles: {
[prodAppId]: createRoleJson._id,
},
}
await config.api.users.updateInfo(body)
const [changedUserInfoResponse, changedUserInfoJson] =
await config.api.users.getInfo(createUserJson.created.successful[0]._id)
expect(changedUserInfoJson.roles[prodAppId]).toBeDefined()
expect(changedUserInfoJson.roles[prodAppId]).toEqual(createRoleJson._id)
await config.api.screens.create(fixtures.screens.generateScreen("BASIC"))
await config.api.screens.create(fixtures.screens.generateScreen("POWER"))
await config.api.screens.create(fixtures.screens.generateScreen("ADMIN"))
await config.api.apps.publish(app.appId)
const [firstappPackageResponse, firstappPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(firstappPackageJson.screens).toBeDefined()
expect(firstappPackageJson.screens.length).toEqual(3)
// login with level 1 user
await config.login(appUser[0].email!, appUser[0].password!)
const [selfInfoResponse, selfInfoJson] = await config.api.users.getSelf()
// fetch app package
const [appPackageResponse, appPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(appPackageJson.screens).toBeDefined()
expect(appPackageJson.screens.length).toEqual(1)
})
it("Custom role access for level 5 permissions", async () => {
const appUser = fixtures.users.generateUser()
expect(appUser[0].builder?.global).toEqual(false)
expect(appUser[0].admin?.global).toEqual(false)
const [createUserResponse, createUserJson] =
await config.api.users.addMultiple(appUser)
// Create App
//Create level 1 role
const role = {
inherits: "BASIC",
permissionId: "admin",
name: "level 5",
}
const [createRoleResponse, createRoleJson] =
await config.api.users.createRole(role)
// Update user roles
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
createUserJson.created.successful[0]._id
)
const prodAppId = db.getProdAppID(app.appId!)
// Roles must always be set with prod appID
const body: User = {
...userInfoJson,
roles: {
[prodAppId]: createRoleJson._id,
},
}
await config.api.users.updateInfo(body)
const [changedUserInfoResponse, changedUserInfoJson] =
await config.api.users.getInfo(createUserJson.created.successful[0]._id)
expect(changedUserInfoJson.roles[prodAppId]).toBeDefined()
expect(changedUserInfoJson.roles[prodAppId]).toEqual(createRoleJson._id)
await config.api.screens.create(fixtures.screens.generateScreen("BASIC"))
await config.api.screens.create(fixtures.screens.generateScreen("POWER"))
await config.api.screens.create(fixtures.screens.generateScreen("ADMIN"))
await config.api.apps.publish(app.appId)
const [firstappPackageResponse, firstappPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(firstappPackageJson.screens).toBeDefined()
expect(firstappPackageJson.screens.length).toEqual(3)
// login with level 1 user
await config.login(appUser[0].email!, appUser[0].password!)
const [selfInfoResponse, selfInfoJson] = await config.api.users.getSelf()
// fetch app package
const [appPackageResponse, appPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(appPackageJson.screens).toBeDefined()
expect(appPackageJson.screens.length).toEqual(1)
})
})

View file

@ -1,175 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import { User } from "@budibase/types"
import { db } from "@budibase/backend-core"
import * as fixtures from "./../../fixtures"
describe.skip("Internal API - Role screen access", () => {
const config = new TestConfiguration()
// Before each test, login as admin. Some tests will require login as a different user
beforeEach(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Check Screen access for BASIC Role", async () => {
// Set up user
const appUser = fixtures.users.generateUser()
expect(appUser[0].builder?.global).toEqual(false)
expect(appUser[0].admin?.global).toEqual(false)
const [createUserResponse, createUserJson] =
await config.api.users.addMultiple(appUser)
// Create App
const app = await config.createApp()
// Update user roles
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
createUserJson.created.successful[0]._id
)
const prodAppId = db.getProdAppID(app.appId!)
// Roles must always be set with prod appID
const body: User = {
...userInfoJson,
roles: {
[prodAppId]: "BASIC",
},
}
await config.api.users.updateInfo(body)
const [changedUserInfoResponse, changedUserInfoJson] =
await config.api.users.getInfo(createUserJson.created.successful[0]._id)
expect(changedUserInfoJson.roles[prodAppId]).toBeDefined()
expect(changedUserInfoJson.roles[prodAppId]).toEqual("BASIC")
await config.api.screens.create(fixtures.screens.generateScreen("BASIC"))
await config.api.screens.create(fixtures.screens.generateScreen("POWER"))
await config.api.screens.create(fixtures.screens.generateScreen("ADMIN"))
await config.api.apps.publish(app.appId)
const [firstappPackageResponse, firstappPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(firstappPackageJson.screens).toBeDefined()
expect(firstappPackageJson.screens.length).toEqual(3)
// login with BASIC user
await config.login(appUser[0].email!, appUser[0].password!)
const [selfInfoResponse, selfInfoJson] = await config.api.users.getSelf()
// fetch app package
const [appPackageResponse, appPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(appPackageJson.screens).toBeDefined()
expect(appPackageJson.screens.length).toEqual(1)
expect(appPackageJson.screens[0].routing.roleId).toEqual("BASIC")
})
it("Check Screen access for POWER role", async () => {
// Set up user
const appUser = fixtures.users.generateUser()
expect(appUser[0].builder?.global).toEqual(false)
expect(appUser[0].admin?.global).toEqual(false)
const [createUserResponse, createUserJson] =
await config.api.users.addMultiple(appUser)
// Create App
const app = await config.createApp()
// Update user roles
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
createUserJson.created.successful[0]._id
)
const prodAppId = db.getProdAppID(app.appId!)
// Roles must always be set with prod appID
const body: User = {
...userInfoJson,
roles: {
[prodAppId]: "POWER",
},
}
await config.api.users.updateInfo(body)
const [changedUserInfoResponse, changedUserInfoJson] =
await config.api.users.getInfo(createUserJson.created.successful[0]._id)
expect(changedUserInfoJson.roles[prodAppId]).toBeDefined()
expect(changedUserInfoJson.roles[prodAppId]).toEqual("POWER")
await config.api.screens.create(fixtures.screens.generateScreen("BASIC"))
await config.api.screens.create(fixtures.screens.generateScreen("POWER"))
await config.api.screens.create(fixtures.screens.generateScreen("ADMIN"))
await config.api.apps.publish(app.appId)
const [firstappPackageResponse, firstappPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(firstappPackageJson.screens).toBeDefined()
expect(firstappPackageJson.screens.length).toEqual(3)
// login with POWER user
await config.login(appUser[0].email!, appUser[0].password!)
const [selfInfoResponse, selfInfoJson] = await config.api.users.getSelf()
// fetch app package
const [appPackageResponse, appPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(appPackageJson.screens).toBeDefined()
expect(appPackageJson.screens.length).toEqual(2)
})
it("Check Screen access for ADMIN role", async () => {
// Set up user
const appUser = fixtures.users.generateUser()
expect(appUser[0].builder?.global).toEqual(false)
expect(appUser[0].admin?.global).toEqual(false)
const [createUserResponse, createUserJson] =
await config.api.users.addMultiple(appUser)
// Create App
const app = await config.createApp()
// Update user roles
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
createUserJson.created.successful[0]._id
)
const prodAppId = db.getProdAppID(app.appId!)
// Roles must always be set with prod appID
const body: User = {
...userInfoJson,
roles: {
[prodAppId]: "ADMIN",
},
}
await config.api.users.updateInfo(body)
const [changedUserInfoResponse, changedUserInfoJson] =
await config.api.users.getInfo(createUserJson.created.successful[0]._id)
expect(changedUserInfoJson.roles[prodAppId]).toBeDefined()
expect(changedUserInfoJson.roles[prodAppId]).toEqual("ADMIN")
await config.api.screens.create(fixtures.screens.generateScreen("BASIC"))
await config.api.screens.create(fixtures.screens.generateScreen("POWER"))
await config.api.screens.create(fixtures.screens.generateScreen("ADMIN"))
await config.api.apps.publish(app.appId)
const [firstappPackageResponse, firstappPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(firstappPackageJson.screens).toBeDefined()
expect(firstappPackageJson.screens.length).toEqual(3)
// login with ADMIN user
await config.login(appUser[0].email!, appUser[0].password!)
const [selfInfoResponse, selfInfoJson] = await config.api.users.getSelf()
// fetch app package
const [appPackageResponse, appPackageJson] =
await config.api.apps.getAppPackage(app.appId!)
expect(appPackageJson.screens).toBeDefined()
expect(appPackageJson.screens.length).toEqual(3)
})
})

View file

@ -1,123 +0,0 @@
import TestConfiguration from "../../config/TestConfiguration"
import { User } from "@budibase/types"
import * as fixtures from "./../../fixtures"
describe.skip("Internal API - Role table access", () => {
const config = new TestConfiguration()
// Before each test, login as admin. Some tests will require login as a different user
beforeEach(async () => {
await config.beforeAll()
})
afterAll(async () => {
await config.afterAll()
})
it("Check Table access for app user", async () => {
const appUser = fixtures.users.generateUser()
expect(appUser[0].builder?.global).toEqual(false)
expect(appUser[0].admin?.global).toEqual(false)
const [createUserResponse, createUserJson] =
await config.api.users.addMultiple(appUser)
const app = await config.createApp()
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
createUserJson.created.successful[0]._id
)
const body: User = {
...userInfoJson,
roles: {
[app.appId!]: "BASIC",
},
}
await config.api.users.updateInfo(body)
const [changedUserInfoResponse, changedUserInfoJson] =
await config.api.users.getInfo(createUserJson.created.successful[0]._id)
expect(changedUserInfoJson.roles[app.appId!]).toBeDefined()
expect(changedUserInfoJson.roles[app.appId!]).toEqual("BASIC")
const [createdTableResponse, createdTableData] =
await config.api.tables.save(fixtures.tables.generateTable())
await config.login(appUser[0].email!, appUser[0].password!)
const newColumn =
fixtures.tables.generateNewColumnForTable(createdTableData)
await config.api.tables.forbiddenSave(newColumn)
await config.api.tables.forbiddenSave(fixtures.tables.generateTable())
})
it.skip("Check Table access for developer", async () => {
const developer = fixtures.users.generateUser(1, "developer")
expect(developer[0].builder?.global).toEqual(true)
const [createUserResponse, createUserJson] =
await config.api.users.addMultiple(developer)
const app = await config.createApp()
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
createUserJson.created.successful[0]._id
)
const body: User = {
...userInfoJson,
roles: {
[app.appId!]: "POWER",
},
}
await config.api.users.updateInfo(body)
const [changedUserInfoResponse, changedUserInfoJson] =
await config.api.users.getInfo(createUserJson.created.successful[0]._id)
expect(changedUserInfoJson.roles[app.appId!]).toBeDefined()
expect(changedUserInfoJson.roles[app.appId!]).toEqual("POWER")
const [createdTableResponse, createdTableData] =
await config.api.tables.save(fixtures.tables.generateTable())
await config.login(developer[0].email!, developer[0].password!)
const newColumn =
fixtures.tables.generateNewColumnForTable(createdTableData)
const [addColumnResponse, addColumnData] = await config.api.tables.save(
newColumn,
true
)
})
it.skip("Check Table access for admin", async () => {
const adminUser = fixtures.users.generateUser(1, "admin")
expect(adminUser[0].builder?.global).toEqual(true)
expect(adminUser[0].admin?.global).toEqual(true)
const [createUserResponse, createUserJson] =
await config.api.users.addMultiple(adminUser)
const app = await config.createApp()
const [userInfoResponse, userInfoJson] = await config.api.users.getInfo(
createUserJson.created.successful[0]._id
)
const body: User = {
...userInfoJson,
roles: {
[app.appId!]: "ADMIN",
},
}
await config.api.users.updateInfo(body)
const [changedUserInfoResponse, changedUserInfoJson] =
await config.api.users.getInfo(createUserJson.created.successful[0]._id)
expect(changedUserInfoJson.roles[app.appId!]).toBeDefined()
expect(changedUserInfoJson.roles[app.appId!]).toEqual("ADMIN")
await config.login(adminUser[0].email!, adminUser[0].password!)
const [createdTableResponse, createdTableData] =
await config.api.tables.save(fixtures.tables.generateTable())
const newColumn =
fixtures.tables.generateNewColumnForTable(createdTableData)
const [addColumnResponse, addColumnData] = await config.api.tables.save(
newColumn,
true
)
})
})

View file

@ -1,114 +0,0 @@
import { DEFAULT_TENANT_ID } from "@budibase/backend-core"
import { AccountInternalAPI } from "../account-api"
import * as fixtures from "../internal-api/fixtures"
import { BudibaseInternalAPI } from "../internal-api"
import { Account, CreateAccountRequest, Feature } from "@budibase/types"
import env from "../environment"
import { APIRequestOpts } from "../types"
const accountsApi = new AccountInternalAPI({})
const internalApi = new BudibaseInternalAPI({})
const API_OPTS: APIRequestOpts = { doExpect: false }
// @ts-ignore
global.qa = {}
async function createAccount(): Promise<[CreateAccountRequest, Account]> {
const account = fixtures.accounts.generateAccount()
await accountsApi.accounts.validateEmail(account.email, API_OPTS)
await accountsApi.accounts.validateTenantId(account.tenantId, API_OPTS)
const [res, newAccount] = await accountsApi.accounts.create(account, {
...API_OPTS,
autoVerify: true,
})
await updateLicense(newAccount.accountId)
return [account, newAccount]
}
const UNLIMITED = { value: -1 }
async function updateLicense(accountId: string) {
const [response] = await accountsApi.licenses.updateLicense(
accountId,
{
overrides: {
// add all features
features: Object.values(Feature),
quotas: {
usage: {
monthly: {
automations: UNLIMITED,
},
static: {
rows: UNLIMITED,
users: UNLIMITED,
userGroups: UNLIMITED,
plugins: UNLIMITED,
},
},
},
},
},
{ doExpect: false }
)
if (response.status !== 200) {
throw new Error(
`Could not update license for accountId=${accountId}: ${response.status}`
)
}
}
async function loginAsAdmin() {
const username = env.BB_ADMIN_USER_EMAIL!
const password = env.BB_ADMIN_USER_PASSWORD!
const tenantId = DEFAULT_TENANT_ID
const [res, cookie] = await internalApi.auth.login(
tenantId,
username,
password,
API_OPTS
)
// @ts-ignore
global.qa.authCookie = cookie
}
async function loginAsAccount(account: CreateAccountRequest) {
const [res, cookie] = await accountsApi.auth.login(
account.email,
account.password,
API_OPTS
)
// @ts-ignore
global.qa.authCookie = cookie
}
async function setup() {
console.log("\nGLOBAL SETUP STARTING")
const env = await internalApi.environment.getEnvironment(API_OPTS)
console.log(`Environment: ${JSON.stringify(env)}`)
if (env.multiTenancy) {
const [account, newAccount] = await createAccount()
// @ts-ignore
global.qa.tenantId = account.tenantId
// @ts-ignore
global.qa.email = account.email
// @ts-ignore
global.qa.accountId = newAccount.accountId
await loginAsAccount(account)
} else {
// @ts-ignore
global.qa.tenantId = DEFAULT_TENANT_ID
await loginAsAdmin()
}
// @ts-ignore
console.log(`Tenant: ${global.qa.tenantId}`)
console.log("GLOBAL SETUP COMPLETE")
}
export default setup

View file

@ -1,32 +0,0 @@
import { AccountInternalAPI } from "../account-api"
import { BudibaseInternalAPI } from "../internal-api"
import { APIRequestOpts } from "../types"
const accountsApi = new AccountInternalAPI({})
const internalApi = new BudibaseInternalAPI({})
const API_OPTS: APIRequestOpts = { doExpect: false }
async function deleteAccount() {
// @ts-ignore
const accountID = global.qa.accountId
const [response] = await accountsApi.accounts.delete(accountID, {
doExpect: false,
})
if (response.status !== 204) {
throw new Error(`status: ${response.status} not equal to expected: 201`)
}
}
async function teardown() {
console.log("\nGLOBAL TEARDOWN STARTING")
const env = await internalApi.environment.getEnvironment(API_OPTS)
if (env.multiTenancy) {
await deleteAccount()
}
console.log("GLOBAL TEARDOWN COMPLETE")
}
export default teardown

View file

@ -1,23 +0,0 @@
import { Response } from "node-fetch"
// boilerplate to allow TS updates to the global scope
export {}
declare global {
namespace jest {
interface Matchers<R> {
toHaveStatusCode(code: number): R
}
}
}
// Expect extensions
expect.extend({
toHaveStatusCode(received: Response, code: number) {
const pass = received.status === code
return {
message: () => `expected ${received.status} to match status code ${code}`,
pass,
}
},
})

View file

@ -1,3 +0,0 @@
const envTimeout = process.env.JEST_TIMEOUT
const timeout = envTimeout && parseInt(envTimeout)
jest.setTimeout(timeout || 60000)

View file

@ -1,23 +0,0 @@
import AppAPI from "./apis/AppAPI"
import UserAPI from "./apis/UserAPI"
import TableAPI from "./apis/TableAPI"
import RowAPI from "./apis/RowAPI"
import BudibasePublicAPIClient from "./BudibasePublicAPIClient"
import { State } from "../../types"
export default class BudibasePublicAPI {
client: BudibasePublicAPIClient
apps: AppAPI
users: UserAPI
tables: TableAPI
rows: RowAPI
constructor(state: State) {
this.client = new BudibasePublicAPIClient(state)
this.apps = new AppAPI(this.client)
this.users = new UserAPI(this.client)
this.tables = new TableAPI(this.client)
this.rows = new RowAPI(this.client, state)
}
}

View file

@ -1,79 +0,0 @@
import env from "../../environment"
import fetch, { HeadersInit } from "node-fetch"
import { State } from "../../types"
type APIMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
interface ApiOptions {
method?: APIMethod
body?: object
headers?: HeadersInit | undefined
}
class BudibasePublicAPIClient {
state: State
host: string
constructor(state: State) {
if (!env.BUDIBASE_URL) {
throw new Error("Must set BUDIBASE_URL env var")
}
this.host = `${env.BUDIBASE_URL}/api/public/v1`
this.state = state
}
apiCall =
(method: APIMethod) =>
async (url = "", options: ApiOptions = {}) => {
const requestOptions = {
method,
body: JSON.stringify(options.body),
headers: {
"x-budibase-api-key": this.state.apiKey,
"x-budibase-app-id": this.state.appId,
"Content-Type": "application/json",
Accept: "application/json",
...options.headers,
redirect: "follow",
follow: 20,
},
}
// prettier-ignore
// @ts-ignore
const response = await fetch(`${this.host}${url}`, requestOptions)
let body: any
const contentType = response.headers.get("content-type")
if (contentType && contentType.includes("application/json")) {
body = await response.json()
} else {
body = await response.text()
}
const data = {
request: requestOptions.body,
response: body,
}
const message = `${method} ${url} - ${response.status}`
const isDebug = process.env.LOG_LEVEL === "debug"
if (response.status > 499) {
console.error(message, data)
} else if (response.status >= 400) {
console.warn(message, data)
} else if (isDebug) {
console.debug(message, data)
}
return [response, body]
}
post = this.apiCall("POST")
get = this.apiCall("GET")
patch = this.apiCall("PATCH")
del = this.apiCall("DELETE")
put = this.apiCall("PUT")
}
export default BudibasePublicAPIClient

View file

@ -1,68 +0,0 @@
import { Response } from "node-fetch"
import BudibasePublicAPIClient from "../BudibasePublicAPIClient"
import * as fixtures from "../../fixtures"
import {
Application,
SearchInputParams,
CreateApplicationParams,
} from "../../../types"
export default class AppAPI {
client: BudibasePublicAPIClient
constructor(client: BudibasePublicAPIClient) {
this.client = client
}
async seed(): Promise<[Response, Application]> {
return this.create(fixtures.apps.generateApp())
}
async create(
body: CreateApplicationParams
): Promise<[Response, Application]> {
const [response, json] = await this.client.post(`/applications`, { body })
return [response, json.data]
}
async read(id: string): Promise<[Response, Application]> {
const [response, json] = await this.client.get(`/applications/${id}`)
return [response, json.data]
}
async search(body: SearchInputParams): Promise<[Response, [Application]]> {
const [response, json] = await this.client.post(`/applications/search`, {
body,
})
return [response, json.data]
}
async update(
id: string,
body: Application
): Promise<[Response, Application]> {
const [response, json] = await this.client.put(`/applications/${id}`, {
body,
})
return [response, json.data]
}
async delete(id: string): Promise<[Response, Application]> {
const [response, json] = await this.client.del(`/applications/${id}`)
return [response, json.data]
}
async publish(id: string): Promise<[Response, any]> {
const [response, json] = await this.client.post(
`/applications/${id}/publish`
)
return [response, json.data]
}
async unpublish(id: string): Promise<[Response]> {
const [response, json] = await this.client.post(
`/applications/${id}/unpublish`
)
return [response]
}
}

View file

@ -1,56 +0,0 @@
import { CreateRowParams, Row, SearchInputParams, State } from "../../../types"
import { HeadersInit, Response } from "node-fetch"
import BudibasePublicAPIClient from "../BudibasePublicAPIClient"
import * as fixtures from "../../fixtures"
export default class RowAPI {
client: BudibasePublicAPIClient
headers?: HeadersInit
state: State
constructor(client: BudibasePublicAPIClient, state: State) {
this.state = state
this.client = client
}
async seed(tableId: string) {
return this.create(fixtures.rows.generateRow({ tableId }))
}
async create(body: CreateRowParams): Promise<[Response, Row]> {
const [response, json] = await this.client.post(
`/tables/${this.state.tableId}/rows`,
{
body,
}
)
return [response, json.data]
}
async read(id: string): Promise<[Response, Row]> {
const [response, json] = await this.client.get(
`/tables/${this.state.tableId}/rows/${id}`
)
return [response, json.data]
}
async search(body: SearchInputParams): Promise<[Response, [Row]]> {
const [response, json] = await this.client.post(
`/tables/${this.state.tableId}/rows/search`,
{ body }
)
return [response, json.data]
}
async update(id: string, body: Row): Promise<[Response, Row]> {
const [response, json] = await this.client.put(
`/tables/${this.state.tableId}/rows/${id}`,
{
body,
}
)
return [response, json.data]
}
}

View file

@ -1,40 +0,0 @@
import { Table, SearchInputParams, CreateTableParams } from "../../../types"
import { HeadersInit, Response } from "node-fetch"
import { generateTable } from "../../fixtures/tables"
import BudibasePublicAPIClient from "../BudibasePublicAPIClient"
export default class TableAPI {
headers?: HeadersInit
client: BudibasePublicAPIClient
constructor(client: BudibasePublicAPIClient) {
this.client = client
}
async seed() {
return this.create(generateTable())
}
async create(body: CreateTableParams): Promise<[Response, Table]> {
const [response, json] = await this.client.post(`/tables`, {
body,
})
return [response, json.data]
}
async read(id: string): Promise<[Response, Table]> {
const [response, json] = await this.client.get(`/tables/${id}`)
return [response, json.data]
}
async search(body: SearchInputParams): Promise<[Response, [Table]]> {
const [response, json] = await this.client.post(`/tables/search`, { body })
return [response, json.data]
}
async update(id: string, body: Table): Promise<[Response, Table]> {
const [response, json] = await this.client.put(`/tables/${id}`, { body })
return [response, json.data]
}
}

View file

@ -1,36 +0,0 @@
import { CreateUserParams, SearchInputParams, User } from "../../../types"
import { Response } from "node-fetch"
import BudibasePublicAPIClient from "../BudibasePublicAPIClient"
import * as fixtures from "../../fixtures"
export default class UserAPI {
client: BudibasePublicAPIClient
constructor(client: BudibasePublicAPIClient) {
this.client = client
}
async seed() {
return this.create(fixtures.users.generateUser())
}
async create(body: CreateUserParams): Promise<[Response, User]> {
const [response, json] = await this.client.post(`/users`, { body })
return [response, json.data]
}
async read(id: string): Promise<[Response, User]> {
const [response, json] = await this.client.get(`/users/${id}`)
return [response, json.data]
}
async search(body: SearchInputParams): Promise<[Response, [User]]> {
const [response, json] = await this.client.post(`/users/search`, { body })
return [response, json.data]
}
async update(id: string, body: User): Promise<[Response, User]> {
const [response, json] = await this.client.put(`/users/${id}`, { body })
return [response, json.data]
}
}

View file

@ -1 +0,0 @@
export { default as BudibasePublicAPI } from "./BudibasePublicAPI"

View file

@ -1,33 +0,0 @@
import { BudibasePublicAPI } from "../api"
import { BudibaseTestConfiguration } from "../../shared"
export default class TestConfiguration<T> extends BudibaseTestConfiguration {
// apis
api: BudibasePublicAPI
context: T
constructor() {
super()
this.api = new BudibasePublicAPI(this.state)
this.context = <T>{}
}
// LIFECYCLE
async beforeAll() {
await super.beforeAll()
await this.setApiKey()
}
async afterAll() {
await super.afterAll()
}
// AUTH
async setApiKey() {
const apiKeyResponse = await this.internalApi.self.getApiKey()
this.state.apiKey = apiKeyResponse.apiKey
}
}

View file

@ -1 +0,0 @@
export { default as TestConfiguration } from "./TestConfiguration"

View file

@ -1,20 +0,0 @@
import { generator } from "../../shared"
import { CreateAccountRequest, Hosting } from "@budibase/types"
export const generateAccount = (): CreateAccountRequest => {
const randomGuid = generator.guid()
//Needs to start with a letter
let tenant: string = "tenant" + randomGuid
tenant = tenant.replace(/-/g, "")
return {
email: `qa+${randomGuid}@budibase.com`,
hosting: Hosting.CLOUD,
name: `qa+${randomGuid}@budibase.com`,
password: `${randomGuid}`,
profession: "software_engineer",
size: "10+",
tenantId: `${tenant}`,
tenantName: `${tenant}`,
}
}

View file

@ -1,10 +0,0 @@
import { generator } from "../../shared"
import { Application, CreateApplicationParams } from "../../types"
export const generateApp = (
overrides: Partial<Application> = {}
): CreateApplicationParams => ({
name: generator.word(),
url: `/${generator.word()}`,
...overrides,
})

View file

@ -1,5 +0,0 @@
export * as accounts from "./accounts"
export * as apps from "./applications"
export * as rows from "./rows"
export * as tables from "./tables"
export * as users from "./users"

View file

@ -1,10 +0,0 @@
import { CreateRowParams, Row } from "../../types"
import { generator } from "../../shared"
export const generateRow = (overrides: Partial<Row> = {}): CreateRowParams => ({
type: "row",
tableId: "seed_table",
testColumn: generator.string({ length: 32, alpha: true, numeric: true }),
relationship: [generator.string({ length: 32, alpha: true, numeric: true })],
...overrides,
})

View file

@ -1,47 +0,0 @@
import { CreateTableParams, Table } from "../../types"
import { generator } from "../../shared"
export const generateTable = (
overrides: Partial<Table> = {}
): CreateTableParams => ({
name: generator.word(),
primaryDisplay: "testColumn",
schema: {
"Auto ID": {
autocolumn: true,
name: "Auto ID",
type: "number",
},
"Created At": {
autocolumn: true,
name: "Created At",
type: "datetime",
},
"Created By": {
autocolumn: true,
fieldName: generator.word(),
name: "Created By",
relationshipType: "many-to-many",
tableId: "ta_users",
type: "link",
},
testColumn: {
name: "testColumn",
type: "string",
},
"Updated At": {
autocolumn: true,
name: "Updated At",
type: "datetime",
},
"Updated By": {
autocolumn: true,
fieldName: generator.word(),
name: "Updated By",
relationshipType: "many-to-many",
tableId: "ta_users",
type: "link",
},
},
...overrides,
})

View file

@ -1,22 +0,0 @@
import { CreateUserParams, User } from "../../types"
import { generator } from "../../shared"
export const generateUser = (
overrides: Partial<User> = {}
): CreateUserParams => ({
email: generator.email({ domain: "example.com" }),
roles: {
[generator.string({ length: 32, alpha: true, numeric: true })]:
generator.word(),
},
password: generator.word(),
status: "active",
forceResetPassword: generator.bool(),
builder: {
global: generator.bool(),
},
admin: {
global: generator.bool(),
},
...overrides,
})

Some files were not shown because too many files have changed in this diff Show more