From 2160f4e5e205729e1af2afce6cf586c3f537e6f0 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 27 Oct 2023 16:24:02 +0100 Subject: [PATCH 1/8] Add valid extension list to shared-core. --- packages/shared-core/src/constants.ts | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/shared-core/src/constants.ts b/packages/shared-core/src/constants.ts index 725c246e2f..312e69c896 100644 --- a/packages/shared-core/src/constants.ts +++ b/packages/shared-core/src/constants.ts @@ -96,3 +96,46 @@ export enum BuilderSocketEvent { export const SocketSessionTTL = 60 export const ValidQueryNameRegex = /^[^()]*$/ export const ValidColumnNameRegex = /^[_a-zA-Z0-9\s]*$/g + +export const ValidFileExtensions = [ + "avif", + "css", + "csv", + "docx", + "drawio", + "editorconfig", + "edl", + "enc", + "export", + "geojson", + "gif", + "htm", + "html", + "ics", + "iqy", + "jfif", + "jpeg", + "jpg", + "json", + "log", + "md", + "mid", + "odt", + "pdf", + "png", + "ris", + "rtf", + "svg", + "tex", + "toml", + "twig", + "txt", + "url", + "wav", + "webp", + "xls", + "xlsx", + "xml", + "yaml", + "yml", +] From 6bb6f106d53ee3e1f866e4317e73f62cba1d207b Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 27 Oct 2023 16:46:30 +0100 Subject: [PATCH 2/8] Apply valid file types to AttachmentCell. --- packages/bbui/src/Form/Dropzone.svelte | 2 ++ .../src/components/grid/cells/AttachmentCell.svelte | 3 +++ packages/shared-core/src/constants.ts | 1 - 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/bbui/src/Form/Dropzone.svelte b/packages/bbui/src/Form/Dropzone.svelte index 2a6fa1c57b..94742ea08d 100644 --- a/packages/bbui/src/Form/Dropzone.svelte +++ b/packages/bbui/src/Form/Dropzone.svelte @@ -17,6 +17,7 @@ export let fileTags = [] export let maximum = undefined export let compact = false + export let extensions = undefined const dispatch = createEventDispatcher() const onChange = e => { @@ -39,6 +40,7 @@ {fileTags} {maximum} {compact} + {extensions} on:change={onChange} /> diff --git a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte index a27c31bbe5..4e2a6025e5 100644 --- a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte @@ -2,6 +2,7 @@ import { onMount } from "svelte" import { getContext } from "svelte" import { Dropzone } from "@budibase/bbui" + import { ValidFileExtensions } from "@budibase/shared-core" export let value export let focused = false @@ -13,6 +14,7 @@ const { API, notifications } = getContext("grid") const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"] + const validExtensions = ValidFileExtensions.map(ext => `.${ext}`).join(", ") let isOpen = false @@ -96,6 +98,7 @@ {value} compact on:change={e => onChange(e.detail)} + extensions={validExtensions} {processFiles} {deleteAttachments} {handleFileTooLarge} diff --git a/packages/shared-core/src/constants.ts b/packages/shared-core/src/constants.ts index 312e69c896..e7c6feb20a 100644 --- a/packages/shared-core/src/constants.ts +++ b/packages/shared-core/src/constants.ts @@ -96,7 +96,6 @@ export enum BuilderSocketEvent { export const SocketSessionTTL = 60 export const ValidQueryNameRegex = /^[^()]*$/ export const ValidColumnNameRegex = /^[_a-zA-Z0-9\s]*$/g - export const ValidFileExtensions = [ "avif", "css", From 5539ff9c9ce292d844c7d88b4a8384e45310b259 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 27 Oct 2023 16:53:32 +0100 Subject: [PATCH 3/8] Apply valid file types to RowFieldControl and AttackmentField. --- .../src/components/backend/DataTable/RowFieldControl.svelte | 5 ++++- .../client/src/components/app/forms/AttachmentField.svelte | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte index 61b706e28e..9d52cb815e 100644 --- a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte +++ b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte @@ -13,6 +13,7 @@ import { capitalise } from "helpers" import LinkedRowSelector from "components/common/LinkedRowSelector.svelte" import Editor from "../../integration/QueryEditor.svelte" + import { ValidFileExtensions } from "@budibase/shared-core" export let defaultValue export let meta @@ -20,6 +21,8 @@ export let readonly export let error + let validExtensions = ValidFileExtensions.map(ext => `.${ext}`).join(", ") + const resolveTimeStamp = timestamp => { if (!timestamp) { return null @@ -59,7 +62,7 @@ bind:value /> {:else if type === "attachment"} - + {:else if type === "boolean"} {:else if type === "array" && meta.constraints.inclusion.length !== 0} diff --git a/packages/client/src/components/app/forms/AttachmentField.svelte b/packages/client/src/components/app/forms/AttachmentField.svelte index e24115ebc0..2effe607ae 100644 --- a/packages/client/src/components/app/forms/AttachmentField.svelte +++ b/packages/client/src/components/app/forms/AttachmentField.svelte @@ -2,13 +2,14 @@ import Field from "./Field.svelte" import { CoreDropzone } from "@budibase/bbui" import { getContext } from "svelte" + import { ValidFileExtensions } from "@budibase/shared-core" export let field export let label export let disabled = false export let compact = false export let validation - export let extensions + export let extensions = ValidFileExtensions.map(ext => `.${ext}`).join(", ") export let onChange export let maximum = undefined From f1aa32e4461b604c551915953940a990f0670404 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 27 Oct 2023 17:19:39 +0100 Subject: [PATCH 4/8] Truncate file size on the grid, validate extension in the attachment API. --- packages/bbui/src/Form/Core/Dropzone.svelte | 10 ++++++---- .../backend/DataTable/RowFieldControl.svelte | 5 ++++- .../server/src/api/controllers/static/index.ts | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/bbui/src/Form/Core/Dropzone.svelte b/packages/bbui/src/Form/Core/Dropzone.svelte index e9ee75bd8b..0b6a9bb94f 100644 --- a/packages/bbui/src/Form/Core/Dropzone.svelte +++ b/packages/bbui/src/Form/Core/Dropzone.svelte @@ -159,8 +159,10 @@ {#if selectedImage.size}
{#if selectedImage.size <= BYTES_IN_MB} - {`${selectedImage.size / BYTES_IN_KB} KB`} - {:else}{`${selectedImage.size / BYTES_IN_MB} MB`}{/if} + {`${(selectedImage.size / BYTES_IN_KB).toFixed(1)} KB`} + {:else}{`${(selectedImage.size / BYTES_IN_MB).toFixed( + 1 + )} MB`}{/if}
{/if} {#if !disabled} @@ -203,8 +205,8 @@ {#if file.size}
{#if file.size <= BYTES_IN_MB} - {`${file.size / BYTES_IN_KB} KB`} - {:else}{`${file.size / BYTES_IN_MB} MB`}{/if} + {`${(file.size / BYTES_IN_KB).toFixed(1)} KB`} + {:else}{`${(file.size / BYTES_IN_MB).toFixed(1)} MB`}{/if}
{/if} {#if !disabled} diff --git a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte index 9d52cb815e..ea1161fe9b 100644 --- a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte +++ b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte @@ -14,6 +14,7 @@ import LinkedRowSelector from "components/common/LinkedRowSelector.svelte" import Editor from "../../integration/QueryEditor.svelte" import { ValidFileExtensions } from "@budibase/shared-core" + import { admin } from "stores/portal" export let defaultValue export let meta @@ -21,7 +22,9 @@ export let readonly export let error - let validExtensions = ValidFileExtensions.map(ext => `.${ext}`).join(", ") + let validExtensions = $admin.cloud + ? ValidFileExtensions.map(ext => `.${ext}`).join(", ") + : "*" const resolveTimeStamp = timestamp => { if (!timestamp) { diff --git a/packages/server/src/api/controllers/static/index.ts b/packages/server/src/api/controllers/static/index.ts index 984cb16c06..e8d403ad12 100644 --- a/packages/server/src/api/controllers/static/index.ts +++ b/packages/server/src/api/controllers/static/index.ts @@ -1,3 +1,5 @@ +import { ValidFileExtensions } from "@budibase/shared-core" + require("svelte/register") import { join } from "../../../utilities/centralPath" @@ -17,6 +19,7 @@ import fs from "fs" import sdk from "../../../sdk" import * as pro from "@budibase/pro" import { App, Ctx } from "@budibase/types" +import environment from "../../../environment" const send = require("koa-send") @@ -78,6 +81,17 @@ export const uploadFile = async function (ctx: Ctx) { const uploads = files.map(async (file: any) => { const fileExtension = [...file.name.split(".")].pop() + if ( + !environment.SELF_HOSTED && + !ValidFileExtensions.includes(fileExtension) + ) { + ctx.throw( + 400, + `Invalid file extension. Valid extensions are: ${ValidFileExtensions.join( + ", " + )}` + ) + } // filenames converted to UUIDs so they are unique const processedFileName = `${uuid.v4()}.${fileExtension}` From 84ba840dbcfea6e059c3872999f6ac716783eee5 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 27 Oct 2023 17:29:05 +0100 Subject: [PATCH 5/8] Apply valid file type change to AttachmentField in cloud only. --- .../client/src/components/app/forms/AttachmentField.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/app/forms/AttachmentField.svelte b/packages/client/src/components/app/forms/AttachmentField.svelte index 2effe607ae..e57510c770 100644 --- a/packages/client/src/components/app/forms/AttachmentField.svelte +++ b/packages/client/src/components/app/forms/AttachmentField.svelte @@ -3,13 +3,16 @@ import { CoreDropzone } from "@budibase/bbui" import { getContext } from "svelte" import { ValidFileExtensions } from "@budibase/shared-core" + import { environmentStore } from "../../../stores/index.js" export let field export let label export let disabled = false export let compact = false export let validation - export let extensions = ValidFileExtensions.map(ext => `.${ext}`).join(", ") + export let extensions = $environmentStore.cloud + ? ValidFileExtensions.map(ext => `.${ext}`).join(", ") + : "*" export let onChange export let maximum = undefined From 436d6a1585a9d904b0339e7331d24a43e3ba135f Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 30 Oct 2023 14:39:12 +0000 Subject: [PATCH 6/8] Revert frontend changes to filter out file extensions in the upload box. --- packages/bbui/src/Form/Dropzone.svelte | 2 -- .../components/backend/DataTable/RowFieldControl.svelte | 8 +------- .../src/components/app/forms/AttachmentField.svelte | 6 +----- .../src/components/grid/cells/AttachmentCell.svelte | 3 --- 4 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/bbui/src/Form/Dropzone.svelte b/packages/bbui/src/Form/Dropzone.svelte index 94742ea08d..2a6fa1c57b 100644 --- a/packages/bbui/src/Form/Dropzone.svelte +++ b/packages/bbui/src/Form/Dropzone.svelte @@ -17,7 +17,6 @@ export let fileTags = [] export let maximum = undefined export let compact = false - export let extensions = undefined const dispatch = createEventDispatcher() const onChange = e => { @@ -40,7 +39,6 @@ {fileTags} {maximum} {compact} - {extensions} on:change={onChange} /> diff --git a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte index ea1161fe9b..61b706e28e 100644 --- a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte +++ b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte @@ -13,8 +13,6 @@ import { capitalise } from "helpers" import LinkedRowSelector from "components/common/LinkedRowSelector.svelte" import Editor from "../../integration/QueryEditor.svelte" - import { ValidFileExtensions } from "@budibase/shared-core" - import { admin } from "stores/portal" export let defaultValue export let meta @@ -22,10 +20,6 @@ export let readonly export let error - let validExtensions = $admin.cloud - ? ValidFileExtensions.map(ext => `.${ext}`).join(", ") - : "*" - const resolveTimeStamp = timestamp => { if (!timestamp) { return null @@ -65,7 +59,7 @@ bind:value /> {:else if type === "attachment"} - + {:else if type === "boolean"} {:else if type === "array" && meta.constraints.inclusion.length !== 0} diff --git a/packages/client/src/components/app/forms/AttachmentField.svelte b/packages/client/src/components/app/forms/AttachmentField.svelte index e57510c770..e24115ebc0 100644 --- a/packages/client/src/components/app/forms/AttachmentField.svelte +++ b/packages/client/src/components/app/forms/AttachmentField.svelte @@ -2,17 +2,13 @@ import Field from "./Field.svelte" import { CoreDropzone } from "@budibase/bbui" import { getContext } from "svelte" - import { ValidFileExtensions } from "@budibase/shared-core" - import { environmentStore } from "../../../stores/index.js" export let field export let label export let disabled = false export let compact = false export let validation - export let extensions = $environmentStore.cloud - ? ValidFileExtensions.map(ext => `.${ext}`).join(", ") - : "*" + export let extensions export let onChange export let maximum = undefined diff --git a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte index 4e2a6025e5..a27c31bbe5 100644 --- a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte @@ -2,7 +2,6 @@ import { onMount } from "svelte" import { getContext } from "svelte" import { Dropzone } from "@budibase/bbui" - import { ValidFileExtensions } from "@budibase/shared-core" export let value export let focused = false @@ -14,7 +13,6 @@ const { API, notifications } = getContext("grid") const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"] - const validExtensions = ValidFileExtensions.map(ext => `.${ext}`).join(", ") let isOpen = false @@ -98,7 +96,6 @@ {value} compact on:change={e => onChange(e.detail)} - extensions={validExtensions} {processFiles} {deleteAttachments} {handleFileTooLarge} From af59039d1c8a5735f0325c73c1c0da846c845c31 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 30 Oct 2023 16:46:27 +0000 Subject: [PATCH 7/8] Add tests for attachment processing endpoint. --- .../src/api/controllers/static/index.ts | 100 ++++++++++-------- .../src/api/routes/tests/attachment.spec.ts | 49 +++++++++ .../src/api/routes/tests/static.spec.js | 8 +- .../src/api/routes/tests/webhook.spec.ts | 8 +- .../integrations/tests/googlesheets.spec.ts | 9 +- .../src/tests/utilities/TestConfiguration.ts | 47 ++++---- .../src/tests/utilities/api/attachment.ts | 35 ++++++ .../server/src/tests/utilities/api/index.ts | 3 + packages/types/src/api/web/app/attachment.ts | 9 ++ packages/types/src/api/web/app/index.ts | 1 + 10 files changed, 199 insertions(+), 70 deletions(-) create mode 100644 packages/server/src/api/routes/tests/attachment.spec.ts create mode 100644 packages/server/src/tests/utilities/api/attachment.ts create mode 100644 packages/types/src/api/web/app/attachment.ts diff --git a/packages/server/src/api/controllers/static/index.ts b/packages/server/src/api/controllers/static/index.ts index e8d403ad12..8fbc0db910 100644 --- a/packages/server/src/api/controllers/static/index.ts +++ b/packages/server/src/api/controllers/static/index.ts @@ -13,35 +13,21 @@ import { } from "../../../utilities/fileSystem" import env from "../../../environment" import { DocumentType } from "../../../db/utils" -import { context, objectStore, utils, configs } from "@budibase/backend-core" +import { + context, + objectStore, + utils, + configs, + BadRequestError, +} from "@budibase/backend-core" import AWS from "aws-sdk" import fs from "fs" import sdk from "../../../sdk" import * as pro from "@budibase/pro" -import { App, Ctx } from "@budibase/types" -import environment from "../../../environment" +import { App, Ctx, ProcessAttachmentResponse, Upload } from "@budibase/types" const send = require("koa-send") -async function prepareUpload({ s3Key, bucket, metadata, file }: any) { - const response = await objectStore.upload({ - bucket, - metadata, - filename: s3Key, - path: file.path, - type: file.type, - }) - - // don't store a URL, work this out on the way out as the URL could change - return { - size: file.size, - name: file.name, - url: objectStore.getAppFileUrl(s3Key), - extension: [...file.name.split(".")].pop(), - key: response.Key, - } -} - export const toggleBetaUiFeature = async function (ctx: Ctx) { const cookieName = `beta:${ctx.params.feature}` @@ -75,34 +61,58 @@ export const serveBuilder = async function (ctx: Ctx) { await send(ctx, ctx.file, { root: builderPath }) } -export const uploadFile = async function (ctx: Ctx) { +export const uploadFile = async function ( + ctx: Ctx<{}, ProcessAttachmentResponse> +) { const file = ctx.request?.files?.file + if (!file) { + throw new BadRequestError("No file provided") + } + let files = file && Array.isArray(file) ? Array.from(file) : [file] - const uploads = files.map(async (file: any) => { - const fileExtension = [...file.name.split(".")].pop() - if ( - !environment.SELF_HOSTED && - !ValidFileExtensions.includes(fileExtension) - ) { - ctx.throw( - 400, - `Invalid file extension. Valid extensions are: ${ValidFileExtensions.join( - ", " - )}` - ) - } - // filenames converted to UUIDs so they are unique - const processedFileName = `${uuid.v4()}.${fileExtension}` + ctx.body = await Promise.all( + files.map(async file => { + if (!file.name) { + throw new BadRequestError( + "Attempted to upload a file without a filename" + ) + } - return prepareUpload({ - file, - s3Key: `${context.getProdAppId()}/attachments/${processedFileName}`, - bucket: ObjectStoreBuckets.APPS, + const extension = [...file.name.split(".")].pop() + if (!extension) { + throw new BadRequestError( + `File "${file.name}" has no extension, an extension is required to upload a file` + ) + } + + if (!env.SELF_HOSTED && !ValidFileExtensions.includes(extension)) { + throw new BadRequestError( + `File "${file.name}" has an invalid extension: "${extension}"` + ) + } + + // filenames converted to UUIDs so they are unique + const processedFileName = `${uuid.v4()}.${extension}` + + const s3Key = `${context.getProdAppId()}/attachments/${processedFileName}` + + const response = await objectStore.upload({ + bucket: ObjectStoreBuckets.APPS, + filename: s3Key, + path: file.path, + type: file.type, + }) + + return { + size: file.size, + name: file.name, + url: objectStore.getAppFileUrl(s3Key), + extension, + key: response.Key, + } }) - }) - - ctx.body = await Promise.all(uploads) + ) } export const deleteObjects = async function (ctx: Ctx) { diff --git a/packages/server/src/api/routes/tests/attachment.spec.ts b/packages/server/src/api/routes/tests/attachment.spec.ts new file mode 100644 index 0000000000..14d2e845f6 --- /dev/null +++ b/packages/server/src/api/routes/tests/attachment.spec.ts @@ -0,0 +1,49 @@ +import * as setup from "./utilities" +import { APIError } from "@budibase/types" + +describe("/api/applications/:appId/sync", () => { + let config = setup.getConfig() + + afterAll(setup.afterAll) + beforeAll(async () => { + await config.init() + }) + + describe("/api/attachments/process", () => { + it("should accept an image file upload", async () => { + let resp = await config.api.attachment.process( + "1px.jpg", + Buffer.from([0]) + ) + expect(resp.length).toBe(1) + + let upload = resp[0] + expect(upload.url.endsWith(".jpg")).toBe(true) + expect(upload.extension).toBe("jpg") + expect(upload.size).toBe(1) + expect(upload.name).toBe("1px.jpg") + }) + + it("should reject an upload with a malicious file extension", async () => { + await config.withEnv({ SELF_HOSTED: undefined }, async () => { + let resp = (await config.api.attachment.process( + "ohno.exe", + Buffer.from([0]), + { expectStatus: 400 } + )) as unknown as APIError + expect(resp.message).toContain("invalid extension") + }) + }) + + it("should reject an upload with no file", async () => { + let resp = (await config.api.attachment.process( + undefined as any, + undefined as any, + { + expectStatus: 400, + } + )) as unknown as APIError + expect(resp.message).toContain("No file provided") + }) + }) +}) diff --git a/packages/server/src/api/routes/tests/static.spec.js b/packages/server/src/api/routes/tests/static.spec.js index 13d963d057..a28d9ecd79 100644 --- a/packages/server/src/api/routes/tests/static.spec.js +++ b/packages/server/src/api/routes/tests/static.spec.js @@ -5,11 +5,15 @@ describe("/static", () => { let request = setup.getRequest() let config = setup.getConfig() let app + let cleanupEnv - afterAll(setup.afterAll) + afterAll(() => { + setup.afterAll() + cleanupEnv() + }) beforeAll(async () => { - config.modeSelf() + cleanupEnv = config.setEnv({ SELF_HOSTED: "true" }) app = await config.init() }) diff --git a/packages/server/src/api/routes/tests/webhook.spec.ts b/packages/server/src/api/routes/tests/webhook.spec.ts index e7046d07c8..118bfca95f 100644 --- a/packages/server/src/api/routes/tests/webhook.spec.ts +++ b/packages/server/src/api/routes/tests/webhook.spec.ts @@ -8,11 +8,15 @@ describe("/webhooks", () => { let request = setup.getRequest() let config = setup.getConfig() let webhook: Webhook + let cleanupEnv: () => void - afterAll(setup.afterAll) + afterAll(() => { + setup.afterAll() + cleanupEnv() + }) const setupTest = async () => { - config.modeSelf() + cleanupEnv = config.setEnv({ SELF_HOSTED: "true" }) await config.init() const autoConfig = basicAutomation() autoConfig.definition.trigger.schema = { diff --git a/packages/server/src/integrations/tests/googlesheets.spec.ts b/packages/server/src/integrations/tests/googlesheets.spec.ts index 748baddc39..a38c6bda45 100644 --- a/packages/server/src/integrations/tests/googlesheets.spec.ts +++ b/packages/server/src/integrations/tests/googlesheets.spec.ts @@ -35,13 +35,18 @@ import { FieldType, Table, TableSchema } from "@budibase/types" describe("Google Sheets Integration", () => { let integration: any, config = new TestConfiguration() + let cleanupEnv: () => void beforeAll(() => { - config.setGoogleAuth("test") + cleanupEnv = config.setEnv({ + GOOGLE_CLIENT_ID: "test", + GOOGLE_CLIENT_SECRET: "test", + }) }) afterAll(async () => { - await config.end() + cleanupEnv() + config.end() }) beforeEach(async () => { diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index cec8c8aa12..5096b054a6 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -58,6 +58,7 @@ import { } from "@budibase/types" import API from "./api" +import { cloneDeep } from "lodash" type DefaultUserValues = { globalUserId: string @@ -188,30 +189,38 @@ class TestConfiguration { } } - // MODES - setMultiTenancy = (value: boolean) => { - env._set("MULTI_TENANCY", value) - coreEnv._set("MULTI_TENANCY", value) + async withEnv(newEnvVars: Partial, f: () => Promise) { + let cleanup = this.setEnv(newEnvVars) + try { + await f() + } finally { + cleanup() + } } - setSelfHosted = (value: boolean) => { - env._set("SELF_HOSTED", value) - coreEnv._set("SELF_HOSTED", value) - } + /* + * Sets the environment variables to the given values and returns a function + * that can be called to reset the environment variables to their original values. + */ + setEnv(newEnvVars: Partial): () => void { + const oldEnv = cloneDeep(env) + const oldCoreEnv = cloneDeep(coreEnv) - setGoogleAuth = (value: string) => { - env._set("GOOGLE_CLIENT_ID", value) - env._set("GOOGLE_CLIENT_SECRET", value) - coreEnv._set("GOOGLE_CLIENT_ID", value) - coreEnv._set("GOOGLE_CLIENT_SECRET", value) - } + let key: keyof typeof newEnvVars + for (key in newEnvVars) { + env._set(key, newEnvVars[key]) + coreEnv._set(key, newEnvVars[key]) + } - modeCloud = () => { - this.setSelfHosted(false) - } + return () => { + for (const [key, value] of Object.entries(oldEnv)) { + env._set(key, value) + } - modeSelf = () => { - this.setSelfHosted(true) + for (const [key, value] of Object.entries(oldCoreEnv)) { + coreEnv._set(key, value) + } + } } // UTILS diff --git a/packages/server/src/tests/utilities/api/attachment.ts b/packages/server/src/tests/utilities/api/attachment.ts new file mode 100644 index 0000000000..a466f1a67e --- /dev/null +++ b/packages/server/src/tests/utilities/api/attachment.ts @@ -0,0 +1,35 @@ +import { + APIError, + Datasource, + ProcessAttachmentResponse, +} from "@budibase/types" +import TestConfiguration from "../TestConfiguration" +import { TestAPI } from "./base" +import fs from "fs" + +export class AttachmentAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + process = async ( + name: string, + file: Buffer | fs.ReadStream | string, + { expectStatus } = { expectStatus: 200 } + ): Promise => { + const result = await this.request + .post(`/api/attachments/process`) + .attach("file", file, name) + .set(this.config.defaultHeaders()) + + if (result.statusCode !== expectStatus) { + throw new Error( + `Expected status ${expectStatus} but got ${ + result.statusCode + }, body: ${JSON.stringify(result.body)}` + ) + } + + return result.body + } +} diff --git a/packages/server/src/tests/utilities/api/index.ts b/packages/server/src/tests/utilities/api/index.ts index fce8237760..30ef7c478d 100644 --- a/packages/server/src/tests/utilities/api/index.ts +++ b/packages/server/src/tests/utilities/api/index.ts @@ -7,6 +7,7 @@ import { DatasourceAPI } from "./datasource" import { LegacyViewAPI } from "./legacyView" import { ScreenAPI } from "./screen" import { ApplicationAPI } from "./application" +import { AttachmentAPI } from "./attachment" export default class API { table: TableAPI @@ -17,6 +18,7 @@ export default class API { datasource: DatasourceAPI screen: ScreenAPI application: ApplicationAPI + attachment: AttachmentAPI constructor(config: TestConfiguration) { this.table = new TableAPI(config) @@ -27,5 +29,6 @@ export default class API { this.datasource = new DatasourceAPI(config) this.screen = new ScreenAPI(config) this.application = new ApplicationAPI(config) + this.attachment = new AttachmentAPI(config) } } diff --git a/packages/types/src/api/web/app/attachment.ts b/packages/types/src/api/web/app/attachment.ts new file mode 100644 index 0000000000..792bdf3885 --- /dev/null +++ b/packages/types/src/api/web/app/attachment.ts @@ -0,0 +1,9 @@ +export interface Upload { + size: number + name: string + url: string + extension: string + key: string +} + +export type ProcessAttachmentResponse = Upload[] diff --git a/packages/types/src/api/web/app/index.ts b/packages/types/src/api/web/app/index.ts index 276d7fa7c1..f5b876009b 100644 --- a/packages/types/src/api/web/app/index.ts +++ b/packages/types/src/api/web/app/index.ts @@ -5,3 +5,4 @@ export * from "./view" export * from "./rows" export * from "./table" export * from "./permission" +export * from "./attachment" From ca9491ce67694db85ff941f639310b5b1c9be56a Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 30 Oct 2023 16:55:57 +0000 Subject: [PATCH 8/8] Surface error message from attachments API to user. --- packages/builder/src/components/common/Dropzone.svelte | 2 +- .../src/components/grid/cells/AttachmentCell.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/components/common/Dropzone.svelte b/packages/builder/src/components/common/Dropzone.svelte index fd2359fd91..daa6ad1807 100644 --- a/packages/builder/src/components/common/Dropzone.svelte +++ b/packages/builder/src/components/common/Dropzone.svelte @@ -23,7 +23,7 @@ try { return await API.uploadBuilderAttachment(data) } catch (error) { - notifications.error("Failed to upload attachment") + notifications.error(error.message || "Failed to upload attachment") return [] } } diff --git a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte index a27c31bbe5..fc0001d55e 100644 --- a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte @@ -55,7 +55,7 @@ try { return await API.uploadBuilderAttachment(data) } catch (error) { - $notifications.error("Failed to upload attachment") + $notifications.error(error.message || "Failed to upload attachment") return [] } }