mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
80 changed files with 2970 additions and 476 deletions
@ -1,190 +1,186 @@ |
|||
<script> |
|||
import groupBy from "lodash/fp/groupBy" |
|||
import { Input, TextArea, Heading, Spacer, Label } from "@budibase/bbui" |
|||
import { createEventDispatcher } from "svelte" |
|||
import { isValid } from "@budibase/string-templates" |
|||
import { handlebarsCompletions } from "constants/completions" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
export let value = "" |
|||
export let bindingDrawer |
|||
export let bindableProperties = [] |
|||
|
|||
let originalValue = value |
|||
let helpers = handlebarsCompletions() |
|||
let getCaretPosition |
|||
let search = "" |
|||
let validity = true |
|||
|
|||
$: categories = Object.entries(groupBy("category", bindableProperties)) |
|||
$: value && checkValid() |
|||
$: dispatch("update", value) |
|||
$: searchRgx = new RegExp(search, "ig") |
|||
|
|||
function checkValid() { |
|||
validity = isValid(value) |
|||
} |
|||
|
|||
function addToText(binding) { |
|||
const position = getCaretPosition() |
|||
const toAdd = `{{ ${binding.path} }}` |
|||
if (position.start) { |
|||
value = |
|||
value.substring(0, position.start) + |
|||
toAdd + |
|||
value.substring(position.end, value.length) |
|||
} else { |
|||
value += toAdd |
|||
} |
|||
} |
|||
export function cancel() { |
|||
dispatch("update", originalValue) |
|||
bindingDrawer.close() |
|||
} |
|||
import groupBy from "lodash/fp/groupBy" |
|||
import { Input, TextArea, Heading, Spacer, Label } from "@budibase/bbui" |
|||
import { createEventDispatcher } from "svelte" |
|||
import { isValid } from "@budibase/string-templates" |
|||
import { handlebarsCompletions } from "constants/completions" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
export let value = "" |
|||
export let bindingDrawer |
|||
export let bindableProperties = [] |
|||
|
|||
let originalValue = value |
|||
let helpers = handlebarsCompletions() |
|||
let getCaretPosition |
|||
let search = "" |
|||
let validity = true |
|||
|
|||
$: categories = Object.entries(groupBy("category", bindableProperties)) |
|||
$: value && checkValid() |
|||
$: dispatch("update", value) |
|||
$: searchRgx = new RegExp(search, "ig") |
|||
|
|||
function checkValid() { |
|||
validity = isValid(value) |
|||
} |
|||
|
|||
function addToText(binding) { |
|||
const position = getCaretPosition() |
|||
const toAdd = `{{ ${binding.path} }}` |
|||
if (position.start) { |
|||
value = |
|||
value.substring(0, position.start) + |
|||
toAdd + |
|||
value.substring(position.end, value.length) |
|||
} else { |
|||
value += toAdd |
|||
} |
|||
} |
|||
export function cancel() { |
|||
dispatch("update", originalValue) |
|||
bindingDrawer.close() |
|||
} |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div class="list"> |
|||
<Heading small>Available bindings</Heading> |
|||
<Spacer medium /> |
|||
<Input extraThin placeholder="Search" bind:value={search} /> |
|||
<Spacer medium /> |
|||
{#each categories as [categoryName, bindings]} |
|||
<Heading extraSmall>{categoryName}</Heading> |
|||
<Spacer extraSmall /> |
|||
{#each bindableProperties.filter(binding => |
|||
binding.label.match(searchRgx) |
|||
) as binding} |
|||
<div class="binding" on:click={() => addToText(binding)}> |
|||
<span class="binding__label">{binding.label}</span> |
|||
<span class="binding__type">{binding.type}</span> |
|||
<br /> |
|||
<div class="binding__description"> |
|||
{binding.description || ''} |
|||
</div> |
|||
</div> |
|||
{/each} |
|||
{/each} |
|||
<Heading extraSmall>Helpers</Heading> |
|||
<Spacer extraSmall /> |
|||
{#each helpers.filter(helper => helper.label.match(searchRgx) || helper.description.match(searchRgx)) as helper} |
|||
<div class="binding" on:click={() => addToText(helper)}> |
|||
<span class="binding__label">{helper.label}</span> |
|||
<br /> |
|||
<div class="binding__description"> |
|||
{@html helper.description || ''} |
|||
</div> |
|||
<pre>{helper.example || ''}</pre> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
<div class="text"> |
|||
<TextArea |
|||
bind:getCaretPosition |
|||
thin |
|||
bind:value |
|||
placeholder="Add text, or click the objects on the left to add them to the textbox." /> |
|||
{#if !validity} |
|||
<p class="syntax-error"> |
|||
Current Handlebars syntax is invalid, please check the guide |
|||
<a href="https://handlebarsjs.com/guide/">here</a> |
|||
for more details. |
|||
</p> |
|||
{/if} |
|||
</div> |
|||
<div class="list"> |
|||
<Heading small>Available bindings</Heading> |
|||
<Spacer medium /> |
|||
<Input extraThin placeholder="Search" bind:value={search} /> |
|||
<Spacer medium /> |
|||
{#each categories as [categoryName, bindings]} |
|||
<Heading extraSmall>{categoryName}</Heading> |
|||
<Spacer extraSmall /> |
|||
{#each bindableProperties.filter(binding => |
|||
binding.label.match(searchRgx) |
|||
) as binding} |
|||
<div class="binding" on:click={() => addToText(binding)}> |
|||
<span class="binding__label">{binding.label}</span> |
|||
<span class="binding__type">{binding.type}</span> |
|||
<br /> |
|||
<div class="binding__description">{binding.description || ''}</div> |
|||
</div> |
|||
{/each} |
|||
{/each} |
|||
<Heading extraSmall>Helpers</Heading> |
|||
<Spacer extraSmall /> |
|||
{#each helpers.filter(helper => helper.label.match(searchRgx) || helper.description.match(searchRgx)) as helper} |
|||
<div class="binding" on:click={() => addToText(helper)}> |
|||
<span class="binding__label">{helper.label}</span> |
|||
<br /> |
|||
<div class="binding__description"> |
|||
{@html helper.description || ''} |
|||
</div> |
|||
<pre>{helper.example || ''}</pre> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
<div class="text"> |
|||
<TextArea |
|||
bind:getCaretPosition |
|||
thin |
|||
bind:value |
|||
placeholder="Add text, or click the objects on the left to add them to the textbox." /> |
|||
{#if !validity} |
|||
<p class="syntax-error"> |
|||
Current Handlebars syntax is invalid, please check the guide |
|||
<a href="https://handlebarsjs.com/guide/">here</a> |
|||
for more details. |
|||
</p> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
|
|||
|
|||
<style> |
|||
.container { |
|||
height: 40vh; |
|||
overflow-y: auto; |
|||
display: grid; |
|||
grid-template-columns: 280px 1fr; |
|||
} |
|||
|
|||
.list { |
|||
border-right: var(--border-light); |
|||
padding: var(--spacing-l); |
|||
overflow: auto; |
|||
} |
|||
.list { |
|||
border-right: var(--border-light); |
|||
padding: var(--spacing-l); |
|||
overflow: auto; |
|||
} |
|||
.container { |
|||
height: 40vh; |
|||
overflow-y: auto; |
|||
display: grid; |
|||
grid-template-columns: 280px 1fr; |
|||
} |
|||
|
|||
.list::-webkit-scrollbar { |
|||
display: none; |
|||
} |
|||
.list { |
|||
border-right: var(--border-light); |
|||
padding: var(--spacing-l); |
|||
overflow: auto; |
|||
} |
|||
.list { |
|||
border-right: var(--border-light); |
|||
padding: var(--spacing-l); |
|||
overflow: auto; |
|||
} |
|||
|
|||
.text { |
|||
padding: var(--spacing-l); |
|||
font-family: var(--font-sans); |
|||
} |
|||
.text :global(textarea) { |
|||
min-height: 100px; |
|||
} |
|||
.text :global(p) { |
|||
margin: 0; |
|||
} |
|||
|
|||
.binding { |
|||
font-size: 12px; |
|||
padding: var(--spacing-s); |
|||
border-radius: var(--border-radius-m); |
|||
} |
|||
.binding:hover { |
|||
background-color: var(--grey-2); |
|||
cursor: pointer; |
|||
} |
|||
.binding__label { |
|||
font-weight: 500; |
|||
text-transform: capitalize; |
|||
} |
|||
.binding__description { |
|||
color: var(--grey-8); |
|||
margin-top: 2px; |
|||
white-space: normal; |
|||
} |
|||
|
|||
pre { |
|||
white-space: normal; |
|||
} |
|||
|
|||
.binding__type { |
|||
font-family: monospace; |
|||
background-color: var(--grey-2); |
|||
border-radius: var(--border-radius-m); |
|||
padding: 2px; |
|||
margin-left: 2px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.editor { |
|||
padding-left: var(--spacing-l); |
|||
} |
|||
.editor :global(textarea) { |
|||
min-height: 60px; |
|||
} |
|||
|
|||
.controls { |
|||
display: grid; |
|||
grid-template-columns: 1fr auto; |
|||
grid-gap: var(--spacing-l); |
|||
align-items: center; |
|||
margin-top: var(--spacing-m); |
|||
} |
|||
|
|||
.syntax-error { |
|||
color: var(--red); |
|||
font-size: 12px; |
|||
} |
|||
|
|||
.syntax-error a { |
|||
color: var(--red); |
|||
text-decoration: underline; |
|||
} |
|||
</style> |
|||
|
|||
.list::-webkit-scrollbar { |
|||
display: none; |
|||
} |
|||
|
|||
.text { |
|||
padding: var(--spacing-l); |
|||
font-family: var(--font-sans); |
|||
} |
|||
.text :global(textarea) { |
|||
min-height: 100px; |
|||
} |
|||
.text :global(p) { |
|||
margin: 0; |
|||
} |
|||
|
|||
.binding { |
|||
font-size: 12px; |
|||
padding: var(--spacing-s); |
|||
border-radius: var(--border-radius-m); |
|||
} |
|||
.binding:hover { |
|||
background-color: var(--grey-2); |
|||
cursor: pointer; |
|||
} |
|||
.binding__label { |
|||
font-weight: 500; |
|||
text-transform: capitalize; |
|||
} |
|||
.binding__description { |
|||
color: var(--grey-8); |
|||
margin-top: 2px; |
|||
white-space: normal; |
|||
} |
|||
|
|||
pre { |
|||
white-space: normal; |
|||
} |
|||
|
|||
.binding__type { |
|||
font-family: monospace; |
|||
background-color: var(--grey-2); |
|||
border-radius: var(--border-radius-m); |
|||
padding: 2px; |
|||
margin-left: 2px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.editor { |
|||
padding-left: var(--spacing-l); |
|||
} |
|||
.editor :global(textarea) { |
|||
min-height: 60px; |
|||
} |
|||
|
|||
.controls { |
|||
display: grid; |
|||
grid-template-columns: 1fr auto; |
|||
grid-gap: var(--spacing-l); |
|||
align-items: center; |
|||
margin-top: var(--spacing-m); |
|||
} |
|||
|
|||
.syntax-error { |
|||
color: var(--red); |
|||
font-size: 12px; |
|||
} |
|||
|
|||
.syntax-error a { |
|||
color: var(--red); |
|||
text-decoration: underline; |
|||
} |
|||
</style> |
|||
|
|||
@ -0,0 +1,17 @@ |
|||
const fetch = jest.requireActual("node-fetch") |
|||
|
|||
module.exports = async (url, opts) => { |
|||
// mocked data based on url
|
|||
if (url.includes("api/apps")) { |
|||
return { |
|||
json: async () => { |
|||
return { |
|||
app1: { |
|||
url: "/app1", |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
} |
|||
return fetch(url, opts) |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
const pg = {} |
|||
|
|||
// constructor
|
|||
function Client() {} |
|||
|
|||
Client.prototype.query = async function() { |
|||
return { |
|||
rows: [ |
|||
{ |
|||
a: "string", |
|||
b: 1, |
|||
}, |
|||
], |
|||
} |
|||
} |
|||
|
|||
Client.prototype.connect = async function() {} |
|||
|
|||
pg.Client = Client |
|||
|
|||
module.exports = pg |
|||
@ -1,5 +1,7 @@ |
|||
const env = require("../../environment") |
|||
|
|||
exports.isEnabled = async function(ctx) { |
|||
ctx.body = JSON.stringify(env.ENABLE_ANALYTICS === "true") |
|||
ctx.body = { |
|||
enabled: env.ENABLE_ANALYTICS === "true", |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,59 @@ |
|||
const setup = require("./utilities") |
|||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") |
|||
const { budibaseAppsDir } = require("../../../utilities/budibaseDir") |
|||
const fs = require("fs") |
|||
const path = require("path") |
|||
|
|||
describe("/api/keys", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
}) |
|||
|
|||
describe("fetch", () => { |
|||
it("should allow fetching", async () => { |
|||
const res = await request |
|||
.get(`/api/keys`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body).toBeDefined() |
|||
}) |
|||
|
|||
it("should check authorization for builder", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "GET", |
|||
url: `/api/keys`, |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("update", () => { |
|||
it("should allow updating a value", async () => { |
|||
fs.writeFileSync(path.join(budibaseAppsDir(), ".env"), "TEST_API_KEY=thing") |
|||
const res = await request |
|||
.put(`/api/keys/TEST`) |
|||
.send({ |
|||
value: "test" |
|||
}) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body["TEST"]).toEqual("test") |
|||
expect(process.env.TEST_API_KEY).toEqual("test") |
|||
}) |
|||
|
|||
it("should check authorization for builder", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "PUT", |
|||
url: `/api/keys/TEST`, |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,106 @@ |
|||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") |
|||
const setup = require("./utilities") |
|||
|
|||
describe("/authenticate", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
}) |
|||
|
|||
describe("authenticate", () => { |
|||
it("should be able to create a layout", async () => { |
|||
await config.createUser("test@test.com", "p4ssw0rd") |
|||
const res = await request |
|||
.post(`/api/authenticate`) |
|||
.send({ |
|||
email: "test@test.com", |
|||
password: "p4ssw0rd", |
|||
}) |
|||
.set(config.publicHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.token).toBeDefined() |
|||
expect(res.body.email).toEqual("test@test.com") |
|||
expect(res.body.password).toBeUndefined() |
|||
}) |
|||
|
|||
it("should error if no app specified", async () => { |
|||
await request |
|||
.post(`/api/authenticate`) |
|||
.expect(400) |
|||
}) |
|||
|
|||
it("should error if no email specified", async () => { |
|||
await request |
|||
.post(`/api/authenticate`) |
|||
.send({ |
|||
password: "test", |
|||
}) |
|||
.set(config.publicHeaders()) |
|||
.expect(400) |
|||
}) |
|||
|
|||
it("should error if no password specified", async () => { |
|||
await request |
|||
.post(`/api/authenticate`) |
|||
.send({ |
|||
email: "test", |
|||
}) |
|||
.set(config.publicHeaders()) |
|||
.expect(400) |
|||
}) |
|||
|
|||
it("should error if invalid user specified", async () => { |
|||
await request |
|||
.post(`/api/authenticate`) |
|||
.send({ |
|||
email: "test", |
|||
password: "test", |
|||
}) |
|||
.set(config.publicHeaders()) |
|||
.expect(401) |
|||
}) |
|||
|
|||
it("should throw same error if wrong password specified", async () => { |
|||
await config.createUser("test@test.com", "password") |
|||
await request |
|||
.post(`/api/authenticate`) |
|||
.send({ |
|||
email: "test@test.com", |
|||
password: "test", |
|||
}) |
|||
.set(config.publicHeaders()) |
|||
.expect(401) |
|||
}) |
|||
|
|||
it("should throw an error for inactive users", async () => { |
|||
await config.createUser("test@test.com", "password") |
|||
await config.makeUserInactive("test@test.com") |
|||
await request |
|||
.post(`/api/authenticate`) |
|||
.send({ |
|||
email: "test@test.com", |
|||
password: "password", |
|||
}) |
|||
.set(config.publicHeaders()) |
|||
.expect(401) |
|||
}) |
|||
}) |
|||
|
|||
describe("fetch self", () => { |
|||
it("should be able to delete the layout", async () => { |
|||
await config.createUser("test@test.com", "p4ssw0rd") |
|||
const headers = await config.login("test@test.com", "p4ssw0rd") |
|||
const res = await request |
|||
.get(`/api/self`) |
|||
.set(headers) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.email).toEqual("test@test.com") |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,32 @@ |
|||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") |
|||
const setup = require("./utilities") |
|||
|
|||
describe("/backups", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
}) |
|||
|
|||
describe("exportAppDump", () => { |
|||
it("should be able to export app", async () => { |
|||
const res = await request |
|||
.get(`/api/backups/export?appId=${config.getAppId()}`) |
|||
.set(config.defaultHeaders()) |
|||
.expect(200) |
|||
expect(res.text).toBeDefined() |
|||
expect(res.text.includes(`"db_name":"${config.getAppId()}"`)).toEqual(true) |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "GET", |
|||
url: `/api/backups/export?appId=${config.getAppId()}`, |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,16 @@ |
|||
const setup = require("./utilities") |
|||
|
|||
describe("test things in the Cloud/Self hosted", () => { |
|||
describe("test self hosted static page", () => { |
|||
it("should be able to load the static page", async () => { |
|||
await setup.switchToCloudForFunction(async () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
await config.init() |
|||
const res = await request.get(`/`).expect(200) |
|||
expect(res.text.includes("<title>Budibase self hosting️</title>")).toEqual(true) |
|||
setup.afterAll() |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,49 @@ |
|||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") |
|||
const setup = require("./utilities") |
|||
const fs = require("fs") |
|||
const { resolve, join } = require("path") |
|||
const { budibaseAppsDir } = require("../../../utilities/budibaseDir") |
|||
|
|||
describe("/component", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
}) |
|||
|
|||
function mock() { |
|||
const manifestFile = "manifest.json" |
|||
const appId = config.getAppId() |
|||
const libraries = ["@budibase/standard-components"] |
|||
for (let library of libraries) { |
|||
let appDirectory = resolve(budibaseAppsDir(), appId, "node_modules", library, "package") |
|||
fs.mkdirSync(appDirectory, { recursive: true }) |
|||
const file = require.resolve(library).split("dist/index.js")[0] + manifestFile |
|||
fs.copyFileSync(file, join(appDirectory, manifestFile)) |
|||
} |
|||
} |
|||
|
|||
describe("fetch definitions", () => { |
|||
it("should be able to fetch definitions", async () => { |
|||
// have to "mock" the files required
|
|||
mock() |
|||
const res = await request |
|||
.get(`/${config.getAppId()}/components/definitions`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body["@budibase/standard-components/container"]).toBeDefined() |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "GET", |
|||
url: `/${config.getAppId()}/components/definitions`, |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,130 @@ |
|||
// mock out node fetch for this
|
|||
jest.mock("node-fetch") |
|||
|
|||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") |
|||
const setup = require("./utilities") |
|||
|
|||
describe("/hosting", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
let app |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
beforeEach(async () => { |
|||
app = await config.init() |
|||
}) |
|||
|
|||
describe("fetchInfo", () => { |
|||
it("should be able to fetch hosting information", async () => { |
|||
const res = await request |
|||
.get(`/api/hosting/info`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body).toEqual({ types: ["cloud", "self"]}) |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "GET", |
|||
url: `/api/hosting/info`, |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("fetchUrls", () => { |
|||
it("should be able to fetch current app URLs", async () => { |
|||
const res = await request |
|||
.get(`/api/hosting/urls`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.app).toEqual(`https://${config.getAppId()}.app.budi.live`) |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "GET", |
|||
url: `/api/hosting/urls`, |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("fetch", () => { |
|||
it("should be able to fetch the current hosting information", async () => { |
|||
const res = await request |
|||
.get(`/api/hosting`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body._id).toBeDefined() |
|||
expect(res.body.hostingUrl).toBeDefined() |
|||
expect(res.body.type).toEqual("cloud") |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "GET", |
|||
url: `/api/hosting`, |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("save", () => { |
|||
it("should be able to update the hosting information", async () => { |
|||
const res = await request |
|||
.post(`/api/hosting`) |
|||
.send({ |
|||
type: "self", |
|||
selfHostKey: "budibase", |
|||
hostingUrl: "localhost:10000", |
|||
useHttps: false, |
|||
}) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.ok).toEqual(true) |
|||
// make sure URL updated
|
|||
const urlRes = await request |
|||
.get(`/api/hosting/urls`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(urlRes.body.app).toEqual(`http://localhost:10000/app`) |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "POST", |
|||
url: `/api/hosting`, |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("getDeployedApps", () => { |
|||
it("should get apps when in builder", async () => { |
|||
const res = await request |
|||
.get(`/api/hosting/apps`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.app1).toEqual({url: "/app1"}) |
|||
}) |
|||
|
|||
it("should get apps when in cloud", async () => { |
|||
await setup.switchToCloudForFunction(async () => { |
|||
const res = await request |
|||
.get(`/api/hosting/apps`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.app1).toEqual({url: "/app1"}) |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,52 @@ |
|||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") |
|||
const setup = require("./utilities") |
|||
|
|||
describe("/integrations", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
}) |
|||
|
|||
describe("fetch", () => { |
|||
it("should be able to get all integration definitions", async () => { |
|||
const res = await request |
|||
.get(`/api/integrations`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.POSTGRES).toBeDefined() |
|||
expect(res.body.POSTGRES.friendlyName).toEqual("PostgreSQL") |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "GET", |
|||
url: `/api/integrations`, |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("find", () => { |
|||
it("should be able to get postgres definition", async () => { |
|||
const res = await request |
|||
.get(`/api/integrations/POSTGRES`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.friendlyName).toEqual("PostgreSQL") |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "GET", |
|||
url: `/api/integrations/POSTGRES`, |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,55 @@ |
|||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") |
|||
const setup = require("./utilities") |
|||
const { basicLayout } = require("./utilities/structures") |
|||
|
|||
describe("/layouts", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
let layout |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
layout = await config.createLayout() |
|||
}) |
|||
|
|||
describe("save", () => { |
|||
it("should be able to create a layout", async () => { |
|||
const res = await request |
|||
.post(`/api/layouts`) |
|||
.send(basicLayout()) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body._rev).toBeDefined() |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "POST", |
|||
url: `/api/layouts`, |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("destroy", () => { |
|||
it("should be able to delete the layout", async () => { |
|||
const res = await request |
|||
.delete(`/api/layouts/${layout._id}/${layout._rev}`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.message).toBeDefined() |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "DELETE", |
|||
url: `/api/layouts/${layout._id}/${layout._rev}`, |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,38 @@ |
|||
const setup = require("./utilities") |
|||
|
|||
describe("/analytics", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
}) |
|||
|
|||
describe("isEnabled", () => { |
|||
it("check if analytics enabled", async () => { |
|||
const res = await request |
|||
.get(`/api/analytics`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(typeof res.body.enabled).toEqual("boolean") |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("/health", () => { |
|||
it("should confirm healthy", async () => { |
|||
let config = setup.getConfig() |
|||
await config.getRequest().get("/health").expect(200) |
|||
}) |
|||
}) |
|||
|
|||
describe("/version", () => { |
|||
it("should confirm version", async () => { |
|||
const config = setup.getConfig() |
|||
const res = await config.getRequest().get("/version").expect(200) |
|||
expect(res.text.split(".").length).toEqual(3) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,84 @@ |
|||
const setup = require("./utilities") |
|||
const { basicScreen } = require("./utilities/structures") |
|||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") |
|||
const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles") |
|||
|
|||
const route = "/test" |
|||
|
|||
describe("/routing", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
let screen, screen2 |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
screen = basicScreen() |
|||
screen.routing.route = route |
|||
screen = await config.createScreen(screen) |
|||
screen2 = basicScreen() |
|||
screen2.routing.roleId = BUILTIN_ROLE_IDS.POWER |
|||
screen2.routing.route = route |
|||
screen2 = await config.createScreen(screen2) |
|||
}) |
|||
|
|||
describe("fetch", () => { |
|||
it("returns the correct routing for basic user", async () => { |
|||
const res = await request |
|||
.get(`/api/routing/client`) |
|||
.set(await config.roleHeaders("basic@test.com", BUILTIN_ROLE_IDS.BASIC)) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.routes).toBeDefined() |
|||
expect(res.body.routes[route]).toEqual({ |
|||
subpaths: { |
|||
[route]: { |
|||
screenId: screen._id, |
|||
roleId: screen.routing.roleId |
|||
} |
|||
} |
|||
}) |
|||
}) |
|||
|
|||
it("returns the correct routing for power user", async () => { |
|||
const res = await request |
|||
.get(`/api/routing/client`) |
|||
.set(await config.roleHeaders("basic@test.com", BUILTIN_ROLE_IDS.POWER)) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.routes).toBeDefined() |
|||
expect(res.body.routes[route]).toEqual({ |
|||
subpaths: { |
|||
[route]: { |
|||
screenId: screen2._id, |
|||
roleId: screen2.routing.roleId |
|||
} |
|||
} |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("fetch all", () => { |
|||
it("should fetch all routes for builder", async () => { |
|||
const res = await request |
|||
.get(`/api/routing`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.routes).toBeDefined() |
|||
expect(res.body.routes[route].subpaths[route]).toBeDefined() |
|||
const subpath = res.body.routes[route].subpaths[route] |
|||
expect(subpath.screens[screen2.routing.roleId]).toEqual(screen2._id) |
|||
expect(subpath.screens[screen.routing.roleId]).toEqual(screen._id) |
|||
}) |
|||
|
|||
it("make sure it is a builder only endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "GET", |
|||
url: `/api/routing`, |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,77 @@ |
|||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") |
|||
const setup = require("./utilities") |
|||
const { basicScreen } = require("./utilities/structures") |
|||
|
|||
describe("/screens", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
let screen |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
screen = await config.createScreen() |
|||
}) |
|||
|
|||
describe("fetch", () => { |
|||
it("should be able to create a layout", async () => { |
|||
const res = await request |
|||
.get(`/api/screens`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.length).toEqual(3) |
|||
expect(res.body.some(s => s._id === screen._id)).toEqual(true) |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "GET", |
|||
url: `/api/screens`, |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("save", () => { |
|||
it("should be able to save a screen", async () => { |
|||
const screenCfg = basicScreen() |
|||
const res = await request |
|||
.post(`/api/screens`) |
|||
.send(screenCfg) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body._rev).toBeDefined() |
|||
expect(res.body.name).toEqual(screenCfg.name) |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "POST", |
|||
url: `/api/screens`, |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("destroy", () => { |
|||
it("should be able to delete the screen", async () => { |
|||
const res = await request |
|||
.delete(`/api/screens/${screen._id}/${screen._rev}`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.message).toBeDefined() |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "DELETE", |
|||
url: `/api/screens/${screen._id}/${screen._rev}`, |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,49 @@ |
|||
const setup = require("./utilities") |
|||
const { budibaseAppsDir } = require("../../../utilities/budibaseDir") |
|||
const fs = require("fs") |
|||
const { join } = require("path") |
|||
|
|||
describe("/templates", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
}) |
|||
|
|||
describe("fetch", () => { |
|||
it("should be able to fetch templates", async () => { |
|||
const res = await request |
|||
.get(`/api/templates`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
// this test is quite light right now, templates aren't heavily utilised yet
|
|||
expect(Array.isArray(res.body)).toEqual(true) |
|||
}) |
|||
}) |
|||
|
|||
describe("export", () => { |
|||
it("should be able to export the basic app", async () => { |
|||
const res = await request |
|||
.post(`/api/templates`) |
|||
.send({ |
|||
templateName: "test", |
|||
}) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.message).toEqual("Created template: test") |
|||
const dir = join( |
|||
budibaseAppsDir(), |
|||
"templates", |
|||
"app", |
|||
"test", |
|||
"db" |
|||
) |
|||
expect(fs.existsSync(dir)).toEqual(true) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,130 @@ |
|||
const setup = require("./utilities") |
|||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") |
|||
const { basicWebhook, basicAutomation } = require("./utilities/structures") |
|||
|
|||
describe("/webhooks", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
let webhook |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
beforeEach(async () => { |
|||
await config.init() |
|||
const autoConfig = basicAutomation() |
|||
autoConfig.definition.trigger = { |
|||
schema: { outputs: { properties: {} } }, |
|||
inputs: {}, |
|||
} |
|||
await config.createAutomation(autoConfig) |
|||
webhook = await config.createWebhook() |
|||
}) |
|||
|
|||
describe("create", () => { |
|||
it("should create a webhook successfully", async () => { |
|||
const automation = await config.createAutomation() |
|||
const res = await request |
|||
.put(`/api/webhooks`) |
|||
.send(basicWebhook(automation._id)) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.webhook).toBeDefined() |
|||
expect(typeof res.body.webhook._id).toEqual("string") |
|||
expect(typeof res.body.webhook._rev).toEqual("string") |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "PUT", |
|||
url: `/api/webhooks`, |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("fetch", () => { |
|||
it("returns the correct routing for basic user", async () => { |
|||
const res = await request |
|||
.get(`/api/webhooks`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(Array.isArray(res.body)).toEqual(true) |
|||
expect(res.body[0]._id).toEqual(webhook._id) |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "GET", |
|||
url: `/api/webhooks`, |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("delete", () => { |
|||
it("should successfully delete", async () => { |
|||
const res = await request |
|||
.delete(`/api/webhooks/${webhook._id}/${webhook._rev}`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body).toBeDefined() |
|||
expect(res.body.ok).toEqual(true) |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "DELETE", |
|||
url: `/api/webhooks/${webhook._id}/${webhook._rev}`, |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("build schema", () => { |
|||
it("should allow building a schema", async () => { |
|||
const res = await request |
|||
.post(`/api/webhooks/schema/${config.getAppId()}/${webhook._id}`) |
|||
.send({ |
|||
a: 1 |
|||
}) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body).toBeDefined() |
|||
// fetch to see if the schema has been updated
|
|||
const fetch = await request |
|||
.get(`/api/webhooks`) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(fetch.body[0]).toBeDefined() |
|||
expect(fetch.body[0].bodySchema).toEqual({ |
|||
properties: { |
|||
a: { type: "integer" } |
|||
}, |
|||
type: "object", |
|||
}) |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await checkBuilderEndpoint({ |
|||
config, |
|||
method: "POST", |
|||
url: `/api/webhooks/schema/${config.getAppId()}/${webhook._id}`, |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("trigger", () => { |
|||
it("should allow triggering from public", async () => { |
|||
const res = await request |
|||
.post(`/api/webhooks/trigger/${config.getAppId()}/${webhook._id}`) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.message).toBeDefined() |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,28 @@ |
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
|||
|
|||
exports[`Authenticated middleware sets the correct APP auth type information when the user is not in the builder 1`] = ` |
|||
Object { |
|||
"apiKey": "1234", |
|||
"appId": "budibase:app:local", |
|||
"role": Role { |
|||
"_id": "ADMIN", |
|||
"inherits": "POWER", |
|||
"name": "Admin", |
|||
"permissionId": "admin", |
|||
}, |
|||
"roleId": "ADMIN", |
|||
} |
|||
`; |
|||
|
|||
exports[`Authenticated middleware sets the correct BUILDER auth type information when the x-budibase-type header is not 'client' 1`] = ` |
|||
Object { |
|||
"apiKey": "1234", |
|||
"appId": "budibase:builder:local", |
|||
"role": Role { |
|||
"_id": "BUILDER", |
|||
"name": "Builder", |
|||
"permissionId": "admin", |
|||
}, |
|||
"roleId": "BUILDER", |
|||
} |
|||
`; |
|||
@ -0,0 +1,125 @@ |
|||
const { AuthTypes } = require("../../constants") |
|||
const authenticatedMiddleware = require("../authenticated") |
|||
const jwt = require("jsonwebtoken") |
|||
jest.mock("jsonwebtoken") |
|||
|
|||
class TestConfiguration { |
|||
constructor(middleware) { |
|||
this.middleware = authenticatedMiddleware |
|||
this.ctx = { |
|||
config: {}, |
|||
auth: {}, |
|||
cookies: { |
|||
set: jest.fn(), |
|||
get: jest.fn() |
|||
}, |
|||
headers: {}, |
|||
params: {}, |
|||
path: "", |
|||
request: { |
|||
headers: {} |
|||
}, |
|||
throw: jest.fn() |
|||
} |
|||
this.next = jest.fn() |
|||
} |
|||
|
|||
setHeaders(headers) { |
|||
this.ctx.headers = headers |
|||
} |
|||
|
|||
executeMiddleware() { |
|||
return this.middleware(this.ctx, this.next) |
|||
} |
|||
|
|||
afterEach() { |
|||
jest.resetAllMocks() |
|||
} |
|||
} |
|||
|
|||
describe("Authenticated middleware", () => { |
|||
let config |
|||
|
|||
beforeEach(() => { |
|||
config = new TestConfiguration() |
|||
}) |
|||
|
|||
afterEach(() => { |
|||
config.afterEach() |
|||
}) |
|||
|
|||
it("calls next() when on the builder path", async () => { |
|||
config.ctx.path = "/_builder" |
|||
|
|||
await config.executeMiddleware() |
|||
|
|||
expect(config.next).toHaveBeenCalled() |
|||
}) |
|||
|
|||
it("sets a new cookie when the current cookie does not match the app id from context", async () => { |
|||
const appId = "app_123" |
|||
config.setHeaders({ |
|||
"x-budibase-app-id": appId |
|||
}) |
|||
config.ctx.cookies.get.mockImplementation(() => "cookieAppId") |
|||
|
|||
await config.executeMiddleware() |
|||
|
|||
expect(config.ctx.cookies.set).toHaveBeenCalledWith( |
|||
"budibase:currentapp:local", |
|||
appId, |
|||
expect.any(Object) |
|||
) |
|||
|
|||
}) |
|||
|
|||
it("sets the correct BUILDER auth type information when the x-budibase-type header is not 'client'", async () => { |
|||
config.ctx.cookies.get.mockImplementation(() => "budibase:builder:local") |
|||
jwt.verify.mockImplementationOnce(() => ({ |
|||
apiKey: "1234", |
|||
roleId: "BUILDER" |
|||
})) |
|||
|
|||
await config.executeMiddleware() |
|||
|
|||
expect(config.ctx.auth.authenticated).toEqual(AuthTypes.BUILDER) |
|||
expect(config.ctx.user).toMatchSnapshot() |
|||
}) |
|||
|
|||
it("sets the correct APP auth type information when the user is not in the builder", async () => { |
|||
config.setHeaders({ |
|||
"x-budibase-type": "client" |
|||
}) |
|||
config.ctx.cookies.get.mockImplementation(() => `budibase:app:local`) |
|||
jwt.verify.mockImplementationOnce(() => ({ |
|||
apiKey: "1234", |
|||
roleId: "ADMIN" |
|||
})) |
|||
|
|||
await config.executeMiddleware() |
|||
|
|||
expect(config.ctx.auth.authenticated).toEqual(AuthTypes.APP) |
|||
expect(config.ctx.user).toMatchSnapshot() |
|||
}) |
|||
|
|||
it("marks the user as unauthenticated when a token cannot be determined from the users cookie", async () => { |
|||
config.executeMiddleware() |
|||
expect(config.ctx.auth.authenticated).toBe(false) |
|||
expect(config.ctx.user.role).toEqual({ |
|||
_id: "PUBLIC", |
|||
name: "Public", |
|||
permissionId: "public" |
|||
}) |
|||
}) |
|||
|
|||
it("clears the cookie when there is an error authenticating in the builder", async () => { |
|||
config.ctx.cookies.get.mockImplementation(() => "budibase:builder:local") |
|||
jwt.verify.mockImplementationOnce(() => { |
|||
throw new Error() |
|||
}) |
|||
|
|||
await config.executeMiddleware() |
|||
|
|||
expect(config.ctx.cookies.set).toBeCalledWith("budibase:builder:local") |
|||
}) |
|||
}) |
|||
@ -0,0 +1,196 @@ |
|||
const authorizedMiddleware = require("../authorized") |
|||
const env = require("../../environment") |
|||
const apiKey = require("../../utilities/security/apikey") |
|||
const { AuthTypes } = require("../../constants") |
|||
const { PermissionTypes, PermissionLevels } = require("../../utilities/security/permissions") |
|||
const { Test } = require("supertest") |
|||
jest.mock("../../environment") |
|||
jest.mock("../../utilities/security/apikey") |
|||
|
|||
class TestConfiguration { |
|||
constructor(role) { |
|||
this.middleware = authorizedMiddleware(role) |
|||
this.next = jest.fn() |
|||
this.throw = jest.fn() |
|||
this.ctx = { |
|||
headers: {}, |
|||
request: { |
|||
url: "" |
|||
}, |
|||
auth: {}, |
|||
next: this.next, |
|||
throw: this.throw |
|||
} |
|||
} |
|||
|
|||
executeMiddleware() { |
|||
return this.middleware(this.ctx, this.next) |
|||
} |
|||
|
|||
setUser(user) { |
|||
this.ctx.user = user |
|||
} |
|||
|
|||
setMiddlewareRequiredPermission(...perms) { |
|||
this.middleware = authorizedMiddleware(...perms) |
|||
} |
|||
|
|||
setResourceId(id) { |
|||
this.ctx.resourceId = id |
|||
} |
|||
|
|||
setAuthenticated(isAuthed) { |
|||
this.ctx.auth = { authenticated: isAuthed } |
|||
} |
|||
|
|||
setRequestUrl(url) { |
|||
this.ctx.request.url = url |
|||
} |
|||
|
|||
setCloudEnv(isCloud) { |
|||
env.CLOUD = isCloud |
|||
} |
|||
|
|||
setRequestHeaders(headers) { |
|||
this.ctx.headers = headers |
|||
} |
|||
|
|||
afterEach() { |
|||
jest.clearAllMocks() |
|||
} |
|||
} |
|||
|
|||
|
|||
describe("Authorization middleware", () => { |
|||
const next = jest.fn() |
|||
let config |
|||
|
|||
afterEach(() => { |
|||
config.afterEach() |
|||
}) |
|||
|
|||
beforeEach(() => { |
|||
config = new TestConfiguration() |
|||
}) |
|||
|
|||
it("passes the middleware for local webhooks", async () => { |
|||
config.setRequestUrl("https://something/webhooks/trigger") |
|||
await config.executeMiddleware() |
|||
expect(config.next).toHaveBeenCalled() |
|||
}) |
|||
|
|||
describe("external web hook call", () => { |
|||
let ctx = {} |
|||
let middleware |
|||
|
|||
beforeEach(() => { |
|||
config = new TestConfiguration() |
|||
config.setCloudEnv(true) |
|||
config.setRequestHeaders({ |
|||
"x-api-key": "abc123", |
|||
"x-instanceid": "instance123", |
|||
}) |
|||
}) |
|||
|
|||
it("passes to next() if api key is valid", async () => { |
|||
apiKey.isAPIKeyValid.mockResolvedValueOnce(true) |
|||
|
|||
await config.executeMiddleware() |
|||
|
|||
expect(config.next).toHaveBeenCalled() |
|||
expect(config.ctx.auth).toEqual({ |
|||
authenticated: AuthTypes.EXTERNAL, |
|||
apiKey: config.ctx.headers["x-api-key"], |
|||
}) |
|||
expect(config.ctx.user).toEqual({ |
|||
appId: config.ctx.headers["x-instanceid"], |
|||
}) |
|||
}) |
|||
|
|||
it("throws if api key is invalid", async () => { |
|||
apiKey.isAPIKeyValid.mockResolvedValueOnce(false) |
|||
|
|||
await config.executeMiddleware() |
|||
|
|||
expect(config.throw).toHaveBeenCalledWith(403, "API key invalid") |
|||
}) |
|||
}) |
|||
|
|||
describe("non-webhook call", () => { |
|||
let config |
|||
|
|||
beforeEach(() => { |
|||
config = new TestConfiguration() |
|||
config.setCloudEnv(true) |
|||
config.setAuthenticated(true) |
|||
}) |
|||
|
|||
it("throws when no user data is present in context", async () => { |
|||
await config.executeMiddleware() |
|||
|
|||
expect(config.throw).toHaveBeenCalledWith(403, "No user info found") |
|||
}) |
|||
|
|||
it("passes on to next() middleware if user is an admin", async () => { |
|||
config.setUser({ |
|||
role: { |
|||
_id: "ADMIN", |
|||
} |
|||
}) |
|||
|
|||
await config.executeMiddleware() |
|||
|
|||
expect(config.next).toHaveBeenCalled() |
|||
}) |
|||
|
|||
it("throws if the user has only builder permissions", async () => { |
|||
config.setCloudEnv(false) |
|||
config.setMiddlewareRequiredPermission(PermissionTypes.BUILDER) |
|||
config.setUser({ |
|||
role: { |
|||
_id: "" |
|||
} |
|||
}) |
|||
await config.executeMiddleware() |
|||
|
|||
expect(config.throw).toHaveBeenCalledWith(403, "Not Authorized") |
|||
}) |
|||
|
|||
it("passes on to next() middleware if the user has resource permission", async () => { |
|||
config.setResourceId(PermissionTypes.QUERY) |
|||
config.setUser({ |
|||
role: { |
|||
_id: "" |
|||
} |
|||
}) |
|||
config.setMiddlewareRequiredPermission(PermissionTypes.QUERY) |
|||
|
|||
await config.executeMiddleware() |
|||
expect(config.next).toHaveBeenCalled() |
|||
}) |
|||
|
|||
it("throws if the user session is not authenticated after permission checks", async () => { |
|||
config.setUser({ |
|||
role: { |
|||
_id: "" |
|||
}, |
|||
}) |
|||
config.setAuthenticated(false) |
|||
|
|||
await config.executeMiddleware() |
|||
expect(config.throw).toHaveBeenCalledWith(403, "Session not authenticated") |
|||
}) |
|||
|
|||
it("throws if the user does not have base permissions to perform the operation", async () => { |
|||
config.setUser({ |
|||
role: { |
|||
_id: "" |
|||
}, |
|||
}) |
|||
config.setMiddlewareRequiredPermission(PermissionTypes.ADMIN, PermissionLevels.BASIC) |
|||
|
|||
await config.executeMiddleware() |
|||
expect(config.throw).toHaveBeenCalledWith(403, "User does not have permission") |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,105 @@ |
|||
const { |
|||
paramResource, |
|||
paramSubResource, |
|||
bodyResource, |
|||
bodySubResource, |
|||
ResourceIdGetter |
|||
} = require("../resourceId") |
|||
|
|||
class TestConfiguration { |
|||
constructor(middleware) { |
|||
this.middleware = middleware |
|||
this.ctx = { |
|||
request: {}, |
|||
} |
|||
this.next = jest.fn() |
|||
} |
|||
|
|||
setParams(params) { |
|||
this.ctx.params = params |
|||
} |
|||
|
|||
setBody(body) { |
|||
this.ctx.body = body |
|||
} |
|||
|
|||
executeMiddleware() { |
|||
return this.middleware(this.ctx, this.next) |
|||
} |
|||
} |
|||
|
|||
describe("resourceId middleware", () => { |
|||
it("calls next() when there is no request object to parse", () => { |
|||
const config = new TestConfiguration(paramResource("main")) |
|||
|
|||
config.executeMiddleware() |
|||
|
|||
expect(config.next).toHaveBeenCalled() |
|||
expect(config.ctx.resourceId).toBeUndefined() |
|||
}) |
|||
|
|||
it("generates a resourceId middleware for context query parameters", () => { |
|||
const config = new TestConfiguration(paramResource("main")) |
|||
config.setParams({ |
|||
main: "test" |
|||
}) |
|||
|
|||
config.executeMiddleware() |
|||
|
|||
expect(config.ctx.resourceId).toEqual("test") |
|||
}) |
|||
|
|||
it("generates a resourceId middleware for context query sub parameters", () => { |
|||
const config = new TestConfiguration(paramSubResource("main", "sub")) |
|||
config.setParams({ |
|||
main: "main", |
|||
sub: "test" |
|||
}) |
|||
|
|||
config.executeMiddleware() |
|||
|
|||
expect(config.ctx.resourceId).toEqual("main") |
|||
expect(config.ctx.subResourceId).toEqual("test") |
|||
}) |
|||
|
|||
it("generates a resourceId middleware for context request body", () => { |
|||
const config = new TestConfiguration(bodyResource("main")) |
|||
config.setBody({ |
|||
main: "test" |
|||
}) |
|||
|
|||
config.executeMiddleware() |
|||
|
|||
expect(config.ctx.resourceId).toEqual("test") |
|||
}) |
|||
|
|||
it("generates a resourceId middleware for context request body sub fields", () => { |
|||
const config = new TestConfiguration(bodySubResource("main", "sub")) |
|||
config.setBody({ |
|||
main: "main", |
|||
sub: "test" |
|||
}) |
|||
|
|||
config.executeMiddleware() |
|||
|
|||
expect(config.ctx.resourceId).toEqual("main") |
|||
expect(config.ctx.subResourceId).toEqual("test") |
|||
}) |
|||
|
|||
it("parses resourceIds correctly for custom middlewares", () => { |
|||
const middleware = new ResourceIdGetter("body") |
|||
.mainResource("custom") |
|||
.subResource("customSub") |
|||
.build() |
|||
config = new TestConfiguration(middleware) |
|||
config.setBody({ |
|||
custom: "test", |
|||
customSub: "subtest" |
|||
}) |
|||
|
|||
config.executeMiddleware() |
|||
|
|||
expect(config.ctx.resourceId).toEqual("test") |
|||
expect(config.ctx.subResourceId).toEqual("subtest") |
|||
}) |
|||
}) |
|||
@ -0,0 +1,75 @@ |
|||
const selfHostMiddleware = require("../selfhost"); |
|||
const env = require("../../environment") |
|||
const hosting = require("../../utilities/builder/hosting"); |
|||
jest.mock("../../environment") |
|||
jest.mock("../../utilities/builder/hosting") |
|||
|
|||
class TestConfiguration { |
|||
constructor() { |
|||
this.next = jest.fn() |
|||
this.throw = jest.fn() |
|||
this.middleware = selfHostMiddleware |
|||
|
|||
this.ctx = { |
|||
next: this.next, |
|||
throw: this.throw |
|||
} |
|||
} |
|||
|
|||
executeMiddleware() { |
|||
return this.middleware(this.ctx, this.next) |
|||
} |
|||
|
|||
setCloudHosted() { |
|||
env.CLOUD = 1 |
|||
env.SELF_HOSTED = 0 |
|||
} |
|||
|
|||
setSelfHosted() { |
|||
env.CLOUD = 0 |
|||
env.SELF_HOSTED = 1 |
|||
} |
|||
|
|||
afterEach() { |
|||
jest.clearAllMocks() |
|||
} |
|||
} |
|||
|
|||
describe("Self host middleware", () => { |
|||
let config |
|||
|
|||
beforeEach(() => { |
|||
config = new TestConfiguration() |
|||
}) |
|||
|
|||
afterEach(() => { |
|||
config.afterEach() |
|||
}) |
|||
|
|||
it("calls next() when CLOUD and SELF_HOSTED env vars are set", async () => { |
|||
env.CLOUD = 1 |
|||
env.SELF_HOSTED = 1 |
|||
|
|||
await config.executeMiddleware() |
|||
expect(config.next).toHaveBeenCalled() |
|||
}) |
|||
|
|||
it("throws when hostingInfo type is cloud", async () => { |
|||
config.setSelfHosted() |
|||
|
|||
hosting.getHostingInfo.mockImplementationOnce(() => ({ type: hosting.HostingTypes.CLOUD })) |
|||
|
|||
await config.executeMiddleware() |
|||
expect(config.throw).toHaveBeenCalledWith(400, "Endpoint unavailable in cloud hosting.") |
|||
expect(config.next).not.toHaveBeenCalled() |
|||
}) |
|||
|
|||
it("calls the self hosting middleware to pass through to next() when the hostingInfo type is self", async () => { |
|||
config.setSelfHosted() |
|||
|
|||
hosting.getHostingInfo.mockImplementationOnce(() => ({ type: hosting.HostingTypes.SELF })) |
|||
|
|||
await config.executeMiddleware() |
|||
expect(config.next).toHaveBeenCalled() |
|||
}) |
|||
}) |
|||
@ -0,0 +1,129 @@ |
|||
const usageQuotaMiddleware = require("../usageQuota") |
|||
const usageQuota = require("../../utilities/usageQuota") |
|||
const CouchDB = require("../../db") |
|||
const env = require("../../environment") |
|||
|
|||
jest.mock("../../db"); |
|||
jest.mock("../../utilities/usageQuota") |
|||
jest.mock("../../environment") |
|||
|
|||
class TestConfiguration { |
|||
constructor() { |
|||
this.throw = jest.fn() |
|||
this.next = jest.fn() |
|||
this.middleware = usageQuotaMiddleware |
|||
this.ctx = { |
|||
throw: this.throw, |
|||
next: this.next, |
|||
user: { |
|||
appId: "test" |
|||
}, |
|||
request: { |
|||
body: {} |
|||
}, |
|||
req: { |
|||
method: "POST", |
|||
url: "/rows" |
|||
} |
|||
} |
|||
} |
|||
|
|||
executeMiddleware() { |
|||
return this.middleware(this.ctx, this.next) |
|||
} |
|||
|
|||
cloudHosted(bool) { |
|||
if (bool) { |
|||
env.CLOUD = 1 |
|||
this.ctx.auth = { apiKey: "test" } |
|||
} else { |
|||
env.CLOUD = 0 |
|||
} |
|||
} |
|||
|
|||
setMethod(method) { |
|||
this.ctx.req.method = method |
|||
} |
|||
|
|||
setUrl(url) { |
|||
this.ctx.req.url = url |
|||
} |
|||
|
|||
setBody(body) { |
|||
this.ctx.request.body = body |
|||
} |
|||
|
|||
setFiles(files) { |
|||
this.ctx.request.files = { file: files } |
|||
} |
|||
} |
|||
|
|||
describe("usageQuota middleware", () => { |
|||
let config |
|||
|
|||
beforeEach(() => { |
|||
config = new TestConfiguration() |
|||
}) |
|||
|
|||
it("skips the middleware if there is no usage property or method", async () => { |
|||
await config.executeMiddleware() |
|||
expect(config.next).toHaveBeenCalled() |
|||
}) |
|||
|
|||
it("passes through to next middleware if document already exists", async () => { |
|||
config.setBody({ |
|||
_id: "test" |
|||
}) |
|||
|
|||
CouchDB.mockImplementationOnce(() => ({ |
|||
get: async () => true |
|||
})) |
|||
|
|||
await config.executeMiddleware() |
|||
|
|||
expect(config.next).toHaveBeenCalled() |
|||
expect(config.ctx.preExisting).toBe(true) |
|||
}) |
|||
|
|||
it("throws if request has _id, but the document no longer exists", async () => { |
|||
config.setBody({ |
|||
_id: "123" |
|||
}) |
|||
|
|||
CouchDB.mockImplementationOnce(() => ({ |
|||
get: async () => { |
|||
throw new Error() |
|||
} |
|||
})) |
|||
|
|||
await config.executeMiddleware() |
|||
expect(config.throw).toHaveBeenCalledWith(404, `${config.ctx.request.body._id} does not exist`) |
|||
}) |
|||
|
|||
it("calculates and persists the correct usage quota for the relevant action", async () => { |
|||
config.setUrl("/rows") |
|||
config.cloudHosted(true) |
|||
|
|||
await config.executeMiddleware() |
|||
|
|||
expect(usageQuota.update).toHaveBeenCalledWith("test", "rows", 1) |
|||
expect(config.next).toHaveBeenCalled() |
|||
}) |
|||
|
|||
it("calculates the correct file size from a file upload call and adds it to quota", async () => { |
|||
config.setUrl("/upload") |
|||
config.cloudHosted(true) |
|||
config.setFiles([ |
|||
{ |
|||
size: 100 |
|||
}, |
|||
{ |
|||
size: 10000 |
|||
}, |
|||
]) |
|||
await config.executeMiddleware() |
|||
|
|||
expect(usageQuota.update).toHaveBeenCalledWith("test", "storage", 10100) |
|||
expect(config.next).toHaveBeenCalled() |
|||
}) |
|||
}) |
|||
Loading…
Reference in new issue