1
0
Fork 0
mirror of synced 2024-07-08 15:56:23 +12:00

More progress on modernising application tests.

This commit is contained in:
Sam Rose 2024-02-26 11:57:56 +00:00
parent b2c4f04aa6
commit b9600d8330
No known key found for this signature in database
6 changed files with 179 additions and 134 deletions

View file

@ -6,7 +6,7 @@ import { Plugin } from "@budibase/types"
// URLS // URLS
export function enrichPluginURLs(plugins: Plugin[]) { export function enrichPluginURLs(plugins: Plugin[]): Plugin[] {
if (!plugins || !plugins.length) { if (!plugins || !plugins.length) {
return [] return []
} }

View file

@ -3,12 +3,12 @@ set -e
if [[ -n $CI ]] if [[ -n $CI ]]
then then
export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot" export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot $NODE_OPTIONS"
echo "jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail $@" echo "jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail $@"
jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail $@ jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail $@
else else
# --maxWorkers performs better in development # --maxWorkers performs better in development
export NODE_OPTIONS="--no-node-snapshot" export NODE_OPTIONS="--no-node-snapshot $NODE_OPTIONS"
echo "jest --coverage --maxWorkers=2 --forceExit $@" echo "jest --coverage --maxWorkers=2 --forceExit $@"
jest --coverage --maxWorkers=2 --forceExit $@ jest --coverage --maxWorkers=2 --forceExit $@
fi fi

View file

@ -48,6 +48,8 @@ import {
Screen, Screen,
UserCtx, UserCtx,
CreateAppRequest, CreateAppRequest,
FetchAppDefinitionResponse,
type FetchAppPackageResponse,
} from "@budibase/types" } from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
import sdk from "../../sdk" import sdk from "../../sdk"
@ -59,23 +61,23 @@ import * as appMigrations from "../../appMigrations"
async function getLayouts() { async function getLayouts() {
const db = context.getAppDB() const db = context.getAppDB()
return ( return (
await db.allDocs( await db.allDocs<Layout>(
getLayoutParams(null, { getLayoutParams(null, {
include_docs: true, include_docs: true,
}) })
) )
).rows.map((row: any) => row.doc) ).rows.map(row => row.doc!)
} }
async function getScreens() { async function getScreens() {
const db = context.getAppDB() const db = context.getAppDB()
return ( return (
await db.allDocs( await db.allDocs<Screen>(
getScreenParams(null, { getScreenParams(null, {
include_docs: true, include_docs: true,
}) })
) )
).rows.map((row: any) => row.doc) ).rows.map(row => row.doc!)
} }
function getUserRoleId(ctx: UserCtx) { function getUserRoleId(ctx: UserCtx) {
@ -175,14 +177,16 @@ export const addSampleData = async (ctx: UserCtx) => {
ctx.status = 200 ctx.status = 200
} }
export async function fetch(ctx: UserCtx) { export async function fetch(ctx: UserCtx<null, App[]>) {
ctx.body = await sdk.applications.fetch( ctx.body = await sdk.applications.fetch(
ctx.query.status as AppStatus, ctx.query.status as AppStatus,
ctx.user ctx.user
) )
} }
export async function fetchAppDefinition(ctx: UserCtx) { export async function fetchAppDefinition(
ctx: UserCtx<null, FetchAppDefinitionResponse>
) {
const layouts = await getLayouts() const layouts = await getLayouts()
const userRoleId = getUserRoleId(ctx) const userRoleId = getUserRoleId(ctx)
const accessController = new roles.AccessController() const accessController = new roles.AccessController()
@ -197,17 +201,19 @@ export async function fetchAppDefinition(ctx: UserCtx) {
} }
} }
export async function fetchAppPackage(ctx: UserCtx) { export async function fetchAppPackage(
ctx: UserCtx<null, FetchAppPackageResponse>
) {
const db = context.getAppDB() const db = context.getAppDB()
const appId = context.getAppId() const appId = context.getAppId()
let application = await db.get<any>(DocumentType.APP_METADATA) let application = await db.get<App>(DocumentType.APP_METADATA)
const layouts = await getLayouts() const layouts = await getLayouts()
let screens = await getScreens() let screens = await getScreens()
const license = await licensing.cache.getCachedLicense() const license = await licensing.cache.getCachedLicense()
// Enrich plugin URLs // Enrich plugin URLs
application.usedPlugins = objectStore.enrichPluginURLs( application.usedPlugins = objectStore.enrichPluginURLs(
application.usedPlugins application.usedPlugins || []
) )
// Only filter screens if the user is not a builder // Only filter screens if the user is not a builder
@ -425,7 +431,9 @@ export async function create(ctx: UserCtx) {
// This endpoint currently operates as a PATCH rather than a PUT // This endpoint currently operates as a PATCH rather than a PUT
// Thus name and url fields are handled only if present // Thus name and url fields are handled only if present
export async function update(ctx: UserCtx) { export async function update(
ctx: UserCtx<{ name?: string; url?: string }, App>
) {
const apps = (await dbCore.getAllApps({ dev: true })) as App[] const apps = (await dbCore.getAllApps({ dev: true })) as App[]
// validation // validation
const name = ctx.request.body.name, const name = ctx.request.body.name,
@ -498,7 +506,7 @@ export async function revertClient(ctx: UserCtx) {
const revertedToVersion = application.revertableVersion const revertedToVersion = application.revertableVersion
const appPackageUpdates = { const appPackageUpdates = {
version: revertedToVersion, version: revertedToVersion,
revertableVersion: null, revertableVersion: undefined,
} }
const app = await updateAppPackage(appPackageUpdates, ctx.params.appId) const app = await updateAppPackage(appPackageUpdates, ctx.params.appId)
await events.app.versionReverted(app, currentVersion, revertedToVersion) await events.app.versionReverted(app, currentVersion, revertedToVersion)
@ -618,12 +626,15 @@ export async function importToApp(ctx: UserCtx) {
ctx.body = { message: "app updated" } ctx.body = { message: "app updated" }
} }
export async function updateAppPackage(appPackage: any, appId: any) { export async function updateAppPackage(
appPackage: Partial<App>,
appId: string
) {
return context.doInAppContext(appId, async () => { return context.doInAppContext(appId, async () => {
const db = context.getAppDB() const db = context.getAppDB()
const application = await db.get<App>(DocumentType.APP_METADATA) const application = await db.get<App>(DocumentType.APP_METADATA)
const newAppPackage = { ...application, ...appPackage } const newAppPackage: App = { ...application, ...appPackage }
if (appPackage._rev !== application._rev) { if (appPackage._rev !== application._rev) {
newAppPackage._rev = application._rev newAppPackage._rev = application._rev
} }

View file

@ -11,25 +11,27 @@ jest.mock("../../../utilities/redis", () => ({
checkDebounce: jest.fn(), checkDebounce: jest.fn(),
shutdown: jest.fn(), shutdown: jest.fn(),
})) }))
import { clearAllApps, checkBuilderEndpoint } from "./utilities/TestFunctions" import { checkBuilderEndpoint } from "./utilities/TestFunctions"
import * as setup from "./utilities" import * as setup from "./utilities"
import { AppStatus } from "../../../db/utils" import { AppStatus } from "../../../db/utils"
import { events, utils, context } from "@budibase/backend-core" import { events, utils, context } from "@budibase/backend-core"
import env from "../../../environment" import env from "../../../environment"
import type { App } from "@budibase/types"
jest.setTimeout(15000) jest.setTimeout(150000000)
describe("/applications", () => { describe("/applications", () => {
let request = setup.getRequest() let request = setup.getRequest()
let config = setup.getConfig() let config = setup.getConfig()
let app: App
afterAll(setup.afterAll) afterAll(setup.afterAll)
beforeAll(async () => await config.init())
beforeAll(async () => {
await config.init()
})
beforeEach(async () => { beforeEach(async () => {
app = await config.api.application.create({ name: utils.newid() })
const deployment = await config.api.application.publish(app.appId)
expect(deployment.status).toBe("SUCCESS")
jest.clearAllMocks() jest.clearAllMocks()
}) })
@ -74,7 +76,7 @@ describe("/applications", () => {
it("migrates navigation settings from old apps", async () => { it("migrates navigation settings from old apps", async () => {
const app = await config.api.application.create({ const app = await config.api.application.create({
name: "Old App", name: utils.newid(),
useTemplate: "true", useTemplate: "true",
templateFile: "src/api/routes/tests/data/old-app.txt", templateFile: "src/api/routes/tests/data/old-app.txt",
}) })
@ -96,77 +98,45 @@ describe("/applications", () => {
}) })
describe("fetch", () => { describe("fetch", () => {
beforeEach(async () => {
// Clean all apps but the onde from config
await clearAllApps(config.getTenantId(), [config.getAppId()!])
})
it("lists all applications", async () => { it("lists all applications", async () => {
await config.createApp("app1")
await config.createApp("app2")
const apps = await config.api.application.fetch({ status: AppStatus.DEV }) const apps = await config.api.application.fetch({ status: AppStatus.DEV })
expect(apps.length).toBeGreaterThan(0)
// two created apps + the inited app
expect(apps.length).toBe(3)
}) })
}) })
describe("fetchAppDefinition", () => { describe("fetchAppDefinition", () => {
it("should be able to get an apps definition", async () => { it("should be able to get an apps definition", async () => {
const res = await request const res = await config.api.application.getDefinition(app.appId)
.get(`/api/applications/${config.getAppId()}/definition`) expect(res.libraries.length).toEqual(1)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.libraries.length).toEqual(1)
}) })
}) })
describe("fetchAppPackage", () => { describe("fetchAppPackage", () => {
it("should be able to fetch the app package", async () => { it("should be able to fetch the app package", async () => {
const res = await request const res = await config.api.application.getAppPackage(app.appId)
.get(`/api/applications/${config.getAppId()}/appPackage`) expect(res.application).toBeDefined()
.set(config.defaultHeaders()) expect(res.application.appId).toEqual(config.getAppId())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.application).toBeDefined()
expect(res.body.application.appId).toEqual(config.getAppId())
}) })
}) })
describe("update", () => { describe("update", () => {
it("should be able to update the app package", async () => { it("should be able to update the app package", async () => {
const res = await request const updatedApp = await config.api.application.update(app.appId, {
.put(`/api/applications/${config.getAppId()}`) name: "TEST_APP",
.send({ })
name: "TEST_APP", expect(updatedApp._rev).toBeDefined()
})
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body._rev).toBeDefined()
expect(events.app.updated).toBeCalledTimes(1) expect(events.app.updated).toBeCalledTimes(1)
}) })
}) })
describe("publish", () => { describe("publish", () => {
it("should publish app with dev app ID", async () => { it("should publish app with dev app ID", async () => {
const appId = config.getAppId() await config.api.application.publish(app.appId)
await request
.post(`/api/applications/${appId}/publish`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(events.app.published).toBeCalledTimes(1) expect(events.app.published).toBeCalledTimes(1)
}) })
it("should publish app with prod app ID", async () => { it("should publish app with prod app ID", async () => {
const appId = config.getProdAppId() await config.api.application.publish(app.appId.replace("_dev", ""))
await request
.post(`/api/applications/${appId}/publish`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(events.app.published).toBeCalledTimes(1) expect(events.app.published).toBeCalledTimes(1)
}) })
}) })
@ -222,33 +192,25 @@ describe("/applications", () => {
describe("sync", () => { describe("sync", () => {
it("app should sync correctly", async () => { it("app should sync correctly", async () => {
const res = await request const { message } = await config.api.application.sync(app.appId)
.post(`/api/applications/${config.getAppId()}/sync`) expect(message).toEqual("App sync completed successfully.")
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.message).toEqual("App sync completed successfully.")
}) })
it("app should not sync if production", async () => { it("app should not sync if production", async () => {
const res = await request const { message } = await config.api.application.sync(
.post(`/api/applications/app_123456/sync`) app.appId.replace("_dev", ""),
.set(config.defaultHeaders()) { statusCode: 400 }
.expect("Content-Type", /json/) )
.expect(400)
expect(res.body.message).toEqual( expect(message).toEqual(
"This action cannot be performed for production apps" "This action cannot be performed for production apps"
) )
}) })
it("app should not sync if sync is disabled", async () => { it("app should not sync if sync is disabled", async () => {
env._set("DISABLE_AUTO_PROD_APP_SYNC", true) env._set("DISABLE_AUTO_PROD_APP_SYNC", true)
const res = await request const { message } = await config.api.application.sync(app.appId)
.post(`/api/applications/${config.getAppId()}/sync`) expect(message).toEqual(
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.message).toEqual(
"App sync disabled. You can reenable with the DISABLE_AUTO_PROD_APP_SYNC environment variable." "App sync disabled. You can reenable with the DISABLE_AUTO_PROD_APP_SYNC environment variable."
) )
env._set("DISABLE_AUTO_PROD_APP_SYNC", false) env._set("DISABLE_AUTO_PROD_APP_SYNC", false)
@ -256,51 +218,26 @@ describe("/applications", () => {
}) })
describe("unpublish", () => { describe("unpublish", () => {
beforeEach(async () => {
// We want to republish as the unpublish will delete the prod app
await config.publish()
})
it("should unpublish app with dev app ID", async () => { it("should unpublish app with dev app ID", async () => {
const appId = config.getAppId() await config.api.application.unpublish(app.appId)
await request
.post(`/api/applications/${appId}/unpublish`)
.set(config.defaultHeaders())
.expect(204)
expect(events.app.unpublished).toBeCalledTimes(1) expect(events.app.unpublished).toBeCalledTimes(1)
}) })
it("should unpublish app with prod app ID", async () => { it("should unpublish app with prod app ID", async () => {
const appId = config.getProdAppId() await config.api.application.unpublish(app.appId.replace("_dev", ""))
await request
.post(`/api/applications/${appId}/unpublish`)
.set(config.defaultHeaders())
.expect(204)
expect(events.app.unpublished).toBeCalledTimes(1) expect(events.app.unpublished).toBeCalledTimes(1)
}) })
}) })
describe("delete", () => { describe("delete", () => {
it("should delete published app and dev apps with dev app ID", async () => { it("should delete published app and dev apps with dev app ID", async () => {
await config.createApp("to-delete") await config.api.application.delete(app.appId)
const appId = config.getAppId()
await request
.delete(`/api/applications/${appId}`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(events.app.deleted).toBeCalledTimes(1) expect(events.app.deleted).toBeCalledTimes(1)
expect(events.app.unpublished).toBeCalledTimes(1) expect(events.app.unpublished).toBeCalledTimes(1)
}) })
it("should delete published app and dev app with prod app ID", async () => { it("should delete published app and dev app with prod app ID", async () => {
await config.createApp("to-delete") await config.api.application.delete(app.appId.replace("_dev", ""))
const appId = config.getProdAppId()
await request
.delete(`/api/applications/${appId}`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(events.app.deleted).toBeCalledTimes(1) expect(events.app.deleted).toBeCalledTimes(1)
expect(events.app.unpublished).toBeCalledTimes(1) expect(events.app.unpublished).toBeCalledTimes(1)
}) })
@ -308,28 +245,18 @@ describe("/applications", () => {
describe("POST /api/applications/:appId/sync", () => { describe("POST /api/applications/:appId/sync", () => {
it("should not sync automation logs", async () => { it("should not sync automation logs", async () => {
// setup the apps
await config.createApp("testing-auto-logs")
const automation = await config.createAutomation() const automation = await config.createAutomation()
await config.publish() await context.doInAppContext(app.appId, () =>
await context.doInAppContext(config.getProdAppId(), () => { config.createAutomationLog(automation)
return config.createAutomationLog(automation) )
})
// do the sync await config.api.application.sync(app.appId)
const appId = config.getAppId()
await request
.post(`/api/applications/${appId}/sync`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
// does exist in prod // does exist in prod
const prodLogs = await config.getAutomationLogs() const prodLogs = await config.getAutomationLogs()
expect(prodLogs.data.length).toBe(1) expect(prodLogs.data.length).toBe(1)
// delete prod app so we revert to dev log search await config.api.application.unpublish(app.appId)
await config.unpublish()
// doesn't exist in dev // doesn't exist in dev
const devLogs = await config.getAutomationLogs() const devLogs = await config.getAutomationLogs()

View file

@ -1,9 +1,14 @@
import { Response } from "supertest" import { Response } from "supertest"
import { App, type CreateAppRequest } from "@budibase/types" import {
App,
type CreateAppRequest,
type FetchAppDefinitionResponse,
type FetchAppPackageResponse,
} from "@budibase/types"
import TestConfiguration from "../TestConfiguration" import TestConfiguration from "../TestConfiguration"
import { TestAPI } from "./base" import { TestAPI } from "./base"
import { AppStatus } from "../../../db/utils" import { AppStatus } from "../../../db/utils"
import { dbObjectAsPojo } from "oracledb" import { constants } from "@budibase/backend-core"
export class ApplicationAPI extends TestAPI { export class ApplicationAPI extends TestAPI {
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {
@ -27,12 +32,55 @@ export class ApplicationAPI extends TestAPI {
const result = await request const result = await request
if (result.statusCode !== 200) { if (result.statusCode !== 200) {
fail(JSON.stringify(result.body)) throw new Error(JSON.stringify(result.body))
} }
return result.body as App return result.body as App
} }
delete = async (appId: string): Promise<void> => {
await this.request
.delete(`/api/applications/${appId}`)
.set(this.config.defaultHeaders())
.expect(200)
}
publish = async (
appId: string
): Promise<{ _id: string; status: string; appUrl: string }> => {
// While the publsih endpoint does take an :appId parameter, it doesn't
// use it. It uses the appId from the context.
let headers = {
...this.config.defaultHeaders(),
[constants.Header.APP_ID]: appId,
}
const result = await this.request
.post(`/api/applications/${appId}/publish`)
.set(headers)
.expect("Content-Type", /json/)
.expect(200)
return result.body as { _id: string; status: string; appUrl: string }
}
unpublish = async (appId: string): Promise<void> => {
await this.request
.post(`/api/applications/${appId}/unpublish`)
.set(this.config.defaultHeaders())
.expect(204)
}
sync = async (
appId: string,
{ statusCode }: { statusCode: number } = { statusCode: 200 }
): Promise<{ message: string }> => {
const result = await this.request
.post(`/api/applications/${appId}/sync`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(statusCode)
return result.body
}
getRaw = async (appId: string): Promise<Response> => { getRaw = async (appId: string): Promise<Response> => {
const result = await this.request const result = await this.request
.get(`/api/applications/${appId}/appPackage`) .get(`/api/applications/${appId}/appPackage`)
@ -47,6 +95,48 @@ export class ApplicationAPI extends TestAPI {
return result.body.application as App return result.body.application as App
} }
getDefinition = async (
appId: string
): Promise<FetchAppDefinitionResponse> => {
const result = await this.request
.get(`/api/applications/${appId}/definition`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
return result.body as FetchAppDefinitionResponse
}
getAppPackage = async (appId: string): Promise<FetchAppPackageResponse> => {
const result = await this.request
.get(`/api/applications/${appId}/appPackage`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
return result.body
}
update = async (
appId: string,
app: { name?: string; url?: string }
): Promise<App> => {
const request = this.request
.put(`/api/applications/${appId}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
for (const key of Object.keys(app)) {
request.field(key, (app as any)[key])
}
const result = await request
if (result.statusCode !== 200) {
throw new Error(JSON.stringify(result.body))
}
return result.body as App
}
fetch = async ({ status }: { status?: AppStatus } = {}): Promise<App[]> => { fetch = async ({ status }: { status?: AppStatus } = {}): Promise<App[]> => {
let query = [] let query = []
if (status) { if (status) {

View file

@ -1,5 +1,5 @@
import { User, Document } from "../" import { User, Document, Layout, Screen, Plugin } from "../"
import { SocketSession } from "../../sdk" import { SocketSession, PlanType } from "../../sdk"
export type AppMetadataErrors = { [key: string]: string[] } export type AppMetadataErrors = { [key: string]: string[] }
@ -24,6 +24,8 @@ export interface App extends Document {
icon?: AppIcon icon?: AppIcon
features?: AppFeatures features?: AppFeatures
automations?: AutomationSettings automations?: AutomationSettings
usedPlugins?: Plugin[]
upgradableVersion?: string
} }
export interface AppInstance { export interface AppInstance {
@ -85,3 +87,18 @@ export interface CreateAppRequest {
encryptionPassword?: string encryptionPassword?: string
templateString?: string templateString?: string
} }
export interface FetchAppDefinitionResponse {
layouts: Layout[]
screens: Screen[]
libraries: string[]
}
export interface FetchAppPackageResponse {
application: App
licenseType: PlanType
screens: Screen[]
layouts: Layout[]
clientLibPath: string
hasLock: boolean
}