2024-07-10 13:24:25 +02:00
|
|
|
import _ from "lodash"
|
2024-07-11 10:04:25 +02:00
|
|
|
import tk from "timekeeper"
|
|
|
|
|
2024-07-18 17:11:11 +02:00
|
|
|
import {
|
|
|
|
CreateRowActionRequest,
|
|
|
|
DocumentType,
|
2024-08-26 13:42:20 +02:00
|
|
|
PermissionLevel,
|
2024-08-26 15:17:18 +02:00
|
|
|
Row,
|
2024-07-18 17:11:11 +02:00
|
|
|
RowActionResponse,
|
|
|
|
} from "@budibase/types"
|
2024-07-10 13:24:25 +02:00
|
|
|
import * as setup from "./utilities"
|
|
|
|
import { generator } from "@budibase/backend-core/tests"
|
2024-08-26 12:43:35 +02:00
|
|
|
import { Expectations } from "../../../tests/utilities/api/base"
|
2024-08-26 13:42:20 +02:00
|
|
|
import { roles } from "@budibase/backend-core"
|
2024-08-26 15:17:18 +02:00
|
|
|
import { automations } from "@budibase/pro"
|
2024-07-10 13:24:25 +02:00
|
|
|
|
2024-07-19 10:56:36 +02:00
|
|
|
const expectAutomationId = () =>
|
|
|
|
expect.stringMatching(`^${DocumentType.AUTOMATION}_.+`)
|
|
|
|
|
2024-07-10 13:24:25 +02:00
|
|
|
describe("/rowsActions", () => {
|
|
|
|
const config = setup.getConfig()
|
|
|
|
|
2024-07-11 10:13:28 +02:00
|
|
|
let tableId: string
|
2024-07-10 13:24:25 +02:00
|
|
|
|
|
|
|
beforeAll(async () => {
|
2024-07-11 10:04:25 +02:00
|
|
|
tk.freeze(new Date())
|
2024-07-10 13:24:25 +02:00
|
|
|
await config.init()
|
2024-07-11 10:13:28 +02:00
|
|
|
})
|
2024-07-10 13:24:25 +02:00
|
|
|
|
2024-07-11 10:13:28 +02:00
|
|
|
beforeEach(async () => {
|
|
|
|
const table = await config.api.table.save(setup.structures.basicTable())
|
|
|
|
tableId = table._id!
|
2024-07-10 13:24:25 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
afterAll(setup.afterAll)
|
|
|
|
|
2024-07-17 11:15:55 +02:00
|
|
|
const createRowAction = config.api.rowAction.save
|
2024-07-11 10:46:29 +02:00
|
|
|
|
2024-07-10 15:41:55 +02:00
|
|
|
function createRowActionRequest(): CreateRowActionRequest {
|
|
|
|
return {
|
2024-07-17 11:52:29 +02:00
|
|
|
name: generator.string(),
|
2024-07-10 15:41:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-11 16:57:32 +02:00
|
|
|
function createRowActionRequests(count: number): CreateRowActionRequest[] {
|
|
|
|
return generator
|
2024-07-17 11:52:29 +02:00
|
|
|
.unique(() => generator.string(), count)
|
2024-07-11 16:57:32 +02:00
|
|
|
.map(name => ({ name }))
|
|
|
|
}
|
|
|
|
|
2024-08-26 12:43:35 +02:00
|
|
|
function unauthorisedTests(
|
|
|
|
apiDelegate: (
|
|
|
|
expectations: Expectations,
|
|
|
|
testConfig?: { publicUser?: boolean }
|
|
|
|
) => Promise<any>
|
|
|
|
) {
|
2024-07-10 13:24:25 +02:00
|
|
|
it("returns unauthorised (401) for unauthenticated requests", async () => {
|
2024-08-26 12:43:35 +02:00
|
|
|
await apiDelegate(
|
2024-07-10 13:24:25 +02:00
|
|
|
{
|
|
|
|
status: 401,
|
|
|
|
body: {
|
|
|
|
message: "Session not authenticated",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{ publicUser: true }
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("returns forbidden (403) for non-builder users", async () => {
|
|
|
|
const user = await config.createUser({
|
|
|
|
builder: {},
|
|
|
|
})
|
|
|
|
await config.withUser(user, async () => {
|
2024-07-11 10:46:29 +02:00
|
|
|
await createRowAction(generator.guid(), createRowActionRequest(), {
|
|
|
|
status: 403,
|
2024-08-26 13:42:20 +02:00
|
|
|
body: {
|
|
|
|
message: "Not Authorized",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
it("returns forbidden (403) for non-builder users even if they have table write permissions", async () => {
|
|
|
|
const user = await config.createUser({
|
|
|
|
builder: {},
|
|
|
|
})
|
|
|
|
const tableId = generator.guid()
|
|
|
|
for (const role of Object.values(roles.BUILTIN_ROLE_IDS)) {
|
|
|
|
await config.api.permission.add({
|
|
|
|
roleId: role,
|
|
|
|
resourceId: tableId,
|
|
|
|
level: PermissionLevel.EXECUTE,
|
|
|
|
})
|
|
|
|
}
|
2024-08-26 14:38:18 +02:00
|
|
|
|
|
|
|
// replicate changes before checking permissions
|
|
|
|
await config.publish()
|
|
|
|
|
2024-08-26 13:42:20 +02:00
|
|
|
await config.withUser(user, async () => {
|
|
|
|
await createRowAction(tableId, createRowActionRequest(), {
|
|
|
|
status: 403,
|
|
|
|
body: {
|
|
|
|
message: "Not Authorized",
|
|
|
|
},
|
2024-07-11 10:46:29 +02:00
|
|
|
})
|
2024-07-10 13:24:25 +02:00
|
|
|
})
|
|
|
|
})
|
2024-07-10 13:56:41 +02:00
|
|
|
|
|
|
|
it("rejects (404) for a non-existing table", async () => {
|
2024-07-11 10:46:29 +02:00
|
|
|
await createRowAction(generator.guid(), createRowActionRequest(), {
|
|
|
|
status: 404,
|
|
|
|
})
|
2024-07-10 13:56:41 +02:00
|
|
|
})
|
2024-07-10 13:24:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
describe("create", () => {
|
2024-08-26 12:43:35 +02:00
|
|
|
unauthorisedTests((expectations, testConfig) =>
|
|
|
|
createRowAction(
|
|
|
|
tableId,
|
|
|
|
createRowActionRequest(),
|
|
|
|
expectations,
|
|
|
|
testConfig
|
|
|
|
)
|
|
|
|
)
|
2024-07-10 13:24:25 +02:00
|
|
|
|
2024-07-11 10:13:28 +02:00
|
|
|
it("creates new row actions for tables without existing actions", async () => {
|
2024-07-10 15:41:55 +02:00
|
|
|
const rowAction = createRowActionRequest()
|
2024-07-11 16:57:32 +02:00
|
|
|
const res = await createRowAction(tableId, rowAction, {
|
|
|
|
status: 201,
|
|
|
|
})
|
2024-07-10 13:24:25 +02:00
|
|
|
|
2024-07-11 10:04:25 +02:00
|
|
|
expect(res).toEqual({
|
2024-07-19 10:56:36 +02:00
|
|
|
name: rowAction.name,
|
2024-07-11 17:16:14 +02:00
|
|
|
id: expect.stringMatching(/^row_action_\w+/),
|
2024-07-11 10:13:28 +02:00
|
|
|
tableId: tableId,
|
2024-07-19 10:56:36 +02:00
|
|
|
automationId: expectAutomationId(),
|
2024-07-11 10:13:28 +02:00
|
|
|
})
|
|
|
|
|
2024-07-11 16:57:32 +02:00
|
|
|
expect(await config.api.rowAction.find(tableId)).toEqual({
|
|
|
|
actions: {
|
2024-07-12 11:29:00 +02:00
|
|
|
[res.id]: {
|
2024-07-19 10:56:36 +02:00
|
|
|
name: rowAction.name,
|
2024-07-12 11:29:00 +02:00
|
|
|
id: res.id,
|
|
|
|
tableId: tableId,
|
2024-07-19 10:56:36 +02:00
|
|
|
automationId: expectAutomationId(),
|
2024-07-12 11:29:00 +02:00
|
|
|
},
|
2024-07-11 16:57:32 +02:00
|
|
|
},
|
2024-07-11 10:04:25 +02:00
|
|
|
})
|
2024-07-10 13:24:25 +02:00
|
|
|
})
|
2024-07-10 15:48:16 +02:00
|
|
|
|
2024-07-17 12:16:14 +02:00
|
|
|
it("trims row action names", async () => {
|
|
|
|
const name = " action name "
|
2024-08-29 16:11:05 +02:00
|
|
|
const res = await createRowAction(tableId, { name })
|
2024-07-17 12:16:14 +02:00
|
|
|
|
2024-07-19 10:56:36 +02:00
|
|
|
expect(res).toEqual(
|
|
|
|
expect.objectContaining({
|
2024-07-19 10:58:09 +02:00
|
|
|
name: "action name",
|
|
|
|
})
|
2024-07-19 10:56:36 +02:00
|
|
|
)
|
2024-07-17 12:16:14 +02:00
|
|
|
|
|
|
|
expect(await config.api.rowAction.find(tableId)).toEqual({
|
|
|
|
actions: {
|
|
|
|
[res.id]: expect.objectContaining({
|
|
|
|
name: "action name",
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2024-07-11 16:57:32 +02:00
|
|
|
it("can create multiple row actions for the same table", async () => {
|
|
|
|
const rowActions = createRowActionRequests(3)
|
|
|
|
const responses: RowActionResponse[] = []
|
|
|
|
for (const action of rowActions) {
|
|
|
|
responses.push(await createRowAction(tableId, action))
|
|
|
|
}
|
2024-07-11 10:19:11 +02:00
|
|
|
|
2024-07-11 16:57:32 +02:00
|
|
|
expect(await config.api.rowAction.find(tableId)).toEqual({
|
|
|
|
actions: {
|
2024-07-18 17:11:11 +02:00
|
|
|
[responses[0].id]: {
|
2024-07-19 10:56:36 +02:00
|
|
|
name: rowActions[0].name,
|
2024-07-18 17:11:11 +02:00
|
|
|
id: responses[0].id,
|
|
|
|
tableId,
|
2024-07-19 11:15:17 +02:00
|
|
|
automationId: expectAutomationId(),
|
2024-07-18 17:11:11 +02:00
|
|
|
},
|
|
|
|
[responses[1].id]: {
|
2024-07-19 10:56:36 +02:00
|
|
|
name: rowActions[1].name,
|
2024-07-18 17:11:11 +02:00
|
|
|
id: responses[1].id,
|
|
|
|
tableId,
|
2024-07-19 11:15:17 +02:00
|
|
|
automationId: expectAutomationId(),
|
2024-07-18 17:11:11 +02:00
|
|
|
},
|
|
|
|
[responses[2].id]: {
|
2024-07-19 10:56:36 +02:00
|
|
|
name: rowActions[2].name,
|
2024-07-18 17:11:11 +02:00
|
|
|
id: responses[2].id,
|
|
|
|
tableId,
|
2024-07-19 11:15:17 +02:00
|
|
|
automationId: expectAutomationId(),
|
2024-07-18 17:11:11 +02:00
|
|
|
},
|
2024-07-11 16:57:32 +02:00
|
|
|
},
|
2024-07-11 10:19:11 +02:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2024-07-10 15:48:16 +02:00
|
|
|
it("rejects with bad request when creating with no name", async () => {
|
|
|
|
const rowAction: CreateRowActionRequest = {
|
2024-07-10 15:49:13 +02:00
|
|
|
name: "",
|
2024-07-10 15:48:16 +02:00
|
|
|
}
|
|
|
|
|
2024-07-11 10:46:29 +02:00
|
|
|
await createRowAction(tableId, rowAction, {
|
2024-07-10 15:48:16 +02:00
|
|
|
status: 400,
|
|
|
|
body: {
|
2024-07-10 15:49:13 +02:00
|
|
|
message: 'Invalid body - "name" is not allowed to be empty',
|
2024-07-10 15:48:16 +02:00
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
2024-07-12 12:17:05 +02:00
|
|
|
|
|
|
|
it("ignores not valid row action data", async () => {
|
|
|
|
const rowAction = createRowActionRequest()
|
|
|
|
const dirtyRowAction = {
|
2024-07-19 10:56:36 +02:00
|
|
|
name: rowAction.name,
|
2024-07-12 12:17:05 +02:00
|
|
|
id: generator.guid(),
|
2024-07-17 11:52:29 +02:00
|
|
|
valueToIgnore: generator.string(),
|
2024-07-12 12:17:05 +02:00
|
|
|
}
|
2024-08-29 16:11:05 +02:00
|
|
|
const res = await createRowAction(tableId, dirtyRowAction)
|
2024-07-12 12:17:05 +02:00
|
|
|
|
|
|
|
expect(res).toEqual({
|
2024-07-19 10:56:36 +02:00
|
|
|
name: rowAction.name,
|
2024-07-12 12:17:05 +02:00
|
|
|
id: expect.any(String),
|
|
|
|
tableId,
|
2024-07-19 10:56:36 +02:00
|
|
|
automationId: expectAutomationId(),
|
2024-07-12 12:17:05 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
expect(await config.api.rowAction.find(tableId)).toEqual({
|
|
|
|
actions: {
|
|
|
|
[res.id]: {
|
2024-07-19 10:56:36 +02:00
|
|
|
name: rowAction.name,
|
2024-07-12 12:17:05 +02:00
|
|
|
id: res.id,
|
|
|
|
tableId: tableId,
|
2024-07-19 11:15:17 +02:00
|
|
|
automationId: expectAutomationId(),
|
2024-07-12 12:17:05 +02:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
2024-07-17 12:18:09 +02:00
|
|
|
|
|
|
|
it("can not create multiple row actions with the same name (for the same table)", async () => {
|
|
|
|
const action = await createRowAction(tableId, {
|
|
|
|
name: "Row action name ",
|
|
|
|
})
|
|
|
|
|
|
|
|
await createRowAction(
|
|
|
|
tableId,
|
|
|
|
{ name: action.name },
|
|
|
|
{
|
|
|
|
status: 409,
|
|
|
|
body: {
|
|
|
|
message: "A row action with the same name already exists.",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await createRowAction(
|
|
|
|
tableId,
|
|
|
|
{ name: "row action name" },
|
|
|
|
{
|
|
|
|
status: 409,
|
|
|
|
body: {
|
|
|
|
message: "A row action with the same name already exists.",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
2024-07-17 12:26:36 +02:00
|
|
|
|
|
|
|
it("can reuse row action names between different tables", async () => {
|
|
|
|
const otherTable = await config.api.table.save(
|
|
|
|
setup.structures.basicTable()
|
|
|
|
)
|
|
|
|
|
|
|
|
const action = await createRowAction(tableId, createRowActionRequest())
|
|
|
|
|
|
|
|
await createRowAction(otherTable._id!, { name: action.name })
|
|
|
|
})
|
2024-07-19 10:58:09 +02:00
|
|
|
|
|
|
|
it("an automation is created when creating a new row action", async () => {
|
|
|
|
const action1 = await createRowAction(tableId, createRowActionRequest())
|
|
|
|
const action2 = await createRowAction(tableId, createRowActionRequest())
|
|
|
|
|
|
|
|
for (const automationId of [action1.automationId, action2.automationId]) {
|
2024-08-29 16:11:05 +02:00
|
|
|
expect(await config.api.automation.get(automationId)).toEqual(
|
|
|
|
expect.objectContaining({ _id: automationId })
|
|
|
|
)
|
2024-07-19 10:58:09 +02:00
|
|
|
}
|
|
|
|
})
|
2024-07-10 13:56:41 +02:00
|
|
|
})
|
2024-07-10 13:24:25 +02:00
|
|
|
|
2024-07-10 13:56:41 +02:00
|
|
|
describe("find", () => {
|
2024-08-26 12:43:35 +02:00
|
|
|
unauthorisedTests((expectations, testConfig) =>
|
|
|
|
config.api.rowAction.find(tableId, expectations, testConfig)
|
|
|
|
)
|
2024-07-10 13:56:41 +02:00
|
|
|
|
2024-07-11 10:59:11 +02:00
|
|
|
it("returns only the actions for the requested table", async () => {
|
2024-07-11 16:57:32 +02:00
|
|
|
const rowActions: RowActionResponse[] = []
|
|
|
|
for (const action of createRowActionRequests(3)) {
|
|
|
|
rowActions.push(await createRowAction(tableId, action))
|
2024-07-11 10:46:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const otherTable = await config.api.table.save(
|
|
|
|
setup.structures.basicTable()
|
|
|
|
)
|
2024-07-11 16:57:32 +02:00
|
|
|
await createRowAction(otherTable._id!, createRowActionRequest())
|
2024-07-11 10:59:11 +02:00
|
|
|
|
|
|
|
const response = await config.api.rowAction.find(tableId)
|
2024-07-12 11:29:00 +02:00
|
|
|
expect(response).toEqual({
|
|
|
|
actions: {
|
|
|
|
[rowActions[0].id]: expect.any(Object),
|
|
|
|
[rowActions[1].id]: expect.any(Object),
|
|
|
|
[rowActions[2].id]: expect.any(Object),
|
|
|
|
},
|
|
|
|
})
|
2024-07-11 10:59:11 +02:00
|
|
|
})
|
|
|
|
|
2024-07-11 11:06:36 +02:00
|
|
|
it("returns empty for tables without row actions", async () => {
|
|
|
|
const response = await config.api.rowAction.find(tableId)
|
2024-07-12 11:29:00 +02:00
|
|
|
expect(response).toEqual({
|
|
|
|
actions: {},
|
|
|
|
})
|
2024-07-11 10:46:29 +02:00
|
|
|
})
|
2024-07-10 13:24:25 +02:00
|
|
|
})
|
2024-07-11 17:08:57 +02:00
|
|
|
|
|
|
|
describe("update", () => {
|
2024-08-26 12:43:35 +02:00
|
|
|
unauthorisedTests((expectations, testConfig) =>
|
|
|
|
config.api.rowAction.update(
|
|
|
|
tableId,
|
|
|
|
generator.guid(),
|
|
|
|
createRowActionRequest(),
|
|
|
|
expectations,
|
|
|
|
testConfig
|
|
|
|
)
|
|
|
|
)
|
2024-07-11 17:08:57 +02:00
|
|
|
|
|
|
|
it("can update existing actions", async () => {
|
|
|
|
for (const rowAction of createRowActionRequests(3)) {
|
|
|
|
await createRowAction(tableId, rowAction)
|
|
|
|
}
|
|
|
|
|
|
|
|
const persisted = await config.api.rowAction.find(tableId)
|
|
|
|
|
|
|
|
const [actionId, actionData] = _.sample(
|
|
|
|
Object.entries(persisted.actions)
|
|
|
|
)!
|
|
|
|
|
2024-07-17 11:52:29 +02:00
|
|
|
const updatedName = generator.string()
|
2024-07-11 17:08:57 +02:00
|
|
|
|
|
|
|
const res = await config.api.rowAction.update(tableId, actionId, {
|
|
|
|
name: updatedName,
|
|
|
|
})
|
|
|
|
|
|
|
|
expect(res).toEqual({
|
2024-07-11 17:16:14 +02:00
|
|
|
id: actionId,
|
2024-07-11 17:08:57 +02:00
|
|
|
tableId,
|
|
|
|
name: updatedName,
|
2024-07-19 10:56:36 +02:00
|
|
|
automationId: actionData.automationId,
|
2024-07-11 17:08:57 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
expect(await config.api.rowAction.find(tableId)).toEqual(
|
|
|
|
expect.objectContaining({
|
|
|
|
actions: expect.objectContaining({
|
|
|
|
[actionId]: {
|
|
|
|
name: updatedName,
|
2024-07-19 10:56:36 +02:00
|
|
|
id: actionData.id,
|
|
|
|
tableId: actionData.tableId,
|
|
|
|
automationId: actionData.automationId,
|
2024-07-11 17:08:57 +02:00
|
|
|
},
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
)
|
|
|
|
})
|
2024-07-11 17:14:14 +02:00
|
|
|
|
2024-07-17 12:16:14 +02:00
|
|
|
it("trims row action names", async () => {
|
2024-08-29 16:11:05 +02:00
|
|
|
const rowAction = await createRowAction(tableId, createRowActionRequest())
|
2024-07-17 12:16:14 +02:00
|
|
|
|
|
|
|
const res = await config.api.rowAction.update(tableId, rowAction.id, {
|
|
|
|
name: " action name ",
|
|
|
|
})
|
|
|
|
|
|
|
|
expect(res).toEqual(expect.objectContaining({ name: "action name" }))
|
|
|
|
|
|
|
|
expect(await config.api.rowAction.find(tableId)).toEqual(
|
|
|
|
expect.objectContaining({
|
|
|
|
actions: expect.objectContaining({
|
|
|
|
[rowAction.id]: expect.objectContaining({
|
|
|
|
name: "action name",
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
2024-07-11 17:14:14 +02:00
|
|
|
it("throws Bad Request when trying to update by a non-existing id", async () => {
|
|
|
|
await createRowAction(tableId, createRowActionRequest())
|
|
|
|
|
|
|
|
await config.api.rowAction.update(
|
|
|
|
tableId,
|
|
|
|
generator.guid(),
|
|
|
|
createRowActionRequest(),
|
|
|
|
{ status: 400 }
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("throws Bad Request when trying to update by a via another table id", async () => {
|
|
|
|
const otherTable = await config.api.table.save(
|
|
|
|
setup.structures.basicTable()
|
|
|
|
)
|
|
|
|
await createRowAction(otherTable._id!, createRowActionRequest())
|
|
|
|
|
|
|
|
const action = await createRowAction(tableId, createRowActionRequest())
|
|
|
|
await config.api.rowAction.update(
|
|
|
|
otherTable._id!,
|
2024-07-11 17:16:14 +02:00
|
|
|
action.id,
|
2024-07-11 17:14:14 +02:00
|
|
|
createRowActionRequest(),
|
|
|
|
{ status: 400 }
|
|
|
|
)
|
|
|
|
})
|
2024-07-17 12:26:36 +02:00
|
|
|
|
|
|
|
it("can not use existing row action names (for the same table)", async () => {
|
|
|
|
const action1 = await createRowAction(tableId, createRowActionRequest())
|
|
|
|
const action2 = await createRowAction(tableId, createRowActionRequest())
|
|
|
|
|
|
|
|
await config.api.rowAction.update(
|
|
|
|
tableId,
|
|
|
|
action1.id,
|
|
|
|
{ name: action2.name },
|
|
|
|
{
|
|
|
|
status: 409,
|
|
|
|
body: {
|
|
|
|
message: "A row action with the same name already exists.",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
2024-07-17 12:30:31 +02:00
|
|
|
|
|
|
|
it("does not throw with name conflicts for the same row action", async () => {
|
|
|
|
const action1 = await createRowAction(tableId, createRowActionRequest())
|
|
|
|
|
|
|
|
await config.api.rowAction.update(tableId, action1.id, {
|
|
|
|
name: action1.name,
|
|
|
|
})
|
|
|
|
})
|
2024-07-11 17:08:57 +02:00
|
|
|
})
|
2024-07-11 17:33:40 +02:00
|
|
|
|
|
|
|
describe("delete", () => {
|
2024-08-26 12:43:35 +02:00
|
|
|
unauthorisedTests((expectations, testConfig) =>
|
|
|
|
config.api.rowAction.delete(
|
|
|
|
tableId,
|
|
|
|
generator.guid(),
|
|
|
|
expectations,
|
|
|
|
testConfig
|
|
|
|
)
|
|
|
|
)
|
2024-07-11 17:33:40 +02:00
|
|
|
|
|
|
|
it("can delete existing actions", async () => {
|
|
|
|
const actions: RowActionResponse[] = []
|
|
|
|
for (const rowAction of createRowActionRequests(3)) {
|
|
|
|
actions.push(await createRowAction(tableId, rowAction))
|
|
|
|
}
|
|
|
|
|
|
|
|
const actionToDelete = _.sample(actions)!
|
|
|
|
|
|
|
|
await config.api.rowAction.delete(tableId, actionToDelete.id, {
|
|
|
|
status: 204,
|
|
|
|
})
|
|
|
|
|
|
|
|
expect(await config.api.rowAction.find(tableId)).toEqual(
|
|
|
|
expect.objectContaining({
|
|
|
|
actions: actions
|
|
|
|
.filter(a => a.id !== actionToDelete.id)
|
|
|
|
.reduce((acc, c) => ({ ...acc, [c.id]: expect.any(Object) }), {}),
|
|
|
|
})
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("throws Bad Request when trying to delete by a non-existing id", async () => {
|
|
|
|
await createRowAction(tableId, createRowActionRequest())
|
|
|
|
|
|
|
|
await config.api.rowAction.delete(tableId, generator.guid(), {
|
|
|
|
status: 400,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
it("throws Bad Request when trying to delete by a via another table id", async () => {
|
|
|
|
const otherTable = await config.api.table.save(
|
|
|
|
setup.structures.basicTable()
|
|
|
|
)
|
|
|
|
await createRowAction(otherTable._id!, createRowActionRequest())
|
|
|
|
|
|
|
|
const action = await createRowAction(tableId, createRowActionRequest())
|
|
|
|
await config.api.rowAction.delete(otherTable._id!, action.id, {
|
|
|
|
status: 400,
|
|
|
|
})
|
|
|
|
})
|
2024-07-19 11:01:09 +02:00
|
|
|
|
|
|
|
it("deletes the linked automation", async () => {
|
|
|
|
const actions: RowActionResponse[] = []
|
|
|
|
for (const rowAction of createRowActionRequests(3)) {
|
|
|
|
actions.push(await createRowAction(tableId, rowAction))
|
|
|
|
}
|
|
|
|
|
|
|
|
const actionToDelete = _.sample(actions)!
|
|
|
|
await config.api.rowAction.delete(tableId, actionToDelete.id, {
|
|
|
|
status: 204,
|
|
|
|
})
|
|
|
|
|
|
|
|
await config.api.automation.get(actionToDelete.automationId, {
|
|
|
|
status: 404,
|
|
|
|
})
|
|
|
|
for (const action of actions.filter(a => a.id !== actionToDelete.id)) {
|
|
|
|
await config.api.automation.get(action.automationId, {
|
|
|
|
status: 200,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
2024-07-11 17:33:40 +02:00
|
|
|
})
|
2024-08-26 12:33:56 +02:00
|
|
|
|
2024-08-26 13:21:34 +02:00
|
|
|
describe("set/unsetViewPermission", () => {
|
|
|
|
describe.each([
|
|
|
|
["setViewPermission", config.api.rowAction.setViewPermission],
|
|
|
|
["unsetViewPermission", config.api.rowAction.unsetViewPermission],
|
|
|
|
])("unauthorisedTests for %s", (__, delegateTest) => {
|
|
|
|
unauthorisedTests((expectations, testConfig) =>
|
|
|
|
delegateTest(
|
|
|
|
tableId,
|
|
|
|
generator.guid(),
|
|
|
|
generator.guid(),
|
|
|
|
expectations,
|
|
|
|
testConfig
|
|
|
|
)
|
2024-08-26 12:43:35 +02:00
|
|
|
)
|
2024-08-26 13:21:34 +02:00
|
|
|
})
|
2024-08-26 12:33:56 +02:00
|
|
|
|
2024-08-26 13:21:34 +02:00
|
|
|
let tableIdForDescribe: string
|
|
|
|
let actionId1: string, actionId2: string
|
|
|
|
let viewId1: string, viewId2: string
|
|
|
|
beforeAll(async () => {
|
|
|
|
tableIdForDescribe = tableId
|
2024-08-26 12:33:56 +02:00
|
|
|
for (const rowAction of createRowActionRequests(3)) {
|
|
|
|
await createRowAction(tableId, rowAction)
|
|
|
|
}
|
|
|
|
const persisted = await config.api.rowAction.find(tableId)
|
|
|
|
|
2024-08-26 13:21:34 +02:00
|
|
|
const actions = _.sampleSize(Object.keys(persisted.actions), 2)
|
|
|
|
actionId1 = actions[0]
|
|
|
|
actionId2 = actions[1]
|
|
|
|
|
|
|
|
viewId1 = (
|
|
|
|
await config.api.viewV2.create(
|
|
|
|
setup.structures.viewV2.createRequest(tableId)
|
|
|
|
)
|
|
|
|
).id
|
|
|
|
viewId2 = (
|
|
|
|
await config.api.viewV2.create(
|
|
|
|
setup.structures.viewV2.createRequest(tableId)
|
|
|
|
)
|
|
|
|
).id
|
|
|
|
})
|
2024-08-26 12:33:56 +02:00
|
|
|
|
2024-08-26 13:21:34 +02:00
|
|
|
beforeEach(() => {
|
|
|
|
// Hack to reuse tables for these given tests
|
|
|
|
tableId = tableIdForDescribe
|
|
|
|
})
|
2024-08-26 12:33:56 +02:00
|
|
|
|
2024-08-26 13:21:34 +02:00
|
|
|
it("can set permission views", async () => {
|
2024-08-29 16:11:05 +02:00
|
|
|
await config.api.rowAction.setViewPermission(tableId, viewId1, actionId1)
|
2024-08-26 12:33:56 +02:00
|
|
|
const action1Result = await config.api.rowAction.setViewPermission(
|
|
|
|
tableId,
|
|
|
|
viewId2,
|
2024-08-29 16:11:05 +02:00
|
|
|
actionId1
|
2024-08-26 12:33:56 +02:00
|
|
|
)
|
|
|
|
const action2Result = await config.api.rowAction.setViewPermission(
|
|
|
|
tableId,
|
|
|
|
viewId1,
|
2024-08-29 16:11:05 +02:00
|
|
|
actionId2
|
2024-08-26 12:33:56 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const expectedAction1 = expect.objectContaining({
|
|
|
|
allowedViews: [viewId1, viewId2],
|
|
|
|
})
|
|
|
|
const expectedAction2 = expect.objectContaining({
|
|
|
|
allowedViews: [viewId1],
|
|
|
|
})
|
|
|
|
|
|
|
|
const expectedActions = expect.objectContaining({
|
|
|
|
[actionId1]: expectedAction1,
|
|
|
|
[actionId2]: expectedAction2,
|
|
|
|
})
|
|
|
|
expect(action1Result).toEqual(expectedAction1)
|
|
|
|
expect(action2Result).toEqual(expectedAction2)
|
|
|
|
expect((await config.api.rowAction.find(tableId)).actions).toEqual(
|
|
|
|
expectedActions
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("can unset permission views", async () => {
|
|
|
|
const actionResult = await config.api.rowAction.unsetViewPermission(
|
|
|
|
tableId,
|
|
|
|
viewId1,
|
2024-08-29 16:11:05 +02:00
|
|
|
actionId1
|
2024-08-26 12:33:56 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const expectedAction = expect.objectContaining({
|
|
|
|
allowedViews: [viewId2],
|
|
|
|
})
|
|
|
|
expect(actionResult).toEqual(expectedAction)
|
|
|
|
expect(
|
2024-08-26 13:21:34 +02:00
|
|
|
(await config.api.rowAction.find(tableId)).actions[actionId1]
|
2024-08-26 12:33:56 +02:00
|
|
|
).toEqual(expectedAction)
|
|
|
|
})
|
2024-08-26 13:21:34 +02:00
|
|
|
|
|
|
|
it.each([
|
|
|
|
["setViewPermission", config.api.rowAction.setViewPermission],
|
|
|
|
["unsetViewPermission", config.api.rowAction.unsetViewPermission],
|
|
|
|
])(
|
|
|
|
"cannot update permission views for unexisting views (%s)",
|
|
|
|
async (__, delegateTest) => {
|
|
|
|
const viewId = generator.guid()
|
|
|
|
|
|
|
|
await delegateTest(tableId, viewId, actionId1, {
|
|
|
|
status: 400,
|
|
|
|
body: {
|
|
|
|
message: `View '${viewId}' not found in '${tableId}'`,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
it.each([
|
|
|
|
["setViewPermission", config.api.rowAction.setViewPermission],
|
|
|
|
["unsetViewPermission", config.api.rowAction.unsetViewPermission],
|
|
|
|
])(
|
|
|
|
"cannot update permission views crossing table views (%s)",
|
|
|
|
async (__, delegateTest) => {
|
|
|
|
const anotherTable = await config.api.table.save(
|
|
|
|
setup.structures.basicTable()
|
|
|
|
)
|
|
|
|
const { id: viewId } = await config.api.viewV2.create(
|
|
|
|
setup.structures.viewV2.createRequest(anotherTable._id!)
|
|
|
|
)
|
|
|
|
|
|
|
|
await delegateTest(tableId, viewId, actionId1, {
|
|
|
|
status: 400,
|
|
|
|
body: {
|
|
|
|
message: `View '${viewId}' not found in '${tableId}'`,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
)
|
2024-08-26 12:33:56 +02:00
|
|
|
})
|
2024-08-26 15:17:18 +02:00
|
|
|
|
|
|
|
describe("trigger", () => {
|
|
|
|
let row: Row
|
|
|
|
let rowAction: RowActionResponse
|
|
|
|
|
2024-08-26 16:12:39 +02:00
|
|
|
beforeEach(async () => {
|
|
|
|
row = await config.api.row.save(tableId, {})
|
|
|
|
rowAction = await createRowAction(tableId, createRowActionRequest())
|
|
|
|
|
|
|
|
await config.publish()
|
2024-08-26 17:26:59 +02:00
|
|
|
tk.travel(Date.now() + 100)
|
2024-08-26 16:12:39 +02:00
|
|
|
})
|
|
|
|
|
2024-08-26 17:26:59 +02:00
|
|
|
async function getAutomationLogs() {
|
|
|
|
const { data: automationLogs } = await config.doInContext(
|
|
|
|
config.getProdAppId(),
|
|
|
|
async () =>
|
|
|
|
automations.logs.logSearch({ startDate: new Date().toISOString() })
|
2024-08-26 15:17:18 +02:00
|
|
|
)
|
2024-08-26 17:26:59 +02:00
|
|
|
return automationLogs
|
|
|
|
}
|
2024-08-26 15:17:18 +02:00
|
|
|
|
|
|
|
it("can trigger an automation given valid data", async () => {
|
2024-08-26 16:12:39 +02:00
|
|
|
await config.api.rowAction.trigger(tableId, rowAction.id, {
|
2024-08-26 17:26:59 +02:00
|
|
|
rowId: row._id!,
|
2024-08-26 16:12:39 +02:00
|
|
|
})
|
2024-08-26 15:17:18 +02:00
|
|
|
|
2024-08-26 17:26:59 +02:00
|
|
|
const automationLogs = await getAutomationLogs()
|
2024-08-26 15:17:18 +02:00
|
|
|
expect(automationLogs).toEqual([
|
|
|
|
expect.objectContaining({
|
|
|
|
automationId: rowAction.automationId,
|
|
|
|
trigger: expect.objectContaining({
|
|
|
|
outputs: {
|
|
|
|
fields: {},
|
|
|
|
row: await config.api.row.get(tableId, row._id!),
|
|
|
|
table: await config.api.table.get(tableId),
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
])
|
|
|
|
})
|
2024-08-26 17:13:52 +02:00
|
|
|
|
|
|
|
it("rejects triggering from a non-allowed view", async () => {
|
|
|
|
const viewId = (
|
|
|
|
await config.api.viewV2.create(
|
|
|
|
setup.structures.viewV2.createRequest(tableId)
|
|
|
|
)
|
|
|
|
).id
|
|
|
|
|
2024-08-26 18:00:14 +02:00
|
|
|
await config.publish()
|
2024-08-26 17:13:52 +02:00
|
|
|
await config.api.rowAction.trigger(
|
|
|
|
viewId,
|
|
|
|
rowAction.id,
|
|
|
|
{
|
|
|
|
rowId: row._id!,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
status: 403,
|
|
|
|
body: {
|
2024-08-26 17:26:59 +02:00
|
|
|
message: `Row action '${rowAction.id}' is not enabled for view '${viewId}'`,
|
2024-08-26 17:13:52 +02:00
|
|
|
},
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2024-08-26 17:26:59 +02:00
|
|
|
const automationLogs = await getAutomationLogs()
|
|
|
|
expect(automationLogs).toEqual([])
|
|
|
|
})
|
|
|
|
|
|
|
|
it("triggers from an allowed view", async () => {
|
|
|
|
const viewId = (
|
|
|
|
await config.api.viewV2.create(
|
|
|
|
setup.structures.viewV2.createRequest(tableId)
|
|
|
|
)
|
|
|
|
).id
|
|
|
|
|
|
|
|
await config.api.rowAction.setViewPermission(
|
|
|
|
tableId,
|
|
|
|
viewId,
|
|
|
|
rowAction.id
|
2024-08-26 17:13:52 +02:00
|
|
|
)
|
2024-08-26 17:26:59 +02:00
|
|
|
|
2024-08-26 18:00:14 +02:00
|
|
|
await config.publish()
|
|
|
|
await config.api.rowAction.trigger(viewId, rowAction.id, {
|
2024-08-26 17:26:59 +02:00
|
|
|
rowId: row._id!,
|
|
|
|
})
|
|
|
|
|
|
|
|
const automationLogs = await getAutomationLogs()
|
2024-08-26 17:13:52 +02:00
|
|
|
expect(automationLogs).toEqual([
|
|
|
|
expect.objectContaining({
|
|
|
|
automationId: rowAction.automationId,
|
|
|
|
}),
|
|
|
|
])
|
|
|
|
})
|
2024-09-04 10:16:59 +02:00
|
|
|
|
2024-09-04 11:11:10 +02:00
|
|
|
describe("role permission checks", () => {
|
|
|
|
function createUser(role: string) {
|
|
|
|
return config.createUser({
|
|
|
|
admin: { global: false },
|
|
|
|
builder: {},
|
|
|
|
roles: { [config.getProdAppId()]: role },
|
|
|
|
})
|
|
|
|
}
|
2024-09-04 10:16:59 +02:00
|
|
|
|
2024-09-04 11:11:10 +02:00
|
|
|
function getRolesHigherThan(role: string) {
|
|
|
|
const result = Object.values(roles.BUILTIN_ROLE_IDS).filter(
|
|
|
|
r => r !== role && roles.lowerBuiltinRoleID(r, role) === role
|
2024-09-04 10:16:59 +02:00
|
|
|
)
|
2024-09-04 11:11:10 +02:00
|
|
|
return result
|
2024-09-04 10:16:59 +02:00
|
|
|
}
|
2024-09-04 11:11:10 +02:00
|
|
|
function getRolesLowerThan(role: string) {
|
|
|
|
const result = Object.values(roles.BUILTIN_ROLE_IDS).filter(
|
|
|
|
r => r !== role && roles.lowerBuiltinRoleID(r, role) === r
|
|
|
|
)
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
const allowedRoleConfig = Object.values(roles.BUILTIN_ROLE_IDS).flatMap(
|
|
|
|
r => [r, ...getRolesLowerThan(r)].map(p => [r, p])
|
|
|
|
)
|
|
|
|
|
|
|
|
const disallowedRoleConfig = Object.values(
|
|
|
|
roles.BUILTIN_ROLE_IDS
|
|
|
|
).flatMap(r => getRolesHigherThan(r).map(p => [r, p]))
|
|
|
|
|
|
|
|
it.each(allowedRoleConfig)(
|
|
|
|
"allows triggering if the user has table read permission (user %s, table %s)",
|
|
|
|
async (userRole, resourcePermission) => {
|
|
|
|
await config.api.permission.add({
|
|
|
|
level: PermissionLevel.READ,
|
|
|
|
resourceId: tableId,
|
|
|
|
roleId: resourcePermission,
|
|
|
|
})
|
|
|
|
|
|
|
|
const normalUser = await createUser(userRole)
|
|
|
|
|
|
|
|
await config.withUser(normalUser, async () => {
|
|
|
|
await config.publish()
|
|
|
|
await config.api.rowAction.trigger(
|
|
|
|
tableId,
|
|
|
|
rowAction.id,
|
|
|
|
{
|
|
|
|
rowId: row._id!,
|
|
|
|
},
|
|
|
|
{ status: 200 }
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
it.each(disallowedRoleConfig)(
|
|
|
|
"rejects if the user does not have table read permission (user %s, table %s)",
|
|
|
|
async (userRole, resourcePermission) => {
|
|
|
|
await config.api.permission.add({
|
|
|
|
level: PermissionLevel.READ,
|
|
|
|
resourceId: tableId,
|
|
|
|
roleId: resourcePermission,
|
|
|
|
})
|
|
|
|
|
|
|
|
const normalUser = await createUser(userRole)
|
|
|
|
|
|
|
|
await config.withUser(normalUser, async () => {
|
|
|
|
await config.publish()
|
|
|
|
await config.api.rowAction.trigger(
|
|
|
|
tableId,
|
|
|
|
rowAction.id,
|
|
|
|
{
|
|
|
|
rowId: row._id!,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
status: 403,
|
|
|
|
body: { message: "User does not have permission" },
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
const automationLogs = await getAutomationLogs()
|
|
|
|
expect(automationLogs).toBeEmpty()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
2024-08-26 15:17:18 +02:00
|
|
|
})
|
2024-07-10 13:24:25 +02:00
|
|
|
})
|