472 lines
15 KiB
TypeScript
472 lines
15 KiB
TypeScript
import { DEFAULT_TABLES } from "../../../db/defaultData/datasource_bb_default"
|
|
import { setEnv } from "../../../environment"
|
|
|
|
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
|
import * as setup from "./utilities"
|
|
import { AppStatus } from "../../../db/utils"
|
|
import { events, utils, context } from "@budibase/backend-core"
|
|
import env from "../../../environment"
|
|
import { type App, BuiltinPermissionID } from "@budibase/types"
|
|
import tk from "timekeeper"
|
|
import * as uuid from "uuid"
|
|
import { structures } from "@budibase/backend-core/tests"
|
|
import nock from "nock"
|
|
import path from "path"
|
|
|
|
describe("/applications", () => {
|
|
let config = setup.getConfig()
|
|
let app: App, cleanup: () => void
|
|
|
|
afterAll(() => {
|
|
setup.afterAll()
|
|
cleanup()
|
|
})
|
|
beforeAll(async () => {
|
|
cleanup = setEnv({ USE_LOCAL_COMPONENT_LIBS: "0" })
|
|
await config.init()
|
|
})
|
|
|
|
beforeEach(async () => {
|
|
app = await config.api.application.create({ name: utils.newid() })
|
|
const deployment = await config.api.application.publish(app.appId)
|
|
if (deployment.status !== "SUCCESS") {
|
|
throw new Error("Failed to publish app")
|
|
}
|
|
jest.clearAllMocks()
|
|
nock.cleanAll()
|
|
})
|
|
|
|
// These need to go first for the app totals to make sense
|
|
describe("permissions", () => {
|
|
it("should only return apps a user has access to", async () => {
|
|
let user = await config.createUser({
|
|
builder: { global: false },
|
|
admin: { global: false },
|
|
})
|
|
|
|
await config.withUser(user, async () => {
|
|
const apps = await config.api.application.fetch()
|
|
expect(apps).toHaveLength(0)
|
|
})
|
|
|
|
user = await config.globalUser({
|
|
...user,
|
|
builder: {
|
|
apps: [config.getProdAppId()],
|
|
},
|
|
})
|
|
|
|
await config.withUser(user, async () => {
|
|
const apps = await config.api.application.fetch()
|
|
expect(apps).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
it("should only return apps a user has access to through a custom role", async () => {
|
|
let user = await config.createUser({
|
|
builder: { global: false },
|
|
admin: { global: false },
|
|
})
|
|
|
|
await config.withUser(user, async () => {
|
|
const apps = await config.api.application.fetch()
|
|
expect(apps).toHaveLength(0)
|
|
})
|
|
|
|
const role = await config.api.roles.save({
|
|
name: "Test",
|
|
inherits: "PUBLIC",
|
|
permissionId: BuiltinPermissionID.READ_ONLY,
|
|
version: "name",
|
|
})
|
|
|
|
user = await config.globalUser({
|
|
...user,
|
|
roles: {
|
|
[config.getProdAppId()]: role.name,
|
|
},
|
|
})
|
|
|
|
await config.withUser(user, async () => {
|
|
const apps = await config.api.application.fetch()
|
|
expect(apps).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
it("should only return apps a user has access to through a custom role on a group", async () => {
|
|
let user = await config.createUser({
|
|
builder: { global: false },
|
|
admin: { global: false },
|
|
})
|
|
|
|
await config.withUser(user, async () => {
|
|
const apps = await config.api.application.fetch()
|
|
expect(apps).toHaveLength(0)
|
|
})
|
|
|
|
const roleName = uuid.v4().replace(/-/g, "")
|
|
const role = await config.api.roles.save({
|
|
name: roleName,
|
|
inherits: "PUBLIC",
|
|
permissionId: BuiltinPermissionID.READ_ONLY,
|
|
version: "name",
|
|
})
|
|
|
|
const group = await config.createGroup(role._id!)
|
|
|
|
user = await config.globalUser({
|
|
...user,
|
|
userGroups: [group._id!],
|
|
})
|
|
|
|
await config.withUser(user, async () => {
|
|
const apps = await config.api.application.fetch()
|
|
expect(apps).toHaveLength(1)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("create", () => {
|
|
const checkScreenCount = async (expectedCount: number) => {
|
|
const res = await config.api.application.getDefinition(
|
|
config.getProdAppId()
|
|
)
|
|
expect(res.screens.length).toEqual(expectedCount)
|
|
}
|
|
|
|
const checkTableCount = async (expectedCount: number) => {
|
|
const tables = await config.api.table.fetch()
|
|
expect(tables.length).toEqual(expectedCount)
|
|
}
|
|
|
|
it("creates empty app with sample data", async () => {
|
|
const app = await config.api.application.create({ name: utils.newid() })
|
|
expect(app._id).toBeDefined()
|
|
expect(events.app.created).toHaveBeenCalledTimes(1)
|
|
|
|
// Ensure we created sample resources
|
|
await checkScreenCount(1)
|
|
await checkTableCount(5)
|
|
})
|
|
|
|
it("creates app from template", async () => {
|
|
nock("https://prod-budi-templates.s3-eu-west-1.amazonaws.com")
|
|
.get(`/templates/app/agency-client-portal.tar.gz`)
|
|
.replyWithFile(
|
|
200,
|
|
path.resolve(__dirname, "data", "agency-client-portal.tar.gz")
|
|
)
|
|
|
|
const app = await config.api.application.create({
|
|
name: utils.newid(),
|
|
useTemplate: "true",
|
|
templateKey: "app/agency-client-portal",
|
|
})
|
|
expect(app._id).toBeDefined()
|
|
expect(events.app.created).toHaveBeenCalledTimes(1)
|
|
expect(events.app.templateImported).toHaveBeenCalledTimes(1)
|
|
|
|
// Ensure we did not create sample data. This template includes exactly
|
|
// this many of each resource.
|
|
await checkScreenCount(1)
|
|
await checkTableCount(5)
|
|
})
|
|
|
|
it("creates app from file", async () => {
|
|
const app = await config.api.application.create({
|
|
name: utils.newid(),
|
|
useTemplate: "true",
|
|
fileToImport: "src/api/routes/tests/data/export.txt",
|
|
})
|
|
expect(app._id).toBeDefined()
|
|
expect(events.app.created).toHaveBeenCalledTimes(1)
|
|
expect(events.app.fileImported).toHaveBeenCalledTimes(1)
|
|
|
|
// Ensure we did not create sample data. This file includes exactly
|
|
// this many of each resource.
|
|
await checkScreenCount(1)
|
|
await checkTableCount(5)
|
|
})
|
|
|
|
it("should apply authorization to endpoint", async () => {
|
|
await checkBuilderEndpoint({
|
|
config,
|
|
method: "POST",
|
|
url: `/api/applications`,
|
|
body: { name: "My App" },
|
|
})
|
|
})
|
|
|
|
it("migrates navigation settings from old apps", async () => {
|
|
const app = await config.api.application.create({
|
|
name: utils.newid(),
|
|
useTemplate: "true",
|
|
fileToImport: "src/api/routes/tests/data/old-app.txt",
|
|
})
|
|
expect(app._id).toBeDefined()
|
|
expect(app.navigation).toBeDefined()
|
|
expect(app.navigation!.hideLogo).toBe(true)
|
|
expect(app.navigation!.title).toBe("Custom Title")
|
|
expect(app.navigation!.hideLogo).toBe(true)
|
|
expect(app.navigation!.navigation).toBe("Left")
|
|
expect(app.navigation!.navBackground).toBe(
|
|
"var(--spectrum-global-color-blue-600)"
|
|
)
|
|
expect(app.navigation!.navTextColor).toBe(
|
|
"var(--spectrum-global-color-gray-50)"
|
|
)
|
|
expect(events.app.created).toHaveBeenCalledTimes(1)
|
|
expect(events.app.fileImported).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it("should reject with a known name", async () => {
|
|
await config.api.application.create(
|
|
{ name: app.name },
|
|
{ body: { message: "App name is already in use." }, status: 400 }
|
|
)
|
|
})
|
|
|
|
it("should reject with a known url", async () => {
|
|
await config.api.application.create(
|
|
{ name: "made up", url: app!.url! },
|
|
{ body: { message: "App URL is already in use." }, status: 400 }
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("fetch", () => {
|
|
it("lists all applications", async () => {
|
|
const apps = await config.api.application.fetch({ status: AppStatus.DEV })
|
|
expect(apps.length).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
describe("fetchAppDefinition", () => {
|
|
it("should be able to get an apps definition", async () => {
|
|
const res = await config.api.application.getDefinition(app.appId)
|
|
expect(res.libraries.length).toEqual(1)
|
|
})
|
|
})
|
|
|
|
describe("fetchAppPackage", () => {
|
|
it("should be able to fetch the app package", async () => {
|
|
const res = await config.api.application.getAppPackage(app.appId)
|
|
expect(res.application).toBeDefined()
|
|
expect(res.application.appId).toEqual(config.getAppId())
|
|
})
|
|
})
|
|
|
|
describe("update", () => {
|
|
it("should be able to update the app package", async () => {
|
|
const updatedApp = await config.api.application.update(app.appId, {
|
|
name: "TEST_APP",
|
|
})
|
|
expect(updatedApp._rev).toBeDefined()
|
|
expect(events.app.updated).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
describe("publish", () => {
|
|
it("should publish app with dev app ID", async () => {
|
|
await config.api.application.publish(app.appId)
|
|
expect(events.app.published).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it("should publish app with prod app ID", async () => {
|
|
await config.api.application.publish(app.appId.replace("_dev", ""))
|
|
expect(events.app.published).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
describe("manage client library version", () => {
|
|
it("should be able to update the app client library version", async () => {
|
|
await config.api.application.updateClient(app.appId)
|
|
expect(events.app.versionUpdated).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it("should be able to revert the app client library version", async () => {
|
|
await config.api.application.updateClient(app.appId)
|
|
await config.api.application.revertClient(app.appId)
|
|
expect(events.app.versionReverted).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
describe("edited at", () => {
|
|
it("middleware should set updatedAt", async () => {
|
|
const app = await tk.withFreeze(
|
|
"2021-01-01",
|
|
async () => await config.api.application.create({ name: utils.newid() })
|
|
)
|
|
expect(app.updatedAt).toEqual("2021-01-01T00:00:00.000Z")
|
|
|
|
const updatedApp = await tk.withFreeze(
|
|
"2021-02-01",
|
|
async () =>
|
|
await config.api.application.update(app.appId, {
|
|
name: "UPDATED_NAME",
|
|
})
|
|
)
|
|
expect(updatedApp._rev).toBeDefined()
|
|
expect(updatedApp.updatedAt).toEqual("2021-02-01T00:00:00.000Z")
|
|
|
|
const fetchedApp = await config.api.application.get(app.appId)
|
|
expect(fetchedApp.updatedAt).toEqual("2021-02-01T00:00:00.000Z")
|
|
})
|
|
})
|
|
|
|
describe("sync", () => {
|
|
it("app should sync correctly", async () => {
|
|
const { message } = await config.api.application.sync(app.appId)
|
|
expect(message).toEqual("App sync completed successfully.")
|
|
})
|
|
|
|
it("app should not sync if production", async () => {
|
|
const { message } = await config.api.application.sync(
|
|
app.appId.replace("_dev", ""),
|
|
{ status: 400 }
|
|
)
|
|
|
|
expect(message).toEqual(
|
|
"This action cannot be performed for production apps"
|
|
)
|
|
})
|
|
|
|
it("app should not sync if sync is disabled", async () => {
|
|
env._set("DISABLE_AUTO_PROD_APP_SYNC", true)
|
|
const { message } = await config.api.application.sync(app.appId)
|
|
expect(message).toEqual(
|
|
"App sync disabled. You can reenable with the DISABLE_AUTO_PROD_APP_SYNC environment variable."
|
|
)
|
|
env._set("DISABLE_AUTO_PROD_APP_SYNC", false)
|
|
})
|
|
})
|
|
|
|
describe("unpublish", () => {
|
|
it("should unpublish app with dev app ID", async () => {
|
|
await config.api.application.unpublish(app.appId)
|
|
expect(events.app.unpublished).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it("should unpublish app with prod app ID", async () => {
|
|
await config.api.application.unpublish(app.appId.replace("_dev", ""))
|
|
expect(events.app.unpublished).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
describe("delete", () => {
|
|
it("should delete published app and dev apps with dev app ID", async () => {
|
|
const prodAppId = app.appId.replace("_dev", "")
|
|
nock("http://localhost:10000")
|
|
.delete(`/api/global/roles/${prodAppId}`)
|
|
.reply(200, {})
|
|
|
|
await config.api.application.delete(app.appId)
|
|
expect(events.app.deleted).toHaveBeenCalledTimes(1)
|
|
expect(events.app.unpublished).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it("should delete published app and dev app with prod app ID", async () => {
|
|
const prodAppId = app.appId.replace("_dev", "")
|
|
nock("http://localhost:10000")
|
|
.delete(`/api/global/roles/${prodAppId}`)
|
|
.reply(200, {})
|
|
|
|
await config.api.application.delete(prodAppId)
|
|
expect(events.app.deleted).toHaveBeenCalledTimes(1)
|
|
expect(events.app.unpublished).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
describe("POST /api/applications/:appId/duplicate", () => {
|
|
it("should duplicate an existing app", async () => {
|
|
const resp = await config.api.application.duplicateApp(
|
|
app.appId,
|
|
{
|
|
name: "to-dupe copy",
|
|
url: "/to-dupe-copy",
|
|
},
|
|
{
|
|
status: 200,
|
|
}
|
|
)
|
|
|
|
expect(events.app.duplicated).toHaveBeenCalled()
|
|
expect(resp.duplicateAppId).toBeDefined()
|
|
expect(resp.sourceAppId).toEqual(app.appId)
|
|
expect(resp.duplicateAppId).not.toEqual(app.appId)
|
|
})
|
|
|
|
it("should reject an unknown app id with a 404", async () => {
|
|
await config.api.application.duplicateApp(
|
|
structures.db.id(),
|
|
{
|
|
name: "to-dupe 123",
|
|
url: "/to-dupe-123",
|
|
},
|
|
{
|
|
status: 404,
|
|
}
|
|
)
|
|
})
|
|
|
|
it("should reject with a known name", async () => {
|
|
await config.api.application.duplicateApp(
|
|
app.appId,
|
|
{
|
|
name: app.name,
|
|
url: "/known-name",
|
|
},
|
|
{ body: { message: "App name is already in use." }, status: 400 }
|
|
)
|
|
expect(events.app.duplicated).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it("should reject with a known url", async () => {
|
|
await config.api.application.duplicateApp(
|
|
app.appId,
|
|
{
|
|
name: "this is fine",
|
|
url: app.url,
|
|
},
|
|
{ body: { message: "App URL is already in use." }, status: 400 }
|
|
)
|
|
expect(events.app.duplicated).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe("POST /api/applications/:appId/sync", () => {
|
|
it("should not sync automation logs", async () => {
|
|
const automation = await config.createAutomation()
|
|
await context.doInAppContext(app.appId, () =>
|
|
config.createAutomationLog(automation)
|
|
)
|
|
|
|
await config.api.application.sync(app.appId)
|
|
|
|
// does exist in prod
|
|
const prodLogs = await config.getAutomationLogs()
|
|
expect(prodLogs.data.length).toBe(1)
|
|
|
|
await config.api.application.unpublish(app.appId)
|
|
|
|
// doesn't exist in dev
|
|
const devLogs = await config.getAutomationLogs()
|
|
expect(devLogs.data.length).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe("POST /api/applications/:appId/sample", () => {
|
|
it("should be able to add sample data", async () => {
|
|
await config.api.application.addSampleData(config.getAppId())
|
|
for (let table of DEFAULT_TABLES) {
|
|
const res = await config.api.row.search(
|
|
table._id!,
|
|
{ query: {} },
|
|
{ status: 200 }
|
|
)
|
|
expect(res.rows.length).not.toEqual(0)
|
|
}
|
|
})
|
|
})
|
|
})
|