1
0
Fork 0
mirror of synced 2024-07-06 23:10:57 +12:00

Merge branch 'master' of github.com:budibase/budibase into reuse-containers

This commit is contained in:
Sam Rose 2024-04-03 11:15:11 +01:00
commit e3fbce25fa
No known key found for this signature in database
142 changed files with 198 additions and 10970 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

@ -65,7 +65,9 @@ jobs:
# Run build all the projects
- name: Build
run: yarn build
run: |
yarn build:oss
yarn build:account-portal
# Check the types of the projects built via esbuild
- name: Check types
run: |
@ -174,35 +176,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

@ -1,5 +1,5 @@
{
"version": "2.22.12",
"version": "2.22.15",
"npmClient": "yarn",
"packages": [
"packages/*",

View file

@ -34,6 +34,8 @@
"get-past-client-version": "node scripts/getPastClientVersion.js",
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
"build": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream",
"build:oss": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --ignore @budibase/account-portal --ignore @budibase/account-portal-server --ignore @budibase/account-portal-ui",
"build:account-portal": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --scope @budibase/account-portal --scope @budibase/account-portal-server --scope @budibase/account-portal-ui",
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
"check:types": "lerna run check:types",
"build:sdk": "lerna run --stream build:sdk",
@ -56,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",

@ -1 +1 @@
Subproject commit 60658a052d2642e5f4a8038e253f771a24f34907
Subproject commit 360ad2dc29c3f1fd5a1182ae258c45666b7f5eb1

View file

@ -20,7 +20,7 @@ export async function lookupTenantId(userId: string) {
return user.tenantId
}
async function getUserDoc(emailOrId: string): Promise<PlatformUser> {
export async function getUserDoc(emailOrId: string): Promise<PlatformUser> {
const db = getPlatformDB()
return db.get(emailOrId)
}
@ -79,6 +79,17 @@ async function addUserDoc(emailOrId: string, newDocFn: () => PlatformUser) {
}
}
export async function addSsoUser(
ssoId: string,
email: string,
userId: string,
tenantId: string
) {
return addUserDoc(ssoId, () =>
newUserSsoIdDoc(ssoId, email, userId, tenantId)
)
}
export async function addUser(
tenantId: string,
userId: string,
@ -91,9 +102,7 @@ export async function addUser(
]
if (ssoId) {
promises.push(
addUserDoc(ssoId, () => newUserSsoIdDoc(ssoId, email, userId, tenantId))
)
promises.push(addSsoUser(ssoId, email, userId, tenantId))
}
await Promise.all(promises)

View file

@ -14,16 +14,16 @@ import {
} from "../db"
import {
BulkDocsResponse,
ContextUser,
CouchFindOptions,
DatabaseQueryOpts,
SearchQuery,
SearchQueryOperators,
SearchUsersRequest,
User,
ContextUser,
DatabaseQueryOpts,
CouchFindOptions,
} from "@budibase/types"
import { getGlobalDB } from "../context"
import * as context from "../context"
import { getGlobalDB } from "../context"
import { isCreator } from "./utils"
import { UserDB } from "./db"
@ -48,6 +48,7 @@ export function isSupportedUserSearch(query: SearchQuery) {
const allowed = [
{ op: SearchQueryOperators.STRING, key: "email" },
{ op: SearchQueryOperators.EQUAL, key: "_id" },
{ op: SearchQueryOperators.ONE_OF, key: "_id" },
]
for (let [key, operation] of Object.entries(query)) {
if (typeof operation !== "object") {
@ -285,6 +286,10 @@ export async function paginatedUsers({
} else if (query?.string?.email) {
userList = await searchGlobalUsersByEmail(query?.string?.email, opts)
property = "email"
} else if (query?.oneOf?._id) {
userList = await bulkGetGlobalUsersById(query?.oneOf?._id, {
cleanup: true,
})
} else {
// no search, query allDocs
const response = await db.allDocs(getGlobalUserParams(null, opts))

View file

@ -31,7 +31,7 @@
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
import BindingSidePanel from "components/common/bindings/BindingSidePanel.svelte"
import { BindingHelpers } from "components/common/bindings/utils"
import { BindingHelpers, BindingType } from "components/common/bindings/utils"
import {
bindingsToCompletions,
hbAutocomplete,
@ -576,6 +576,7 @@
{
js: true,
dontDecode: true,
type: BindingType.RUNTIME,
}
)}
mode="javascript"

View file

@ -5,7 +5,7 @@
import { licensing } from "stores/portal"
import { isPremiumOrAbove } from "helpers/planTitle"
$: premiumOrAboveLicense = isPremiumOrAbove($licensing?.license.plan.type)
$: premiumOrAboveLicense = isPremiumOrAbove($licensing?.license?.plan?.type)
let show
let hide

View file

@ -1,6 +1,11 @@
import { decodeJSBinding } from "@budibase/string-templates"
import { hbInsert, jsInsert } from "components/common/CodeEditor"
export const BindingType = {
READABLE: "readableBinding",
RUNTIME: "runtimeBinding",
}
export class BindingHelpers {
constructor(getCaretPosition, insertAtPos, { disableWrapping } = {}) {
this.getCaretPosition = getCaretPosition
@ -25,16 +30,20 @@ export class BindingHelpers {
}
// Adds a data binding to the expression
onSelectBinding(value, binding, { js, dontDecode }) {
onSelectBinding(
value,
binding,
{ js, dontDecode, type = BindingType.READABLE }
) {
const { start, end } = this.getCaretPosition()
if (js) {
const jsVal = dontDecode ? value : decodeJSBinding(value)
const insertVal = jsInsert(jsVal, start, end, binding.readableBinding, {
const insertVal = jsInsert(jsVal, start, end, binding[type], {
disableWrapping: this.disableWrapping,
})
this.insertAtPos({ start, end, value: insertVal })
} else {
const insertVal = hbInsert(value, start, end, binding.readableBinding)
const insertVal = hbInsert(value, start, end, binding[type])
this.insertAtPos({ start, end, value: insertVal })
}
}

View file

@ -148,7 +148,7 @@ export const enrichedApps = derived([appsStore, auth], ([$store, $auth]) => {
deployed: app.status === AppStatus.DEPLOYED,
lockedYou: app.lockedBy && app.lockedBy.email === $auth.user?.email,
lockedOther: app.lockedBy && app.lockedBy.email !== $auth.user?.email,
favourite: $auth?.user.appFavourites?.includes(app.appId),
favourite: $auth.user?.appFavourites?.includes(app.appId),
}))
: []

View file

@ -9,6 +9,7 @@ import {
QueryType,
} from "@budibase/types"
import { db as dbCore } from "@budibase/backend-core"
import { HOST_ADDRESS } from "./utils"
interface CouchDBConfig {
url: string
@ -28,7 +29,7 @@ const SCHEMA: Integration = {
url: {
type: DatasourceFieldType.STRING,
required: true,
default: "http://localhost:5984",
default: `http://${HOST_ADDRESS}:5984`,
},
database: {
type: DatasourceFieldType.STRING,

View file

@ -8,6 +8,7 @@ import {
} from "@budibase/types"
import { Client, ClientOptions } from "@elastic/elasticsearch"
import { HOST_ADDRESS } from "./utils"
interface ElasticsearchConfig {
url: string
@ -29,7 +30,7 @@ const SCHEMA: Integration = {
url: {
type: DatasourceFieldType.STRING,
required: true,
default: "http://localhost:9200",
default: `http://${HOST_ADDRESS}:9200`,
},
ssl: {
type: DatasourceFieldType.BOOLEAN,

View file

@ -22,6 +22,7 @@ import {
finaliseExternalTables,
SqlClient,
checkExternalTables,
HOST_ADDRESS,
} from "./utils"
import Sql from "./base/sql"
import { MSSQLTablesResponse, MSSQLColumn } from "./base/types"
@ -88,7 +89,6 @@ const SCHEMA: Integration = {
user: {
type: DatasourceFieldType.STRING,
required: true,
default: "localhost",
},
password: {
type: DatasourceFieldType.PASSWORD,
@ -96,7 +96,7 @@ const SCHEMA: Integration = {
},
server: {
type: DatasourceFieldType.STRING,
default: "localhost",
default: HOST_ADDRESS,
},
port: {
type: DatasourceFieldType.NUMBER,

View file

@ -22,6 +22,7 @@ import {
InsertManyResult,
} from "mongodb"
import environment from "../environment"
import { HOST_ADDRESS } from "./utils"
export interface MongoDBConfig {
connectionString: string
@ -51,7 +52,7 @@ const getSchema = () => {
connectionString: {
type: DatasourceFieldType.STRING,
required: true,
default: "mongodb://localhost:27017",
default: `mongodb://${HOST_ADDRESS}:27017`,
display: "Connection string",
},
db: {

View file

@ -21,6 +21,7 @@ import {
generateColumnDefinition,
finaliseExternalTables,
checkExternalTables,
HOST_ADDRESS,
} from "./utils"
import dayjs from "dayjs"
import { NUMBER_REGEX } from "../utilities"
@ -49,7 +50,7 @@ const SCHEMA: Integration = {
datasource: {
host: {
type: DatasourceFieldType.STRING,
default: "localhost",
default: HOST_ADDRESS,
required: true,
},
port: {

View file

@ -22,6 +22,7 @@ import {
finaliseExternalTables,
getSqlQuery,
SqlClient,
HOST_ADDRESS,
} from "./utils"
import Sql from "./base/sql"
import {
@ -63,7 +64,7 @@ const SCHEMA: Integration = {
datasource: {
host: {
type: DatasourceFieldType.STRING,
default: "localhost",
default: HOST_ADDRESS,
required: true,
},
port: {

View file

@ -21,6 +21,7 @@ import {
finaliseExternalTables,
SqlClient,
checkExternalTables,
HOST_ADDRESS,
} from "./utils"
import Sql from "./base/sql"
import { PostgresColumn } from "./base/types"
@ -72,7 +73,7 @@ const SCHEMA: Integration = {
datasource: {
host: {
type: DatasourceFieldType.STRING,
default: "localhost",
default: HOST_ADDRESS,
required: true,
},
port: {

View file

@ -6,6 +6,7 @@ import {
QueryType,
} from "@budibase/types"
import Redis from "ioredis"
import { HOST_ADDRESS } from "./utils"
interface RedisConfig {
host: string
@ -28,7 +29,7 @@ const SCHEMA: Integration = {
host: {
type: DatasourceFieldType.STRING,
required: true,
default: "localhost",
default: HOST_ADDRESS,
},
port: {
type: DatasourceFieldType.NUMBER,

View file

@ -13,6 +13,7 @@ import {
DEFAULT_BB_DATASOURCE_ID,
} from "../constants"
import { helpers } from "@budibase/shared-core"
import env from "../environment"
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
const ROW_ID_REGEX = /^\[.*]$/g
@ -92,6 +93,14 @@ export enum SqlClient {
ORACLE = "oracledb",
}
const isCloud = env.isProd() && !env.SELF_HOSTED
const isSelfHost = env.isProd() && env.SELF_HOSTED
export const HOST_ADDRESS = isSelfHost
? "host.docker.internal"
: isCloud
? ""
: "localhost"
export function isExternalTableID(tableId: string) {
return tableId.includes(DocumentType.DATASOURCE)
}

View file

@ -68,6 +68,11 @@ export interface CreateAdminUserRequest {
ssoId?: string
}
export interface AddSSoUserRequest {
ssoId: string
email: string
}
export interface CreateAdminUserResponse {
_id: string
_rev: string

View file

@ -3,6 +3,7 @@ import env from "../../../environment"
import {
AcceptUserInviteRequest,
AcceptUserInviteResponse,
AddSSoUserRequest,
BulkUserRequest,
BulkUserResponse,
CloudAccount,
@ -15,6 +16,7 @@ import {
LockName,
LockType,
MigrationType,
PlatformUserByEmail,
SaveUserResponse,
SearchUsersRequest,
User,
@ -53,6 +55,25 @@ export const save = async (ctx: UserCtx<User, SaveUserResponse>) => {
}
}
export const addSsoSupport = async (ctx: Ctx<AddSSoUserRequest>) => {
const { email, ssoId } = ctx.request.body
try {
// Status is changed to 404 from getUserDoc if user is not found
let userByEmail = (await platform.users.getUserDoc(
email
)) as PlatformUserByEmail
await platform.users.addSsoUser(
ssoId,
email,
userByEmail.userId,
userByEmail.tenantId
)
ctx.status = 200
} catch (err: any) {
ctx.throw(err.status || 400, err)
}
}
const bulkDelete = async (userIds: string[], currentUserId: string) => {
if (userIds?.indexOf(currentUserId) !== -1) {
throw new Error("Unable to delete self.")
@ -208,7 +229,7 @@ export const search = async (ctx: Ctx<SearchUsersRequest>) => {
}
// Validate we aren't trying to search on any illegal fields
if (!userSdk.core.isSupportedUserSearch(body.query)) {
ctx.throw(400, "Can only search by string.email or equal._id")
ctx.throw(400, "Can only search by string.email, equal._id or oneOf._id")
}
}

View file

@ -41,6 +41,10 @@ const PUBLIC_ENDPOINTS = [
route: "/api/global/users/init",
method: "POST",
},
{
route: "/api/global/users/sso",
method: "POST",
},
{
route: "/api/global/users/invite/accept",
method: "POST",
@ -81,6 +85,11 @@ const NO_TENANCY_ENDPOINTS = [
route: "/api/global/users/init",
method: "POST",
},
// tenant is retrieved from the user found by the requested email
{
route: "/api/global/users/sso",
method: "POST",
},
// deprecated single tenant sso callback
{
route: "/api/admin/auth/google/callback",

View file

@ -520,10 +520,51 @@ describe("/api/global/users", () => {
})
}
function createPasswordUser() {
return config.doInTenant(() => {
const user = structures.users.user()
return userSdk.db.save(user)
})
}
it("should be able to update an sso user that has no password", async () => {
const user = await createSSOUser()
await config.api.users.saveUser(user)
})
it("sso support couldn't be used by admin. It is cloud restricted and needs internal key", async () => {
const user = await config.createUser()
const ssoId = "fake-ssoId"
await config.api.users
.addSsoSupportDefaultAuth(ssoId, user.email)
.expect("Content-Type", /json/)
.expect(403)
})
it("if user email doesn't exist, SSO support couldn't be added. Not found error returned", async () => {
const ssoId = "fake-ssoId"
const email = "fake-email@budibase.com"
await config.api.users
.addSsoSupportInternalAPIAuth(ssoId, email)
.expect("Content-Type", /json/)
.expect(404)
})
it("if user email exist, SSO support is added", async () => {
const user = await createPasswordUser()
const ssoId = "fakessoId"
await config.api.users
.addSsoSupportInternalAPIAuth(ssoId, user.email)
.expect(200)
})
it("if user ssoId is already assigned, no change will be applied", async () => {
const user = await createSSOUser()
user.ssoId = "testssoId"
await config.api.users
.addSsoSupportInternalAPIAuth(user.ssoId, user.email)
.expect(200)
})
})
})
@ -608,6 +649,24 @@ describe("/api/global/users", () => {
expect(response.body.data[0]._id).toBe(user._id)
})
it("should be able to search by oneOf _id", async () => {
const [user, user2, user3] = await Promise.all([
config.createUser(),
config.createUser(),
config.createUser(),
])
const response = await config.api.users.searchUsers({
query: { oneOf: { _id: [user._id, user2._id] } },
})
expect(response.body.data.length).toBe(2)
const foundUserIds = response.body.data.map((user: User) => user._id)
expect(foundUserIds).toContain(user._id)
expect(foundUserIds).toContain(user2._id)
expect(
response.body.data.find((user: User) => user._id === user3._id)
).toBeUndefined()
})
it("should be able to search by _id with numeric prefixing", async () => {
const user = await config.createUser()
const response = await config.api.users.searchUsers({

View file

@ -65,6 +65,12 @@ router
users.buildUserSaveValidation(),
controller.save
)
.post(
"/api/global/users/sso",
cloudRestricted,
users.buildAddSsoSupport(),
controller.addSsoSupport
)
.post(
"/api/global/users/bulk",
auth.adminOnly,

View file

@ -41,6 +41,15 @@ export const buildUserSaveValidation = () => {
return auth.joiValidator.body(Joi.object(schema).required().unknown(true))
}
export const buildAddSsoSupport = () => {
return auth.joiValidator.body(
Joi.object({
ssoId: Joi.string().required(),
email: Joi.string().required(),
}).required()
)
}
export const buildUserBulkUserValidation = (isSelf = false) => {
if (!isSelf) {
schema = {

View file

@ -127,6 +127,20 @@ export class UserAPI extends TestAPI {
.expect(status ? status : 200)
}
addSsoSupportInternalAPIAuth = (ssoId: string, email: string) => {
return this.request
.post(`/api/global/users/sso`)
.send({ ssoId, email })
.set(this.config.internalAPIHeaders())
}
addSsoSupportDefaultAuth = (ssoId: string, email: string) => {
return this.request
.post(`/api/global/users/sso`)
.send({ ssoId, email })
.set(this.config.defaultHeaders())
}
deleteUser = (userId: string, status?: number) => {
return this.request
.delete(`/api/global/users/${userId}`)

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!
)
})
})

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