@budibase/server 2.22.15 → 2.22.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,11 @@
1
1
  import { Datasource, SourceName } from "@budibase/types"
2
- import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
2
+ import { GenericContainer, Wait } from "testcontainers"
3
3
  import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy"
4
+ import { rawQuery } from "./mysql"
5
+ import { generator, testContainerUtils } from "@budibase/backend-core/tests"
6
+ import { startContainer } from "."
4
7
 
5
- let container: StartedTestContainer | undefined
8
+ let ports: Promise<testContainerUtils.Port[]>
6
9
 
7
10
  class MariaDBWaitStrategy extends AbstractWaitStrategy {
8
11
  async waitUntilReady(container: any, boundPorts: any, startTime?: Date) {
@@ -21,38 +24,38 @@ class MariaDBWaitStrategy extends AbstractWaitStrategy {
21
24
  }
22
25
  }
23
26
 
24
- export async function start(): Promise<StartedTestContainer> {
25
- return await new GenericContainer("mariadb:lts")
26
- .withExposedPorts(3306)
27
- .withEnvironment({ MARIADB_ROOT_PASSWORD: "password" })
28
- .withWaitStrategy(new MariaDBWaitStrategy())
29
- .start()
30
- }
27
+ export async function getDatasource(): Promise<Datasource> {
28
+ if (!ports) {
29
+ ports = startContainer(
30
+ new GenericContainer("mariadb:lts")
31
+ .withExposedPorts(3306)
32
+ .withEnvironment({ MARIADB_ROOT_PASSWORD: "password" })
33
+ .withWaitStrategy(new MariaDBWaitStrategy())
34
+ )
35
+ }
31
36
 
32
- export async function datasource(): Promise<Datasource> {
33
- if (!container) {
34
- container = await start()
37
+ const port = (await ports).find(x => x.container === 3306)?.host
38
+ if (!port) {
39
+ throw new Error("MariaDB port not found")
35
40
  }
36
- const host = container.getHost()
37
- const port = container.getMappedPort(3306)
38
41
 
39
- return {
42
+ const config = {
43
+ host: "127.0.0.1",
44
+ port,
45
+ user: "root",
46
+ password: "password",
47
+ database: "mysql",
48
+ }
49
+
50
+ const datasource = {
40
51
  type: "datasource_plus",
41
52
  source: SourceName.MYSQL,
42
53
  plus: true,
43
- config: {
44
- host,
45
- port,
46
- user: "root",
47
- password: "password",
48
- database: "mysql",
49
- },
54
+ config,
50
55
  }
51
- }
52
56
 
53
- export async function stop() {
54
- if (container) {
55
- await container.stop()
56
- container = undefined
57
- }
57
+ const database = generator.guid().replaceAll("-", "")
58
+ await rawQuery(datasource, `CREATE DATABASE \`${database}\``)
59
+ datasource.config.database = database
60
+ return datasource
58
61
  }
@@ -1,43 +1,39 @@
1
+ import { generator, testContainerUtils } from "@budibase/backend-core/tests"
1
2
  import { Datasource, SourceName } from "@budibase/types"
2
- import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
3
+ import { GenericContainer, Wait } from "testcontainers"
4
+ import { startContainer } from "."
3
5
 
4
- let container: StartedTestContainer | undefined
6
+ let ports: Promise<testContainerUtils.Port[]>
5
7
 
6
- export async function start(): Promise<StartedTestContainer> {
7
- return await new GenericContainer("mongo:7.0-jammy")
8
- .withExposedPorts(27017)
9
- .withEnvironment({
10
- MONGO_INITDB_ROOT_USERNAME: "mongo",
11
- MONGO_INITDB_ROOT_PASSWORD: "password",
12
- })
13
- .withWaitStrategy(
14
- Wait.forSuccessfulCommand(
15
- `mongosh --eval "db.version()"`
16
- ).withStartupTimeout(10000)
8
+ export async function getDatasource(): Promise<Datasource> {
9
+ if (!ports) {
10
+ ports = startContainer(
11
+ new GenericContainer("mongo:7.0-jammy")
12
+ .withExposedPorts(27017)
13
+ .withEnvironment({
14
+ MONGO_INITDB_ROOT_USERNAME: "mongo",
15
+ MONGO_INITDB_ROOT_PASSWORD: "password",
16
+ })
17
+ .withWaitStrategy(
18
+ Wait.forSuccessfulCommand(
19
+ `mongosh --eval "db.version()"`
20
+ ).withStartupTimeout(10000)
21
+ )
17
22
  )
18
- .start()
19
- }
23
+ }
20
24
 
21
- export async function datasource(): Promise<Datasource> {
22
- if (!container) {
23
- container = await start()
25
+ const port = (await ports).find(x => x.container === 27017)
26
+ if (!port) {
27
+ throw new Error("MongoDB port not found")
24
28
  }
25
- const host = container.getHost()
26
- const port = container.getMappedPort(27017)
29
+
27
30
  return {
28
31
  type: "datasource",
29
32
  source: SourceName.MONGODB,
30
33
  plus: false,
31
34
  config: {
32
- connectionString: `mongodb://mongo:password@${host}:${port}`,
33
- db: "mongo",
35
+ connectionString: `mongodb://mongo:password@127.0.0.1:${port.host}`,
36
+ db: generator.guid(),
34
37
  },
35
38
  }
36
39
  }
37
-
38
- export async function stop() {
39
- if (container) {
40
- await container.stop()
41
- container = undefined
42
- }
43
- }
@@ -1,43 +1,41 @@
1
1
  import { Datasource, SourceName } from "@budibase/types"
2
- import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
3
-
4
- let container: StartedTestContainer | undefined
5
-
6
- export async function start(): Promise<StartedTestContainer> {
7
- return await new GenericContainer(
8
- "mcr.microsoft.com/mssql/server:2022-latest"
9
- )
10
- .withExposedPorts(1433)
11
- .withEnvironment({
12
- ACCEPT_EULA: "Y",
13
- MSSQL_SA_PASSWORD: "Password_123",
14
- // This is important, as Microsoft allow us to use the "Developer" edition
15
- // of SQL Server for development and testing purposes. We can't use other
16
- // versions without a valid license, and we cannot use the Developer
17
- // version in production.
18
- MSSQL_PID: "Developer",
19
- })
20
- .withWaitStrategy(
21
- Wait.forSuccessfulCommand(
22
- "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Password_123 -q 'SELECT 1'"
23
- )
24
- )
25
- .start()
26
- }
2
+ import { GenericContainer, Wait } from "testcontainers"
3
+ import mssql from "mssql"
4
+ import { generator, testContainerUtils } from "@budibase/backend-core/tests"
5
+ import { startContainer } from "."
6
+
7
+ let ports: Promise<testContainerUtils.Port[]>
27
8
 
28
- export async function datasource(): Promise<Datasource> {
29
- if (!container) {
30
- container = await start()
9
+ export async function getDatasource(): Promise<Datasource> {
10
+ if (!ports) {
11
+ ports = startContainer(
12
+ new GenericContainer("mcr.microsoft.com/mssql/server:2022-latest")
13
+ .withExposedPorts(1433)
14
+ .withEnvironment({
15
+ ACCEPT_EULA: "Y",
16
+ MSSQL_SA_PASSWORD: "Password_123",
17
+ // This is important, as Microsoft allow us to use the "Developer" edition
18
+ // of SQL Server for development and testing purposes. We can't use other
19
+ // versions without a valid license, and we cannot use the Developer
20
+ // version in production.
21
+ MSSQL_PID: "Developer",
22
+ })
23
+ .withWaitStrategy(
24
+ Wait.forSuccessfulCommand(
25
+ "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Password_123 -q 'SELECT 1'"
26
+ )
27
+ )
28
+ )
31
29
  }
32
- const host = container.getHost()
33
- const port = container.getMappedPort(1433)
34
30
 
35
- return {
31
+ const port = (await ports).find(x => x.container === 1433)?.host
32
+
33
+ const datasource: Datasource = {
36
34
  type: "datasource_plus",
37
35
  source: SourceName.SQL_SERVER,
38
36
  plus: true,
39
37
  config: {
40
- server: host,
38
+ server: "127.0.0.1",
41
39
  port,
42
40
  user: "sa",
43
41
  password: "Password_123",
@@ -46,11 +44,28 @@ export async function datasource(): Promise<Datasource> {
46
44
  },
47
45
  },
48
46
  }
47
+
48
+ const database = generator.guid().replaceAll("-", "")
49
+ await rawQuery(datasource, `CREATE DATABASE "${database}"`)
50
+ datasource.config!.database = database
51
+
52
+ return datasource
49
53
  }
50
54
 
51
- export async function stop() {
52
- if (container) {
53
- await container.stop()
54
- container = undefined
55
+ export async function rawQuery(ds: Datasource, sql: string) {
56
+ if (!ds.config) {
57
+ throw new Error("Datasource config is missing")
58
+ }
59
+ if (ds.source !== SourceName.SQL_SERVER) {
60
+ throw new Error("Datasource source is not SQL Server")
61
+ }
62
+
63
+ const pool = new mssql.ConnectionPool(ds.config! as mssql.config)
64
+ const client = await pool.connect()
65
+ try {
66
+ const { recordset } = await client.query(sql)
67
+ return recordset
68
+ } finally {
69
+ await pool.close()
55
70
  }
56
71
  }
@@ -1,8 +1,11 @@
1
1
  import { Datasource, SourceName } from "@budibase/types"
2
- import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
2
+ import { GenericContainer, Wait } from "testcontainers"
3
3
  import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy"
4
+ import mysql from "mysql2/promise"
5
+ import { generator, testContainerUtils } from "@budibase/backend-core/tests"
6
+ import { startContainer } from "."
4
7
 
5
- let container: StartedTestContainer | undefined
8
+ let ports: Promise<testContainerUtils.Port[]>
6
9
 
7
10
  class MySQLWaitStrategy extends AbstractWaitStrategy {
8
11
  async waitUntilReady(container: any, boundPorts: any, startTime?: Date) {
@@ -24,38 +27,50 @@ class MySQLWaitStrategy extends AbstractWaitStrategy {
24
27
  }
25
28
  }
26
29
 
27
- export async function start(): Promise<StartedTestContainer> {
28
- return await new GenericContainer("mysql:8.3")
29
- .withExposedPorts(3306)
30
- .withEnvironment({ MYSQL_ROOT_PASSWORD: "password" })
31
- .withWaitStrategy(new MySQLWaitStrategy().withStartupTimeout(10000))
32
- .start()
33
- }
34
-
35
- export async function datasource(): Promise<Datasource> {
36
- if (!container) {
37
- container = await start()
30
+ export async function getDatasource(): Promise<Datasource> {
31
+ if (!ports) {
32
+ ports = startContainer(
33
+ new GenericContainer("mysql:8.3")
34
+ .withExposedPorts(3306)
35
+ .withEnvironment({ MYSQL_ROOT_PASSWORD: "password" })
36
+ .withWaitStrategy(new MySQLWaitStrategy().withStartupTimeout(10000))
37
+ )
38
38
  }
39
- const host = container.getHost()
40
- const port = container.getMappedPort(3306)
41
39
 
42
- return {
40
+ const port = (await ports).find(x => x.container === 3306)?.host
41
+
42
+ const datasource: Datasource = {
43
43
  type: "datasource_plus",
44
44
  source: SourceName.MYSQL,
45
45
  plus: true,
46
46
  config: {
47
- host,
47
+ host: "127.0.0.1",
48
48
  port,
49
49
  user: "root",
50
50
  password: "password",
51
51
  database: "mysql",
52
52
  },
53
53
  }
54
+
55
+ const database = generator.guid().replaceAll("-", "")
56
+ await rawQuery(datasource, `CREATE DATABASE \`${database}\``)
57
+ datasource.config!.database = database
58
+ return datasource
54
59
  }
55
60
 
56
- export async function stop() {
57
- if (container) {
58
- await container.stop()
59
- container = undefined
61
+ export async function rawQuery(ds: Datasource, sql: string) {
62
+ if (!ds.config) {
63
+ throw new Error("Datasource config is missing")
64
+ }
65
+ if (ds.source !== SourceName.MYSQL) {
66
+ throw new Error("Datasource source is not MySQL")
67
+ }
68
+
69
+ const connection = await mysql.createConnection(ds.config)
70
+ try {
71
+ const [rows] = await connection.query(sql)
72
+ return rows
73
+ } finally {
74
+ connection.end()
60
75
  }
61
76
  }
@@ -1,33 +1,33 @@
1
1
  import { Datasource, SourceName } from "@budibase/types"
2
- import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
3
-
4
- let container: StartedTestContainer | undefined
5
-
6
- export async function start(): Promise<StartedTestContainer> {
7
- return await new GenericContainer("postgres:16.1-bullseye")
8
- .withExposedPorts(5432)
9
- .withEnvironment({ POSTGRES_PASSWORD: "password" })
10
- .withWaitStrategy(
11
- Wait.forSuccessfulCommand(
12
- "pg_isready -h localhost -p 5432"
13
- ).withStartupTimeout(10000)
14
- )
15
- .start()
16
- }
2
+ import { GenericContainer, Wait } from "testcontainers"
3
+ import pg from "pg"
4
+ import { generator, testContainerUtils } from "@budibase/backend-core/tests"
5
+ import { startContainer } from "."
6
+
7
+ let ports: Promise<testContainerUtils.Port[]>
17
8
 
18
- export async function datasource(): Promise<Datasource> {
19
- if (!container) {
20
- container = await start()
9
+ export async function getDatasource(): Promise<Datasource> {
10
+ if (!ports) {
11
+ ports = startContainer(
12
+ new GenericContainer("postgres:16.1-bullseye")
13
+ .withExposedPorts(5432)
14
+ .withEnvironment({ POSTGRES_PASSWORD: "password" })
15
+ .withWaitStrategy(
16
+ Wait.forSuccessfulCommand(
17
+ "pg_isready -h localhost -p 5432"
18
+ ).withStartupTimeout(10000)
19
+ )
20
+ )
21
21
  }
22
- const host = container.getHost()
23
- const port = container.getMappedPort(5432)
24
22
 
25
- return {
23
+ const port = (await ports).find(x => x.container === 5432)?.host
24
+
25
+ const datasource: Datasource = {
26
26
  type: "datasource_plus",
27
27
  source: SourceName.POSTGRES,
28
28
  plus: true,
29
29
  config: {
30
- host,
30
+ host: "127.0.0.1",
31
31
  port,
32
32
  database: "postgres",
33
33
  user: "postgres",
@@ -38,11 +38,28 @@ export async function datasource(): Promise<Datasource> {
38
38
  ca: false,
39
39
  },
40
40
  }
41
+
42
+ const database = generator.guid().replaceAll("-", "")
43
+ await rawQuery(datasource, `CREATE DATABASE "${database}"`)
44
+ datasource.config!.database = database
45
+
46
+ return datasource
41
47
  }
42
48
 
43
- export async function stop() {
44
- if (container) {
45
- await container.stop()
46
- container = undefined
49
+ export async function rawQuery(ds: Datasource, sql: string) {
50
+ if (!ds.config) {
51
+ throw new Error("Datasource config is missing")
52
+ }
53
+ if (ds.source !== SourceName.POSTGRES) {
54
+ throw new Error("Datasource source is not Postgres")
55
+ }
56
+
57
+ const client = new pg.Client(ds.config)
58
+ await client.connect()
59
+ try {
60
+ const { rows } = await client.query(sql)
61
+ return rows
62
+ } finally {
63
+ await client.end()
47
64
  }
48
65
  }
@@ -25,8 +25,6 @@ const clearMigrations = async () => {
25
25
  }
26
26
  }
27
27
 
28
- jest.setTimeout(10000)
29
-
30
28
  describe("migrations", () => {
31
29
  const config = new TestConfig()
32
30
 
@@ -17,8 +17,6 @@ import {
17
17
  generator,
18
18
  } from "@budibase/backend-core/tests"
19
19
 
20
- jest.setTimeout(30000)
21
-
22
20
  describe("external search", () => {
23
21
  const config = new TestConfiguration()
24
22
 
@@ -2,17 +2,11 @@ import env from "../environment"
2
2
  import { env as coreEnv, timers } from "@budibase/backend-core"
3
3
  import { testContainerUtils } from "@budibase/backend-core/tests"
4
4
 
5
- if (!process.env.DEBUG) {
6
- global.console.log = jest.fn() // console.log are ignored in tests
7
- global.console.warn = jest.fn() // console.warn are ignored in tests
8
- }
9
-
10
5
  if (!process.env.CI) {
11
- // set a longer timeout in dev for debugging
12
- // 100 seconds
6
+ // set a longer timeout in dev for debugging 100 seconds
13
7
  jest.setTimeout(100 * 1000)
14
8
  } else {
15
- jest.setTimeout(10 * 1000)
9
+ jest.setTimeout(30 * 1000)
16
10
  }
17
11
 
18
12
  testContainerUtils.setupEnv(env, coreEnv)
@@ -1,6 +1,7 @@
1
1
  import TestConfiguration from "../TestConfiguration"
2
- import { SuperTest, Test, Response } from "supertest"
2
+ import request, { SuperTest, Test, Response } from "supertest"
3
3
  import { ReadStream } from "fs"
4
+ import { getServer } from "../../../app"
4
5
 
5
6
  type Headers = Record<string, string | string[] | undefined>
6
7
  type Method = "get" | "post" | "put" | "patch" | "delete"
@@ -76,7 +77,8 @@ export abstract class TestAPI {
76
77
  protected _requestRaw = async (
77
78
  method: "get" | "post" | "put" | "patch" | "delete",
78
79
  url: string,
79
- opts?: RequestOpts
80
+ opts?: RequestOpts,
81
+ attempt = 0
80
82
  ): Promise<Response> => {
81
83
  const {
82
84
  headers = {},
@@ -107,26 +109,29 @@ export abstract class TestAPI {
107
109
  const headersFn = publicUser
108
110
  ? this.config.publicHeaders.bind(this.config)
109
111
  : this.config.defaultHeaders.bind(this.config)
110
- let request = this.request[method](url).set(
112
+
113
+ const app = getServer()
114
+ let req = request(app)[method](url)
115
+ req = req.set(
111
116
  headersFn({
112
117
  "x-budibase-include-stacktrace": "true",
113
118
  })
114
119
  )
115
120
  if (headers) {
116
- request = request.set(headers)
121
+ req = req.set(headers)
117
122
  }
118
123
  if (body) {
119
- request = request.send(body)
124
+ req = req.send(body)
120
125
  }
121
126
  for (const [key, value] of Object.entries(fields)) {
122
- request = request.field(key, value)
127
+ req = req.field(key, value)
123
128
  }
124
129
 
125
130
  for (const [key, value] of Object.entries(files)) {
126
131
  if (isAttachedFile(value)) {
127
- request = request.attach(key, value.file, value.name)
132
+ req = req.attach(key, value.file, value.name)
128
133
  } else {
129
- request = request.attach(key, value as any)
134
+ req = req.attach(key, value as any)
130
135
  }
131
136
  }
132
137
  if (expectations?.headers) {
@@ -136,11 +141,25 @@ export abstract class TestAPI {
136
141
  `Got an undefined expected value for header "${key}", if you want to check for the absence of a header, use headersNotPresent`
137
142
  )
138
143
  }
139
- request = request.expect(key, value as any)
144
+ req = req.expect(key, value as any)
140
145
  }
141
146
  }
142
147
 
143
- return await request
148
+ try {
149
+ return await req
150
+ } catch (e: any) {
151
+ // We've found that occasionally the connection between supertest and the
152
+ // server supertest starts gets reset. Not sure why, but retrying it
153
+ // appears to work. I don't particularly like this, but it's better than
154
+ // flakiness.
155
+ if (e.code === "ECONNRESET") {
156
+ if (attempt > 2) {
157
+ throw e
158
+ }
159
+ return await this._requestRaw(method, url, opts, attempt + 1)
160
+ }
161
+ throw e
162
+ }
144
163
  }
145
164
 
146
165
  protected _checkResponse = (
@@ -170,7 +189,18 @@ export abstract class TestAPI {
170
189
  }
171
190
  }
172
191
 
173
- throw new Error(message)
192
+ if (response.error) {
193
+ // Sometimes the error can be between supertest and the app, and when
194
+ // that happens response.error is sometimes populated with `text` that
195
+ // gives more detail about the error. The `message` is almost always
196
+ // useless from what I've seen.
197
+ if (response.error.text) {
198
+ response.error.message = response.error.text
199
+ }
200
+ throw new Error(message, { cause: response.error })
201
+ } else {
202
+ throw new Error(message)
203
+ }
174
204
  }
175
205
 
176
206
  if (expectations?.headersNotPresent) {