@budibase/server
Version:
Budibase Web Server
1,086 lines (947 loc) • 34.6 kB
text/typescript
import { DEFAULT_TABLES } from "../../../db/defaultData/datasource_bb_default"
import { setEnv, withEnv } from "../../../environment"
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
import * as setup from "./utilities"
import { AppStatus } from "../../../db/utils"
import {
events,
utils,
context,
roles,
features,
Header,
db,
} from "@budibase/backend-core"
import env from "../../../environment"
import {
type App,
BuiltinPermissionID,
Screen,
WorkspaceApp,
} 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"
import {
basicScreen,
customScreen,
basicTable,
basicQuery,
} from "../../../tests/utilities/structures"
import { createAutomationBuilder } from "../../../automations/tests/utilities/AutomationTestBuilder"
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()
})
async function createNewApp() {
app = await config.newTenant()
await config.publish()
}
beforeEach(async () => {
await createNewApp()
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("fetchClientApps", () => {
it("should return apps with default workspace app when published", async () => {
const response = await config.api.application.fetchClientApps()
expect(response.apps).toHaveLength(1)
expect(response.apps[0]).toEqual(
expect.objectContaining({
prodId: config.getProdAppId(),
url: app.url,
})
)
})
it("should return multiple apps when published app with workspace apps exists", async () => {
await config.api.workspaceApp.create(
structures.workspaceApps.createRequest({
name: "Test Workspace App",
url: "/testapp",
})
)
await config.publish()
const response = await config.api.application.fetchClientApps()
expect(response.apps.length).toBe(2)
const testApp = response.apps.find(a => a.name === "Test Workspace App")
expect(testApp).toEqual(
expect.objectContaining({
prodId: config.getProdAppId(),
name: "Test Workspace App",
url: `${app.url}/testapp`,
})
)
})
it("should handle creating multiple workspace apps", async () => {
const { workspaceApp: workspaceApp1 } =
await config.api.workspaceApp.create(
structures.workspaceApps.createRequest({
name: "App One",
url: "/appone",
})
)
const { workspaceApp: workspaceApp2 } =
await config.api.workspaceApp.create(
structures.workspaceApps.createRequest({
name: "App Two",
url: "/apptwo",
})
)
const app = await config.publish()
const response = await config.api.application.fetchClientApps()
expect(response.apps.length).toBe(3)
expect(response.apps).toEqual(
expect.arrayContaining([
{
appId: expect.stringMatching(
new RegExp(`^${app.appId}_workspace_app_.+`)
),
name: app.name,
prodId: app.appId,
updatedAt: app.updatedAt,
url: app.url,
},
{
appId: `${app.appId}_${workspaceApp1._id}`,
name: "App One",
prodId: config.getProdAppId(),
updatedAt: app.updatedAt,
url: `${app.url}/appone`,
},
{
appId: `${app.appId}_${workspaceApp2._id}`,
name: "App Two",
prodId: config.getProdAppId(),
updatedAt: app.updatedAt,
url: `${app.url}/apptwo`,
},
])
)
})
it("should return apps from multiple published workspaces", async () => {
const { workspaceApp: app1Workspace1 } =
await config.api.workspaceApp.create(
structures.workspaceApps.createRequest({
name: "App One",
url: "/appone",
})
)
app = await config.publish()
const secondApp = await tk.withFreeze(new Date(), async () => {
// Create second app
let secondApp = await config.api.application.create({
name: "Second App",
})
await config.api.workspaceApp.create(
structures.workspaceApps.createRequest({
name: "App Two",
url: "/apptwo",
})
)
await config.api.application.publish(secondApp.appId)
return secondApp
})
const response = await config.api.application.fetchClientApps()
expect(response.apps).toHaveLength(3)
expect(response.apps).toEqual(
expect.arrayContaining([
{
appId: expect.stringMatching(
new RegExp(`^${app.appId}_workspace_app_.+`)
),
name: app.name,
prodId: app.appId,
updatedAt: app.updatedAt,
url: app.url,
},
{
appId: `${app.appId}_${app1Workspace1._id}`,
name: "App One",
prodId: config.getProdAppId(),
updatedAt: app.updatedAt,
url: `${app.url}/appone`,
},
{
appId: expect.stringMatching(
new RegExp(
`^${db.getProdAppID(secondApp.appId)}_workspace_app_.+`
)
),
name: secondApp.name,
prodId: db.getProdAppID(secondApp.appId),
updatedAt: secondApp.updatedAt,
url: secondApp.url,
},
])
)
})
it("should not return unpublished apps", async () => {
const { workspaceApp: app1Workspace1 } =
await config.api.workspaceApp.create(
structures.workspaceApps.createRequest({
name: "App One",
url: "/appone",
})
)
app = await config.publish()
// Non published workspace
await config.api.workspaceApp.create(
structures.workspaceApps.createRequest({
name: "Another app",
url: "/other",
})
)
// Create second app
const secondApp = await tk.withFreeze(new Date(), async () => {
const secondApp = await config.api.application.create({
name: "Second App",
})
await config.api.application.publish(secondApp.appId)
return secondApp
})
// Unpublished app
await config.api.application.create({
name: "Third App",
})
const response = await config.api.application.fetchClientApps()
expect(response.apps).toHaveLength(3)
expect(response.apps).toEqual(
expect.arrayContaining([
{
appId: expect.stringMatching(
new RegExp(`^${app.appId}_workspace_app_.+`)
),
name: app.name,
prodId: app.appId,
updatedAt: app.updatedAt,
url: app.url,
},
{
appId: `${app.appId}_${app1Workspace1._id}`,
name: "App One",
prodId: config.getProdAppId(),
updatedAt: app.updatedAt,
url: `${app.url}/appone`,
},
{
appId: expect.stringMatching(
new RegExp(
`^${db.getProdAppID(secondApp.appId)}_workspace_app_.+`
)
),
name: secondApp.name,
prodId: db.getProdAppID(secondApp.appId),
updatedAt: secondApp.updatedAt,
url: secondApp.url,
},
])
)
})
})
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())
})
it("should retrieve all the screens for builder calls", async () => {
await config.api.screen.save(basicScreen())
await config.api.screen.save(basicScreen())
await config.api.screen.save(basicScreen())
const res = await config.api.application.getAppPackage(app.appId)
expect(res.screens).toHaveLength(4) // Default one + 3 created
})
it("should retrieve all the screens for public calls", async () => {
const [_screen1, screen2, _screen3] = await Promise.all([
config.api.screen.save(basicScreen()),
config.api.screen.save(
customScreen({ roleId: roles.BUILTIN_ROLE_IDS.PUBLIC, route: "/" })
),
config.api.screen.save(basicScreen()),
])
await config.publish()
const res = await config.api.application.getAppPackage(app.appId, {
publicUser: true,
})
expect(res.screens).toHaveLength(1)
expect(res.screens).toContainEqual(
expect.objectContaining({ _id: screen2._id })
)
})
describe("workspace apps", () => {
describe("on", () => {
let featureCleanup: () => void
beforeAll(() => {
featureCleanup = features.testutils.setFeatureFlags("*", {
WORKSPACE_APPS: true,
})
})
afterAll(() => {
featureCleanup()
})
it("should retrieve all the screens for builder calls", async () => {
await config.api.workspaceApp.create(
structures.workspaceApps.createRequest()
)
const res = await config.api.application.getAppPackage(app.appId)
expect(res.screens).toHaveLength(1)
})
describe("should retrieve only the screens for a given workspace app", () => {
let workspaceAppInfo: {
workspaceApp: WorkspaceApp
screens: Screen[]
}[]
beforeEach(async () => {
const appPackage = await config.api.application.getAppPackage(
app.appId
)
const [defaultWorkspaceApp] = (
await config.api.workspaceApp.fetch()
).workspaceApps
const { workspaceApp: workspaceApp1 } =
await config.api.workspaceApp.create(
structures.workspaceApps.createRequest({
url: "/app1",
})
)
const { workspaceApp: workspaceApp2 } =
await config.api.workspaceApp.create(
structures.workspaceApps.createRequest({
url: "/app2",
})
)
workspaceAppInfo = []
async function createScreens(
workspaceApp: WorkspaceApp,
routes: string[]
) {
const screens = []
for (const route of routes) {
const screen = await config.api.screen.save({
...basicScreen(route),
workspaceAppId: workspaceApp._id!,
})
screens.push(screen)
}
workspaceAppInfo.push({
workspaceApp,
screens,
})
}
await createScreens(defaultWorkspaceApp, ["/page-1"])
await createScreens(workspaceApp1, ["/", "/page-1", "/page-2"])
await createScreens(workspaceApp2, ["/", "/page-1"])
workspaceAppInfo[0].screens.unshift(...appPackage.screens)
})
it.each(["", "/"])(
"should retrieve only the screens for a the workspace all with empty prefix",
async closingChar => {
await config.withHeaders(
{
referer: `http://localhost:10000/${config.appId}${closingChar}`,
},
async () => {
const res = await config.api.application.getAppPackage(
app.appId,
{
headers: {
[Header.TYPE]: "client",
},
}
)
expect(res.screens).toHaveLength(2)
expect(res.screens).toEqual(
expect.arrayContaining(
workspaceAppInfo[0].screens.map(s =>
expect.objectContaining({ _id: s._id })
)
)
)
}
)
}
)
it.each(["", "/"])(
"should retrieve only the screens for a the workspace from the base url of it",
async closingChar => {
const { url } = workspaceAppInfo[1].workspaceApp
await config.withHeaders(
{
referer: `http://localhost:10000/${config.appId}${url}${closingChar}`,
},
async () => {
const res = await config.api.application.getAppPackage(
app.appId,
{
headers: {
[Header.TYPE]: "client",
},
}
)
expect(res.screens).toHaveLength(3)
expect(res.screens).toEqual(
expect.arrayContaining(
workspaceAppInfo[1].screens.map(s =>
expect.objectContaining({ _id: s._id })
)
)
)
}
)
}
)
it("should retrieve only the screens for a the workspace from a page url", async () => {
const { url } = workspaceAppInfo[1].workspaceApp
await config.withHeaders(
{
referer: `http://localhost:10000/${config.appId}${url}#page-1`,
},
async () => {
const res = await config.api.application.getAppPackage(
app.appId,
{
headers: {
[Header.TYPE]: "client",
},
}
)
expect(res.screens).toHaveLength(3)
expect(res.screens).toEqual(
expect.arrayContaining(
workspaceAppInfo[1].screens.map(s =>
expect.objectContaining({ _id: s._id })
)
)
)
}
)
})
it("should retrieve only the screens for a the workspace for prod app", async () => {
await config.publish()
await config.withProdApp(() =>
config.withHeaders(
{
referer: `http://localhost:10000/app${config.prodApp?.url}`,
},
async () => {
const res = await config.api.application.getAppPackage(
app.appId,
{
headers: {
[Header.TYPE]: "client",
},
}
)
expect(res.screens).toHaveLength(2)
expect(res.screens).toEqual(
expect.arrayContaining(
workspaceAppInfo[0].screens.map(s =>
expect.objectContaining({ _id: s._id })
)
)
)
}
)
)
})
})
})
describe("off", () => {
it("should retrieve all the screens", async () => {
const res = await config.api.application.getAppPackage(app.appId)
expect(res.screens).toHaveLength(1)
})
})
})
})
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)
})
// API to publish filtered resources currently disabled, skip test while not needed
it.skip("should publish app with filtered resources, filtering by automation", async () => {
// create data resources
const table = await config.createTable(basicTable())
// all internal resources are published if any used
const tableUnused = await config.createTable(basicTable())
const datasource = await config.createDatasource()
const query = await config.createQuery(basicQuery(datasource._id!))
// automation to publish
const { automation } = await createAutomationBuilder(config)
.onRowSaved({ tableId: table._id! })
.executeQuery({ query: { queryId: query._id! } })
.save()
const rowAction = await config.api.rowAction.save(table._id!, {
name: "test",
})
// create some assets that won't be published
const unpublishedDatasource = await config.createDatasource()
const { automation: unpublishedAutomation } =
await createAutomationBuilder(config)
.onRowSaved({ tableId: table._id! })
.save()
await config.api.application.filteredPublish(app.appId, {
automationIds: [automation._id!],
})
await config.withProdApp(async () => {
const { automations } = await config.api.automation.fetch()
expect(
automations.find(auto => auto._id === automation._id!)
).toBeDefined()
expect(
automations.find(auto => auto._id === unpublishedAutomation._id!)
).toBeUndefined()
// row action automations should be published if row action published
expect(
automations.find(auto => auto._id === rowAction.automationId)
).toBeDefined()
const datasources = await config.api.datasource.fetch()
expect(datasources.find(ds => ds._id === datasource._id!)).toBeDefined()
expect(
datasources.find(ds => ds._id === unpublishedDatasource._id!)
).toBeUndefined()
const tables = await config.api.table.fetch()
expect(tables.find(tbl => tbl._id === table._id)).toBeDefined()
expect(tables.find(tbl => tbl._id === tableUnused._id)).toBeDefined()
const { actions } = await config.api.rowAction.find(table._id!)
expect(
Object.values(actions).find(action => action.id === rowAction.id)
).toBeDefined()
})
})
// API to publish filtered resources currently disabled, skip test while not needed
it.skip("should publish app with filtered resources, filtering by workspace app", async () => {
// create two screens with different workspaceAppIds
const { workspaceApp: workspaceApp1 } =
await config.api.workspaceApp.create(
structures.workspaceApps.createRequest()
)
const { workspaceApp: workspaceApp2 } =
await config.api.workspaceApp.create(
structures.workspaceApps.createRequest()
)
const publishedScreen = await config.api.screen.save({
...basicScreen("/published-screen"),
workspaceAppId: workspaceApp1._id,
name: "published-screen",
})
const unpublishedScreen = await config.api.screen.save({
...basicScreen("/unpublished-screen"),
workspaceAppId: workspaceApp2._id,
name: "unpublished-screen",
})
await config.api.application.filteredPublish(app.appId, {
workspaceAppIds: [workspaceApp1._id],
})
await config.withProdApp(async () => {
const screens = await config.api.screen.list()
// published screen should be included
expect(
screens.find(screen => screen._id === publishedScreen._id)
).toBeDefined()
// unpublished screen should not be included
expect(
screens.find(screen => screen._id === unpublishedScreen._id)
).toBeUndefined()
})
})
})
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.withApp(app, () =>
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)
})
it("should remove MIGRATING_APP header if present during deletion", async () => {
const appMigrationsModule = await import(
"../../../appMigrations/migrations"
)
const migrationMock = jest.fn()
appMigrationsModule.MIGRATIONS.push({
id: "99999999999999_test_deletion",
func: migrationMock,
})
const prodAppId = app.appId.replace("_dev", "")
nock("http://localhost:10000")
.delete(`/api/global/roles/${prodAppId}`)
.reply(200, {})
expect(migrationMock).not.toHaveBeenCalled()
await withEnv(
{
SYNC_MIGRATION_CHECKS_MS: 1000,
},
() =>
config.api.application.delete(app.appId, {
headersNotPresent: [Header.MIGRATING_APP],
})
)
expect(migrationMock).toHaveBeenCalledTimes(2)
expect(events.app.deleted).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)
}
})
})
})