From 75c2392b601d70931971b1368d27041679dc31bb Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 8 Feb 2024 15:32:38 +0000 Subject: [PATCH] Flesh out Postgres tests, add MySQL tests. --- .../api/routes/tests/queries/mysql.spec.ts | 239 ++++++++++++++++++ .../api/routes/tests/queries/postgres.spec.ts | 73 ++++++ .../src/integrations/tests/utils/index.ts | 3 +- .../src/integrations/tests/utils/mysql.ts | 52 ++++ .../src/integrations/tests/utils/postgres.ts | 4 +- 5 files changed, 367 insertions(+), 4 deletions(-) create mode 100644 packages/server/src/api/routes/tests/queries/mysql.spec.ts create mode 100644 packages/server/src/integrations/tests/utils/mysql.ts diff --git a/packages/server/src/api/routes/tests/queries/mysql.spec.ts b/packages/server/src/api/routes/tests/queries/mysql.spec.ts new file mode 100644 index 0000000000..1c9c1d3865 --- /dev/null +++ b/packages/server/src/api/routes/tests/queries/mysql.spec.ts @@ -0,0 +1,239 @@ +import { Datasource, Query } from "@budibase/types" +import * as setup from "../utilities" +import { databaseTestProviders } from "../../../../integrations/tests/utils" +import mysql from "mysql2/promise" + +jest.unmock("mysql2") +jest.unmock("mysql2/promise") + +const createTableSQL = ` +CREATE TABLE test_table ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(50) NOT NULL +) +` + +const insertSQL = ` +INSERT INTO test_table (name) VALUES ('one'), ('two'), ('three'), ('four'), ('five') +` + +const dropTableSQL = ` +DROP TABLE test_table +` + +describe("/queries", () => { + let config = setup.getConfig() + let datasource: Datasource + + async function createQuery(query: Partial): Promise { + const defaultQuery: Query = { + datasourceId: datasource._id!, + name: "New Query", + parameters: [], + fields: {}, + schema: {}, + queryVerb: "read", + transformer: "return data", + readable: true, + } + return await config.api.query.create({ ...defaultQuery, ...query }) + } + + async function withConnection( + callback: (client: mysql.Connection) => Promise + ): Promise { + const ds = await databaseTestProviders.mysql.datasource() + const con = await mysql.createConnection(ds.config!) + try { + await callback(con) + } finally { + con.end() + } + } + + afterAll(async () => { + await databaseTestProviders.mysql.stop() + setup.afterAll() + }) + + beforeAll(async () => { + await config.init() + datasource = await config.api.datasource.create( + await databaseTestProviders.mysql.datasource() + ) + }) + + beforeEach(async () => { + await withConnection(async connection => { + const resp = await connection.query(createTableSQL) + await connection.query(insertSQL) + }) + }) + + afterEach(async () => { + await withConnection(async connection => { + await connection.query(dropTableSQL) + }) + }) + + it("should execute a query", async () => { + const query = await createQuery({ + fields: { + sql: "SELECT * FROM test_table ORDER BY id", + }, + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { + id: 1, + name: "one", + }, + { + id: 2, + name: "two", + }, + { + id: 3, + name: "three", + }, + { + id: 4, + name: "four", + }, + { + id: 5, + name: "five", + }, + ]) + }) + + it("should be able to transform a query", async () => { + const query = await createQuery({ + fields: { + sql: "SELECT * FROM test_table WHERE id = 1", + }, + transformer: ` + data[0].id = data[0].id + 1; + return data; + `, + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { + id: 2, + name: "one", + }, + ]) + }) + + it("should be able to insert with bindings", async () => { + const query = await createQuery({ + fields: { + sql: "INSERT INTO test_table (name) VALUES ({{ foo }})", + }, + parameters: [ + { + name: "foo", + default: "bar", + }, + ], + queryVerb: "create", + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { + foo: "baz", + }, + }) + + expect(result.data).toEqual([ + { + created: true, + }, + ]) + + await withConnection(async connection => { + const [rows] = await connection.query( + "SELECT * FROM test_table WHERE name = 'baz'" + ) + expect(rows).toHaveLength(1) + }) + }) + + it("should be able to update rows", async () => { + const query = await createQuery({ + fields: { + sql: "UPDATE test_table SET name = {{ name }} WHERE id = {{ id }}", + }, + parameters: [ + { + name: "id", + default: "", + }, + { + name: "name", + default: "updated", + }, + ], + queryVerb: "update", + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { + id: "1", + name: "foo", + }, + }) + + expect(result.data).toEqual([ + { + updated: true, + }, + ]) + + await withConnection(async connection => { + const [rows] = await connection.query( + "SELECT * FROM test_table WHERE id = 1" + ) + expect(rows).toEqual([{ id: 1, name: "foo" }]) + }) + }) + + it("should be able to delete rows", async () => { + const query = await createQuery({ + fields: { + sql: "DELETE FROM test_table WHERE id = {{ id }}", + }, + parameters: [ + { + name: "id", + default: "", + }, + ], + queryVerb: "delete", + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { + id: "1", + }, + }) + + expect(result.data).toEqual([ + { + deleted: true, + }, + ]) + + await withConnection(async connection => { + const [rows] = await connection.query( + "SELECT * FROM test_table WHERE id = 1" + ) + expect(rows).toHaveLength(0) + }) + }) +}) diff --git a/packages/server/src/api/routes/tests/queries/postgres.spec.ts b/packages/server/src/api/routes/tests/queries/postgres.spec.ts index 487644e787..fd6a2b7d3c 100644 --- a/packages/server/src/api/routes/tests/queries/postgres.spec.ts +++ b/packages/server/src/api/routes/tests/queries/postgres.spec.ts @@ -167,4 +167,77 @@ describe("/queries", () => { expect(rows).toHaveLength(1) }) }) + + it("should be able to update rows", async () => { + const query = await createQuery({ + fields: { + sql: "UPDATE test_table SET name = {{ name }} WHERE id = {{ id }}", + }, + parameters: [ + { + name: "id", + default: "", + }, + { + name: "name", + default: "updated", + }, + ], + queryVerb: "update", + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { + id: "1", + name: "foo", + }, + }) + + expect(result.data).toEqual([ + { + updated: true, + }, + ]) + + await withClient(async client => { + const { rows } = await client.query( + "SELECT * FROM test_table WHERE id = 1" + ) + expect(rows).toEqual([{ id: 1, name: "foo" }]) + }) + }) + + it("should be able to delete rows", async () => { + const query = await createQuery({ + fields: { + sql: "DELETE FROM test_table WHERE id = {{ id }}", + }, + parameters: [ + { + name: "id", + default: "", + }, + ], + queryVerb: "delete", + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { + id: "1", + }, + }) + + expect(result.data).toEqual([ + { + deleted: true, + }, + ]) + + await withClient(async client => { + const { rows } = await client.query( + "SELECT * FROM test_table WHERE id = 1" + ) + expect(rows).toHaveLength(0) + }) + }) }) diff --git a/packages/server/src/integrations/tests/utils/index.ts b/packages/server/src/integrations/tests/utils/index.ts index 77fb5d7128..b6e4e43e7a 100644 --- a/packages/server/src/integrations/tests/utils/index.ts +++ b/packages/server/src/integrations/tests/utils/index.ts @@ -3,6 +3,7 @@ jest.unmock("pg") import { Datasource } from "@budibase/types" import * as postgres from "./postgres" import * as mongodb from "./mongodb" +import * as mysql from "./mysql" import { StartedTestContainer } from "testcontainers" jest.setTimeout(30000) @@ -13,4 +14,4 @@ export interface DatabaseProvider { datasource(): Promise } -export const databaseTestProviders = { postgres, mongodb } +export const databaseTestProviders = { postgres, mongodb, mysql } diff --git a/packages/server/src/integrations/tests/utils/mysql.ts b/packages/server/src/integrations/tests/utils/mysql.ts new file mode 100644 index 0000000000..2c44dd1373 --- /dev/null +++ b/packages/server/src/integrations/tests/utils/mysql.ts @@ -0,0 +1,52 @@ +import { Datasource, SourceName } from "@budibase/types" +import { GenericContainer, Wait, StartedTestContainer } from "testcontainers" + +let container: StartedTestContainer | undefined + +export async function start(): Promise { + return await new GenericContainer("mysql:8.3") + .withExposedPorts(3306) + .withEnvironment({ MYSQL_ROOT_PASSWORD: "password" }) + .withWaitStrategy( + Wait.forSuccessfulCommand( + // Because MySQL first starts itself up, runs an init script, then restarts, + // it's possible for the mysqladmin ping to succeed early and then tests to + // run against a MySQL that's mid-restart and fail. To avoid this, we run + // the ping command three times with a small delay between each. + ` + mysqladmin ping -h localhost -P 3306 -u root -ppassword && sleep 0.5 && + mysqladmin ping -h localhost -P 3306 -u root -ppassword && sleep 0.5 && + mysqladmin ping -h localhost -P 3306 -u root -ppassword + ` + ) + ) + .start() +} + +export async function datasource(): Promise { + if (!container) { + container = await start() + } + const host = container.getHost() + const port = container.getMappedPort(3306) + + return { + type: "datasource_plus", + source: SourceName.MYSQL, + plus: true, + config: { + host, + port, + user: "root", + password: "password", + database: "mysql", + }, + } +} + +export async function stop() { + if (container) { + await container.stop() + container = undefined + } +} diff --git a/packages/server/src/integrations/tests/utils/postgres.ts b/packages/server/src/integrations/tests/utils/postgres.ts index 82a62e3916..4bf42c7f88 100644 --- a/packages/server/src/integrations/tests/utils/postgres.ts +++ b/packages/server/src/integrations/tests/utils/postgres.ts @@ -8,9 +8,7 @@ export async function start(): Promise { .withExposedPorts(5432) .withEnvironment({ POSTGRES_PASSWORD: "password" }) .withWaitStrategy( - Wait.forSuccessfulCommand( - "pg_isready -h localhost -p 5432" - ).withStartupTimeout(10000) + Wait.forSuccessfulCommand("pg_isready -h localhost -p 5432") ) .start() }