diff --git a/packages/server/src/api/routes/tests/permissions.spec.ts b/packages/server/src/api/routes/tests/permissions.spec.ts index 1eabf6edbb..af7a2a578e 100644 --- a/packages/server/src/api/routes/tests/permissions.spec.ts +++ b/packages/server/src/api/routes/tests/permissions.spec.ts @@ -16,7 +16,7 @@ import { ViewV2, } from "@budibase/types" import * as setup from "./utilities" -import { mocks } from "@budibase/backend-core/tests" +import { generator, mocks } from "@budibase/backend-core/tests" const { basicRow } = setup.structures const { BUILTIN_ROLE_IDS } = roles @@ -44,7 +44,10 @@ describe("/permission", () => { table = (await config.createTable()) as typeof table row = await config.createRow() - view = await config.api.viewV2.create({ tableId: table._id }) + view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + }) perms = await config.api.permission.add({ roleId: STD_ROLE_ID, resourceId: table._id, diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 371045687b..86577ecd73 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -42,6 +42,7 @@ tk.freeze(timestamp) jest.unmock("mysql2") jest.unmock("mysql2/promise") jest.unmock("mssql") +jest.unmock("pg") describe.each([ ["internal", undefined], @@ -152,8 +153,8 @@ describe.each([ table = await config.api.table.save(defaultTable()) }) - describe("save, load, update", () => { - it("returns a success message when the row is created", async () => { + describe("create", () => { + it("creates a new row successfully", async () => { const rowUsage = await getRowUsage() const row = await config.api.row.save(table._id!, { name: "Test Contact", @@ -163,7 +164,44 @@ describe.each([ await assertRowUsage(rowUsage + 1) }) - it("Increment row autoId per create row request", async () => { + it("fails to create a row for a table that does not exist", async () => { + const rowUsage = await getRowUsage() + await config.api.row.save("1234567", {}, { status: 404 }) + await assertRowUsage(rowUsage) + }) + + it("fails to create a row if required fields are missing", async () => { + const rowUsage = await getRowUsage() + const table = await config.api.table.save( + saveTableRequest({ + schema: { + required: { + type: FieldType.STRING, + name: "required", + constraints: { + type: "string", + presence: true, + }, + }, + }, + }) + ) + await config.api.row.save( + table._id!, + {}, + { + status: 500, + body: { + validationErrors: { + required: ["can't be blank"], + }, + }, + } + ) + await assertRowUsage(rowUsage) + }) + + it("increment row autoId per create row request", async () => { const rowUsage = await getRowUsage() const newTable = await config.api.table.save( @@ -198,52 +236,6 @@ describe.each([ await assertRowUsage(rowUsage + 10) }) - it("updates a row successfully", async () => { - const existing = await config.api.row.save(table._id!, {}) - const rowUsage = await getRowUsage() - - const res = await config.api.row.save(table._id!, { - _id: existing._id, - _rev: existing._rev, - name: "Updated Name", - }) - - expect(res.name).toEqual("Updated Name") - await assertRowUsage(rowUsage) - }) - - it("should load a row", async () => { - const existing = await config.api.row.save(table._id!, {}) - - const res = await config.api.row.get(table._id!, existing._id!) - - expect(res).toEqual({ - ...existing, - ...defaultRowFields, - }) - }) - - it("should list all rows for given tableId", async () => { - const table = await config.api.table.save(defaultTable()) - const rows = await Promise.all([ - config.api.row.save(table._id!, {}), - config.api.row.save(table._id!, {}), - ]) - - const res = await config.api.row.fetch(table._id!) - expect(res.map(r => r._id)).toEqual( - expect.arrayContaining(rows.map(r => r._id)) - ) - }) - - it("load should return 404 when row does not exist", async () => { - const table = await config.api.table.save(defaultTable()) - await config.api.row.save(table._id!, {}) - await config.api.row.get(table._id!, "1234567", { - status: 404, - }) - }) - isInternal && it("row values are coerced", async () => { const str: FieldSchema = { @@ -296,8 +288,6 @@ describe.each([ } const table = await config.api.table.save( saveTableRequest({ - name: "TestTable2", - type: "table", schema: { name: str, stringUndefined: str, @@ -404,53 +394,60 @@ describe.each([ }) }) - describe("view save", () => { - it("views have extra data trimmed", async () => { - const table = await config.api.table.save( - saveTableRequest({ - name: "orders", - schema: { - Country: { - type: FieldType.STRING, - name: "Country", - }, - Story: { - type: FieldType.STRING, - name: "Story", - }, - }, - }) - ) + describe("get", () => { + it("reads an existing row successfully", async () => { + const existing = await config.api.row.save(table._id!, {}) - const createViewResponse = await config.api.viewV2.create({ - tableId: table._id!, - name: uuid.v4(), - schema: { - Country: { - visible: true, - }, - }, - }) + const res = await config.api.row.get(table._id!, existing._id!) - const createRowResponse = await config.api.row.save( - createViewResponse.id, - { - Country: "Aussy", - Story: "aaaaa", - } - ) - - const row = await config.api.row.get(table._id!, createRowResponse._id!) - expect(row.Story).toBeUndefined() - expect(row).toEqual({ + expect(res).toEqual({ + ...existing, ...defaultRowFields, - Country: "Aussy", - id: createRowResponse.id, - _id: createRowResponse._id, - _rev: createRowResponse._rev, - tableId: table._id, }) }) + + it("returns 404 when row does not exist", async () => { + const table = await config.api.table.save(defaultTable()) + await config.api.row.save(table._id!, {}) + await config.api.row.get(table._id!, "1234567", { + status: 404, + }) + }) + }) + + describe("fetch", () => { + it("fetches all rows for given tableId", async () => { + const table = await config.api.table.save(defaultTable()) + const rows = await Promise.all([ + config.api.row.save(table._id!, {}), + config.api.row.save(table._id!, {}), + ]) + + const res = await config.api.row.fetch(table._id!) + expect(res.map(r => r._id)).toEqual( + expect.arrayContaining(rows.map(r => r._id)) + ) + }) + + it("returns 404 when table does not exist", async () => { + await config.api.row.fetch("1234567", { status: 404 }) + }) + }) + + describe("update", () => { + it("updates an existing row successfully", async () => { + const existing = await config.api.row.save(table._id!, {}) + const rowUsage = await getRowUsage() + + const res = await config.api.row.save(table._id!, { + _id: existing._id, + _rev: existing._rev, + name: "Updated Name", + }) + + expect(res.name).toEqual("Updated Name") + await assertRowUsage(rowUsage) + }) }) describe("patch", () => { @@ -947,6 +944,7 @@ describe.each([ const table = await config.api.table.save(await userTable()) const view = await config.api.viewV2.create({ tableId: table._id!, + name: generator.guid(), schema: { name: { visible: true }, surname: { visible: true }, @@ -984,6 +982,7 @@ describe.each([ const tableId = table._id! const view = await config.api.viewV2.create({ tableId: tableId, + name: generator.guid(), schema: { name: { visible: true }, address: { visible: true }, @@ -1026,6 +1025,7 @@ describe.each([ const tableId = table._id! const view = await config.api.viewV2.create({ tableId: tableId, + name: generator.guid(), schema: { name: { visible: true }, address: { visible: true }, @@ -1049,6 +1049,7 @@ describe.each([ const tableId = table._id! const view = await config.api.viewV2.create({ tableId: tableId, + name: generator.guid(), schema: { name: { visible: true }, address: { visible: true }, @@ -1109,6 +1110,7 @@ describe.each([ const createViewResponse = await config.api.viewV2.create({ tableId: table._id!, + name: generator.guid(), }) const response = await config.api.viewV2.search(createViewResponse.id) @@ -1155,6 +1157,7 @@ describe.each([ const createViewResponse = await config.api.viewV2.create({ tableId: table._id!, + name: generator.guid(), query: [ { operator: SearchQueryOperators.EQUAL, field: "age", value: 40 }, ], @@ -1279,6 +1282,7 @@ describe.each([ async (sortParams, expected) => { const createViewResponse = await config.api.viewV2.create({ tableId: table._id!, + name: generator.guid(), sort: sortParams, schema: viewSchema, }) @@ -1299,6 +1303,7 @@ describe.each([ async (sortParams, expected) => { const createViewResponse = await config.api.viewV2.create({ tableId: table._id!, + name: generator.guid(), sort: { field: "name", order: SortOrder.ASCENDING, @@ -1339,6 +1344,7 @@ describe.each([ const view = await config.api.viewV2.create({ tableId: table._id!, + name: generator.guid(), schema: { name: { visible: true } }, }) const response = await config.api.viewV2.search(view.id) @@ -1361,6 +1367,7 @@ describe.each([ const table = await config.api.table.save(await userTable()) const createViewResponse = await config.api.viewV2.create({ tableId: table._id!, + name: generator.guid(), }) const response = await config.api.viewV2.search(createViewResponse.id) expect(response.rows).toHaveLength(0) @@ -1376,6 +1383,7 @@ describe.each([ const createViewResponse = await config.api.viewV2.create({ tableId: table._id!, + name: generator.guid(), }) const response = await config.api.viewV2.search(createViewResponse.id, { limit, @@ -1392,6 +1400,7 @@ describe.each([ ) const view = await config.api.viewV2.create({ tableId: table._id!, + name: generator.guid(), }) const rows = (await config.api.viewV2.search(view.id)).rows @@ -1466,6 +1475,7 @@ describe.each([ view = await config.api.viewV2.create({ tableId: table._id!, + name: generator.guid(), }) }) diff --git a/packages/server/src/api/routes/tests/table.spec.ts b/packages/server/src/api/routes/tests/table.spec.ts index 77704a0408..4321f012aa 100644 --- a/packages/server/src/api/routes/tests/table.spec.ts +++ b/packages/server/src/api/routes/tests/table.spec.ts @@ -20,7 +20,7 @@ import sdk from "../../../sdk" import * as uuid from "uuid" import tk from "timekeeper" -import { mocks } from "@budibase/backend-core/tests" +import { generator, mocks } from "@budibase/backend-core/tests" import { TableToBuild } from "../../../tests/utilities/TestConfiguration" tk.freeze(mocks.date.MOCK_DATE) @@ -417,8 +417,8 @@ describe("/tables", () => { it("should fetch views", async () => { const tableId = config.table!._id! const views = [ - await config.api.viewV2.create({ tableId }), - await config.api.viewV2.create({ tableId }), + await config.api.viewV2.create({ tableId, name: generator.guid() }), + await config.api.viewV2.create({ tableId, name: generator.guid() }), ] const res = await request @@ -455,7 +455,7 @@ describe("/tables", () => { }, })) - await config.api.viewV2.create({ tableId }) + await config.api.viewV2.create({ tableId, name: generator.guid() }) await config.createLegacyView() const res = await config.api.table.fetch() diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 5198e63338..ded5e08d29 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -1,9 +1,11 @@ import * as setup from "./utilities" import { CreateViewRequest, + Datasource, FieldSchema, FieldType, INTERNAL_TABLE_SOURCE_ID, + SaveTableRequest, SearchQueryOperators, SortOrder, SortType, @@ -14,65 +16,88 @@ import { ViewV2, } from "@budibase/types" import { generator } from "@budibase/backend-core/tests" -import { generateDatasourceID } from "../../../db/utils" +import * as uuid from "uuid" +import { databaseTestProviders } from "../../../integrations/tests/utils" +import merge from "lodash/merge" -function priceTable(): Table { - return { - name: "table", - type: "table", - sourceId: INTERNAL_TABLE_SOURCE_ID, - sourceType: TableSourceType.INTERNAL, - schema: { - Price: { - type: FieldType.NUMBER, - name: "Price", - constraints: {}, - }, - Category: { - type: FieldType.STRING, - name: "Category", - constraints: { - type: "string", - }, - }, - }, - } -} - -const config = setup.getConfig() - -beforeAll(async () => { - await config.init() -}) +jest.unmock("mysql2") +jest.unmock("mysql2/promise") +jest.unmock("mssql") +jest.unmock("pg") describe.each([ - ["internal ds", () => config.createTable(priceTable())], - [ - "external ds", - async () => { - const datasource = await config.createDatasource({ - datasource: { - ...setup.structures.basicDatasource().datasource, - plus: true, - _id: generateDatasourceID({ plus: true }), - }, - }) + ["internal", undefined], + ["postgres", databaseTestProviders.postgres], + ["mysql", databaseTestProviders.mysql], + ["mssql", databaseTestProviders.mssql], + ["mariadb", databaseTestProviders.mariadb], +])("/v2/views (%s)", (_, dsProvider) => { + const config = setup.getConfig() - return config.createExternalTable({ - ...priceTable(), - sourceId: datasource._id, - sourceType: TableSourceType.EXTERNAL, - }) - }, - ], -])("/v2/views (%s)", (_, tableBuilder) => { let table: Table + let datasource: Datasource + + function saveTableRequest( + ...overrides: Partial[] + ): SaveTableRequest { + const req: SaveTableRequest = { + name: uuid.v4().substring(0, 16), + type: "table", + sourceType: datasource + ? TableSourceType.EXTERNAL + : TableSourceType.INTERNAL, + sourceId: datasource ? datasource._id! : INTERNAL_TABLE_SOURCE_ID, + primary: ["id"], + schema: { + id: { + type: FieldType.AUTO, + name: "id", + autocolumn: true, + constraints: { + presence: true, + }, + }, + }, + } + return merge(req, ...overrides) + } + + function priceTable(): SaveTableRequest { + return saveTableRequest({ + schema: { + Price: { + type: FieldType.NUMBER, + name: "Price", + constraints: {}, + }, + Category: { + type: FieldType.STRING, + name: "Category", + constraints: { + type: "string", + }, + }, + }, + }) + } beforeAll(async () => { - table = await tableBuilder() + await config.init() + + if (dsProvider) { + datasource = await config.createDatasource({ + datasource: await dsProvider.datasource(), + }) + } + table = await config.api.table.save(priceTable()) }) - afterAll(setup.afterAll) + afterAll(async () => { + if (dsProvider) { + await dsProvider.stop() + } + setup.afterAll() + }) describe("create", () => { it("persist the view when the view is successfully created", async () => { @@ -186,9 +211,12 @@ describe.each([ let view: ViewV2 beforeEach(async () => { - table = await tableBuilder() + table = await config.api.table.save(priceTable()) - view = await config.api.viewV2.create({ name: "View A" }) + view = await config.api.viewV2.create({ + tableId: table._id!, + name: "View A", + }) }) it("can update an existing view data", async () => { @@ -247,6 +275,9 @@ describe.each([ ...updatedData, schema: { ...table.schema, + id: expect.objectContaining({ + visible: false, + }), Category: expect.objectContaining({ visible: false, }), @@ -320,23 +351,27 @@ describe.each([ }) it("cannot update views v1", async () => { - const viewV1 = await config.createLegacyView() - await config.api.viewV2.update( - { - ...viewV1, - }, - { + const viewV1 = await config.api.legacyView.save({ + tableId: table._id!, + name: generator.guid(), + filters: [], + schema: {}, + }) + + await config.api.viewV2.update(viewV1 as unknown as ViewV2, { + status: 400, + body: { + message: "Only views V2 can be updated", status: 400, - body: { - message: "Only views V2 can be updated", - status: 400, - }, - } - ) + }, + }) }) it("cannot update the a view with unmatching ids between url and body", async () => { - const anotherView = await config.api.viewV2.create() + const anotherView = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + }) const result = await config .request!.put(`/api/v2/views/${anotherView.id}`) .send(view) @@ -411,7 +446,10 @@ describe.each([ let view: ViewV2 beforeAll(async () => { - view = await config.api.viewV2.create() + view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + }) }) it("can delete an existing view", async () => { @@ -448,4 +486,43 @@ describe.each([ expect(viewSchema.Price?.visible).toEqual(false) }) }) + + describe("read", () => { + it("views have extra data trimmed", async () => { + const table = await config.api.table.save( + saveTableRequest({ + name: "orders", + schema: { + Country: { + type: FieldType.STRING, + name: "Country", + }, + Story: { + type: FieldType.STRING, + name: "Story", + }, + }, + }) + ) + + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: uuid.v4(), + schema: { + Country: { + visible: true, + }, + }, + }) + + let row = await config.api.row.save(view.id, { + Country: "Aussy", + Story: "aaaaa", + }) + + row = await config.api.row.get(table._id!, row._id!) + expect(row.Story).toBeUndefined() + expect(row.Country).toEqual("Aussy") + }) + }) }) diff --git a/packages/server/src/sdk/tests/rows/search.spec.ts b/packages/server/src/sdk/tests/rows/search.spec.ts deleted file mode 100644 index feae5e7ee8..0000000000 --- a/packages/server/src/sdk/tests/rows/search.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as search from "../../app/rows/search" - -describe("removeEmptyFilters", () => { - it("0 should not be removed", () => { - const filters = search.removeEmptyFilters({ - equal: { - column: 0, - }, - }) - expect((filters.equal as any).column).toBe(0) - }) - - it("empty string should be removed", () => { - const filters = search.removeEmptyFilters({ - equal: { - column: "", - }, - }) - expect(Object.values(filters.equal as any).length).toBe(0) - }) -}) diff --git a/packages/server/src/tests/utilities/api/viewV2.ts b/packages/server/src/tests/utilities/api/viewV2.ts index d4539e00b1..bd6241b7cd 100644 --- a/packages/server/src/tests/utilities/api/viewV2.ts +++ b/packages/server/src/tests/utilities/api/viewV2.ts @@ -11,21 +11,9 @@ import sdk from "../../../sdk" export class ViewV2API extends TestAPI { create = async ( - viewData?: Partial, + view: CreateViewRequest, expectations?: Expectations ): Promise => { - let tableId = viewData?.tableId - if (!tableId && !this.config.table) { - throw "Test requires table to be configured." - } - - tableId = tableId || this.config.table!._id! - const view = { - tableId, - name: generator.guid(), - ...viewData, - } - const exp: Expectations = { status: 201, ...expectations,