@decohere/data-postgres 0.0.0-stage → 0.0.1-dev.5
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.
- package/README.md +24 -2
- package/package.json +35 -4
- package/src/bootstrap.ts +82 -0
- package/src/index.ts +8 -0
- package/src/server-manager.ts +300 -0
- package/src/user-scripts.ts +234 -0
- package/tests/bootstrap-schemas.docker.spec.ts +145 -0
- package/tests/bootstrap.spec.ts +203 -0
- package/tests/db-least-privilege.pentest.spec.ts +125 -0
- package/tests/postgres-container.ts +139 -0
- package/tests/prepare-database.docker.spec.ts +96 -0
- package/tests/server-manager.bootstrap.spec.ts +72 -0
- package/tests/trusted-dev-postgres.spec.ts +217 -0
- package/tests/user-scripts.pentest.spec.ts +42 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ConnectionInfo } from "@decohere/data";
|
|
2
|
+
import { Pool } from "pg";
|
|
3
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
4
|
+
import { PostgresServerManager } from "../src/server-manager.js";
|
|
5
|
+
import { type PostgresContainer, startPostgres } from "./postgres-container.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Real-Postgres gate for scaffold-shaped bootstrap.
|
|
9
|
+
* Catches bugs that SQL-shape / mocked-Pool tests miss (PASSWORD $1, missing maintenance DB).
|
|
10
|
+
* Needs Docker: runs under `pnpm test:docker`, not `pnpm test`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const BOOTSTRAP_USER = "decohere_bootstrap";
|
|
14
|
+
const BOOTSTRAP_PASSWORD = "boot'strap-secret!";
|
|
15
|
+
const APP_USER = "crypto_api_app";
|
|
16
|
+
const APP_PASSWORD = "app'secret!";
|
|
17
|
+
const MIGRATE_USER = "crypto_api_migrate";
|
|
18
|
+
const MIGRATE_PASSWORD = "mig'secret!";
|
|
19
|
+
const APP_DATABASE = "crypto_api_dev";
|
|
20
|
+
|
|
21
|
+
describe("prepareDatabase against real Postgres", () => {
|
|
22
|
+
let postgres: PostgresContainer;
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
postgres = await startPostgres();
|
|
26
|
+
await postgres.superuser.query(
|
|
27
|
+
`CREATE ROLE ${BOOTSTRAP_USER} LOGIN PASSWORD '${BOOTSTRAP_PASSWORD.replaceAll("'", "''")}' CREATEDB CREATEROLE NOSUPERUSER NOBYPASSRLS`,
|
|
28
|
+
);
|
|
29
|
+
await postgres.superuser.query(`GRANT CONNECT ON DATABASE postgres TO ${BOOTSTRAP_USER}`);
|
|
30
|
+
}, 120_000);
|
|
31
|
+
|
|
32
|
+
afterAll(async () => {
|
|
33
|
+
await postgres?.stop();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function connectionInfo(overrides: Partial<ConnectionInfo> = {}): ConnectionInfo {
|
|
37
|
+
return {
|
|
38
|
+
name: "api",
|
|
39
|
+
provider: "postgres",
|
|
40
|
+
host: postgres.host,
|
|
41
|
+
port: postgres.port,
|
|
42
|
+
database: APP_DATABASE,
|
|
43
|
+
synchronize: true,
|
|
44
|
+
seed: false,
|
|
45
|
+
username: APP_USER,
|
|
46
|
+
password: APP_PASSWORD,
|
|
47
|
+
bootstrap: true,
|
|
48
|
+
adminUser: BOOTSTRAP_USER,
|
|
49
|
+
adminPassword: BOOTSTRAP_PASSWORD,
|
|
50
|
+
migrateUsername: MIGRATE_USER,
|
|
51
|
+
migratePassword: MIGRATE_PASSWORD,
|
|
52
|
+
...overrides,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
it("connects as decohere_bootstrap, creates the app DB, and provisions app/migrate roles", async () => {
|
|
57
|
+
await using manager = new PostgresServerManager("crypto-it", connectionInfo());
|
|
58
|
+
await manager.connect();
|
|
59
|
+
await manager.initializeDatabase(APP_DATABASE);
|
|
60
|
+
await manager.provisionRoles({
|
|
61
|
+
database: APP_DATABASE,
|
|
62
|
+
appUsername: APP_USER,
|
|
63
|
+
appPassword: APP_PASSWORD,
|
|
64
|
+
migrateUsername: MIGRATE_USER,
|
|
65
|
+
migratePassword: MIGRATE_PASSWORD,
|
|
66
|
+
allowAppDdl: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const app = new Pool({
|
|
70
|
+
host: postgres.host,
|
|
71
|
+
port: postgres.port,
|
|
72
|
+
user: APP_USER,
|
|
73
|
+
password: APP_PASSWORD,
|
|
74
|
+
database: APP_DATABASE,
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
const result = await app.query("SELECT 1 AS ok");
|
|
78
|
+
expect(result.rows[0]?.ok).toBe(1);
|
|
79
|
+
} finally {
|
|
80
|
+
await app.end();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const migrate = new Pool({
|
|
84
|
+
host: postgres.host,
|
|
85
|
+
port: postgres.port,
|
|
86
|
+
user: MIGRATE_USER,
|
|
87
|
+
password: MIGRATE_PASSWORD,
|
|
88
|
+
database: APP_DATABASE,
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
await migrate.query("CREATE TABLE IF NOT EXISTS it_probe (id int)");
|
|
92
|
+
} finally {
|
|
93
|
+
await migrate.end();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ConnectionInfo } from "@decohere/data";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const { PoolMock, connectMock } = vi.hoisted(() => {
|
|
5
|
+
const connectMock = vi.fn();
|
|
6
|
+
const PoolMock = vi.fn(function Pool(
|
|
7
|
+
this: { connect: typeof connectMock },
|
|
8
|
+
_options: unknown,
|
|
9
|
+
) {
|
|
10
|
+
this.connect = connectMock;
|
|
11
|
+
return this;
|
|
12
|
+
});
|
|
13
|
+
return { PoolMock, connectMock };
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
vi.mock("pg", () => ({ Pool: PoolMock }));
|
|
17
|
+
|
|
18
|
+
import { PostgresServerManager } from "../src/server-manager.js";
|
|
19
|
+
|
|
20
|
+
function adminInfo(overrides: Partial<ConnectionInfo> = {}): ConnectionInfo {
|
|
21
|
+
return {
|
|
22
|
+
name: "api",
|
|
23
|
+
provider: "postgres",
|
|
24
|
+
host: "postgres",
|
|
25
|
+
port: 5432,
|
|
26
|
+
database: "crypto_api_dev",
|
|
27
|
+
synchronize: true,
|
|
28
|
+
seed: true,
|
|
29
|
+
username: "crypto_api_app",
|
|
30
|
+
password: "app-secret",
|
|
31
|
+
bootstrap: true,
|
|
32
|
+
adminUser: "decohere_bootstrap",
|
|
33
|
+
adminPassword: "bootstrap-secret",
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("PostgresServerManager.connect", () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
PoolMock.mockClear();
|
|
41
|
+
connectMock.mockReset();
|
|
42
|
+
connectMock.mockResolvedValue({ release: vi.fn() });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("connects the bootstrap role to the postgres maintenance database", async () => {
|
|
46
|
+
const manager = new PostgresServerManager("crypto", adminInfo());
|
|
47
|
+
await manager.connect();
|
|
48
|
+
|
|
49
|
+
expect(PoolMock).toHaveBeenCalledWith(
|
|
50
|
+
expect.objectContaining({
|
|
51
|
+
user: "decohere_bootstrap",
|
|
52
|
+
password: "bootstrap-secret",
|
|
53
|
+
host: "postgres",
|
|
54
|
+
port: 5432,
|
|
55
|
+
database: "postgres",
|
|
56
|
+
application_name: "crypto",
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
expect(PoolMock.mock.calls[0]?.[0]).not.toEqual(
|
|
60
|
+
expect.objectContaining({ database: "decohere_bootstrap" }),
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("wraps connection failures with host/port context", async () => {
|
|
65
|
+
connectMock.mockRejectedValue(new Error('database "decohere_bootstrap" does not exist'));
|
|
66
|
+
const manager = new PostgresServerManager("crypto", adminInfo());
|
|
67
|
+
|
|
68
|
+
await expect(manager.connect()).rejects.toThrow(
|
|
69
|
+
/Cannot connect to PostgreSQL at postgres:5432/,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { ConnectionInfo } from "@decohere/data";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const { PoolMock, connectMock, queryMock } = vi.hoisted(() => {
|
|
5
|
+
const connectMock = vi.fn();
|
|
6
|
+
const queryMock = vi.fn();
|
|
7
|
+
const PoolMock = vi.fn(function Pool(
|
|
8
|
+
this: { connect: typeof connectMock; query: typeof queryMock; end: () => Promise<void> },
|
|
9
|
+
_options: unknown,
|
|
10
|
+
) {
|
|
11
|
+
this.connect = connectMock;
|
|
12
|
+
this.query = queryMock;
|
|
13
|
+
this.end = async () => {};
|
|
14
|
+
return this;
|
|
15
|
+
});
|
|
16
|
+
return { PoolMock, connectMock, queryMock };
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
vi.mock("pg", () => ({ Pool: PoolMock }));
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
assertBootstrapCredentials,
|
|
23
|
+
assertDataSourceAuth,
|
|
24
|
+
shouldBootstrapDatabase,
|
|
25
|
+
} from "@decohere/data";
|
|
26
|
+
import { createUserQuery } from "../src/user-scripts.js";
|
|
27
|
+
import { PostgresServerManager } from "../src/server-manager.js";
|
|
28
|
+
|
|
29
|
+
// A local dev Postgres that trusts every connection has no passwords at all, and says so
|
|
30
|
+
// with auth: trust.
|
|
31
|
+
function trustedInfo(overrides: Partial<ConnectionInfo> = {}): ConnectionInfo {
|
|
32
|
+
return {
|
|
33
|
+
name: "api",
|
|
34
|
+
provider: "postgres",
|
|
35
|
+
host: "postgres",
|
|
36
|
+
port: 5432,
|
|
37
|
+
database: "crypto_api_dev",
|
|
38
|
+
synchronize: true,
|
|
39
|
+
seed: true,
|
|
40
|
+
username: "crypto_api_app",
|
|
41
|
+
auth: "trust",
|
|
42
|
+
migrateUsername: "crypto_api_migrate",
|
|
43
|
+
adminUser: "decohere_bootstrap",
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sqlSent(): string[] {
|
|
49
|
+
return queryMock.mock.calls.map((call) => String(call[0]));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("trust auth is declared, never guessed", () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
PoolMock.mockClear();
|
|
55
|
+
connectMock.mockReset();
|
|
56
|
+
connectMock.mockResolvedValue({ release: vi.fn() });
|
|
57
|
+
queryMock.mockReset();
|
|
58
|
+
queryMock.mockImplementation(async (sql: string) =>
|
|
59
|
+
sql.includes("datdba")
|
|
60
|
+
? { rowCount: 1, rows: [{ owner: "decohere_bootstrap" }] }
|
|
61
|
+
: { rowCount: 0, rows: [] },
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("checking a data source's auth", () => {
|
|
66
|
+
it("accepts auth: trust with no passwords in development", () => {
|
|
67
|
+
expect(() => assertDataSourceAuth(trustedInfo(), "development")).not.toThrow();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("refuses auth: trust outside development", () => {
|
|
71
|
+
expect(() => assertDataSourceAuth(trustedInfo(), "production")).toThrow(
|
|
72
|
+
/Data source "api" sets auth: trust, which is allowed only in development/,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("refuses a missing password without auth: trust, in development too", () => {
|
|
77
|
+
for (const env of ["development", "production"]) {
|
|
78
|
+
expect(() => assertDataSourceAuth(trustedInfo({ auth: undefined }), env)).toThrow(
|
|
79
|
+
/Data source "api" has no password\. Set password, or auth: trust/,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("treats an empty password as missing", () => {
|
|
85
|
+
expect(() =>
|
|
86
|
+
assertDataSourceAuth(trustedInfo({ auth: "password", password: "" }), "development"),
|
|
87
|
+
).toThrow(/has no password/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("accepts a password without auth, in any environment", () => {
|
|
91
|
+
expect(() =>
|
|
92
|
+
assertDataSourceAuth(
|
|
93
|
+
trustedInfo({ auth: undefined, password: "s3cret" }),
|
|
94
|
+
"production",
|
|
95
|
+
),
|
|
96
|
+
).not.toThrow();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("refuses an auth it does not know", () => {
|
|
100
|
+
expect(() =>
|
|
101
|
+
assertDataSourceAuth(
|
|
102
|
+
trustedInfo({ auth: "md5" as ConnectionInfo["auth"] }),
|
|
103
|
+
"development",
|
|
104
|
+
),
|
|
105
|
+
).toThrow(/auth must be "password" or "trust" \(got "md5"\)/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("refuses a migrate role with no password when bootstrapping without auth: trust", () => {
|
|
109
|
+
expect(() =>
|
|
110
|
+
assertBootstrapCredentials(trustedInfo({ auth: undefined, password: "s3cret" })),
|
|
111
|
+
).toThrow(/Data source "api" has migrateUsername but no migratePassword/);
|
|
112
|
+
expect(() => assertBootstrapCredentials(trustedInfo())).not.toThrow();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("creating a role", () => {
|
|
117
|
+
it("leaves PASSWORD out when no password is given", () => {
|
|
118
|
+
const { text } = createUserQuery("crypto_api_app");
|
|
119
|
+
expect(text).toBe(
|
|
120
|
+
'CREATE USER "crypto_api_app" WITH LOGIN NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE',
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("treats a password a config file set to null as no password", () => {
|
|
125
|
+
const { text } = createUserQuery("crypto_api_app", null);
|
|
126
|
+
expect(text).not.toContain("PASSWORD");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("still refuses an empty password, which is a typo and not a choice", () => {
|
|
130
|
+
expect(() => createUserQuery("crypto_api_app", "")).toThrow(/Invalid password/);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("deciding to bootstrap", () => {
|
|
135
|
+
it("bootstraps in development with auth: trust and no admin password", () => {
|
|
136
|
+
expect(shouldBootstrapDatabase(trustedInfo(), "development")).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("no longer lets development alone stand in for an admin password", () => {
|
|
140
|
+
expect(shouldBootstrapDatabase(trustedInfo({ auth: undefined }), "development")).toBe(
|
|
141
|
+
false,
|
|
142
|
+
);
|
|
143
|
+
expect(() =>
|
|
144
|
+
shouldBootstrapDatabase(
|
|
145
|
+
trustedInfo({ auth: undefined, bootstrap: true }),
|
|
146
|
+
"development",
|
|
147
|
+
),
|
|
148
|
+
).toThrow(/adminPassword may be left out only with auth: trust/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("still needs an admin user", () => {
|
|
152
|
+
expect(
|
|
153
|
+
shouldBootstrapDatabase(trustedInfo({ adminUser: undefined }), "development"),
|
|
154
|
+
).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("connecting as the bootstrap role", () => {
|
|
159
|
+
it("connects with no password when the server is trusted", async () => {
|
|
160
|
+
const manager = new PostgresServerManager("crypto", trustedInfo());
|
|
161
|
+
await manager.connect();
|
|
162
|
+
|
|
163
|
+
const options = PoolMock.mock.calls[0]?.[0] as { user: string; password?: string };
|
|
164
|
+
expect(options.user).toBe("decohere_bootstrap");
|
|
165
|
+
expect(options.password).toBeUndefined();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("asks for the admin password when the server is not trusted", async () => {
|
|
169
|
+
const manager = new PostgresServerManager("crypto", trustedInfo({ auth: undefined }));
|
|
170
|
+
await expect(manager.connect()).rejects.toThrow(/adminUser and adminPassword/);
|
|
171
|
+
expect(PoolMock).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("provisioning the app and migrate roles", () => {
|
|
176
|
+
it("creates both without a PASSWORD clause when the server is trusted", async () => {
|
|
177
|
+
const manager = new PostgresServerManager("crypto", trustedInfo());
|
|
178
|
+
await manager.provisionRoles({
|
|
179
|
+
database: "crypto_api_dev",
|
|
180
|
+
appUsername: "crypto_api_app",
|
|
181
|
+
migrateUsername: "crypto_api_migrate",
|
|
182
|
+
allowAppDdl: false,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const creates = sqlSent().filter((sql) => sql.startsWith("CREATE USER"));
|
|
186
|
+
expect(creates).toHaveLength(2);
|
|
187
|
+
for (const sql of creates) {
|
|
188
|
+
expect(sql).not.toContain("PASSWORD");
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("still sets a password on a role that was given one", async () => {
|
|
193
|
+
const manager = new PostgresServerManager("crypto", trustedInfo());
|
|
194
|
+
await manager.provisionRoles({
|
|
195
|
+
database: "crypto_api_dev",
|
|
196
|
+
appUsername: "crypto_api_app",
|
|
197
|
+
appPassword: "app-secret",
|
|
198
|
+
allowAppDdl: false,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const create = sqlSent().find((sql) => sql.startsWith("CREATE USER"));
|
|
202
|
+
expect(create).toContain("PASSWORD 'app-secret'");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("refuses a role with no password when the server is not trusted", async () => {
|
|
206
|
+
const manager = new PostgresServerManager("crypto", trustedInfo({ auth: undefined }));
|
|
207
|
+
await expect(
|
|
208
|
+
manager.provisionRoles({
|
|
209
|
+
database: "crypto_api_prod",
|
|
210
|
+
appUsername: "crypto_api_app",
|
|
211
|
+
allowAppDdl: false,
|
|
212
|
+
}),
|
|
213
|
+
).rejects.toThrow(/password is required for database role "crypto_api_app"/i);
|
|
214
|
+
expect(sqlSent().some((sql) => sql.startsWith("CREATE USER"))).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
assertSafeSqlIdent,
|
|
4
|
+
createUserQuery,
|
|
5
|
+
quoteIdent,
|
|
6
|
+
quoteLiteral,
|
|
7
|
+
userScripts,
|
|
8
|
+
} from "../src/user-scripts.js";
|
|
9
|
+
|
|
10
|
+
describe("pen-test — data's Postgres bootstrap SQL injection", () => {
|
|
11
|
+
it("rejects username that would break out of SQL string literals", () => {
|
|
12
|
+
expect(() => userScripts.createOwnerUser("evil'; DROP ROLE x; --", "pw", "db")).toThrow(
|
|
13
|
+
/Invalid username/,
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("escapes password metacharacters in CREATE USER (Postgres forbids PASSWORD $1)", () => {
|
|
18
|
+
const create = createUserQuery("apiuser", "x'; DROP ROLE x; --");
|
|
19
|
+
expect(create.text).not.toMatch(/PASSWORD\s+\$\d/);
|
|
20
|
+
expect(create.values).toEqual([]);
|
|
21
|
+
expect(create.text).toContain(quoteLiteral("x'; DROP ROLE x; --"));
|
|
22
|
+
expect(create.text).toContain("''"); // doubled quote from escaping
|
|
23
|
+
// Breakout must stay inside the password literal, not become SQL.
|
|
24
|
+
expect(create.text).toMatch(/PASSWORD 'x''; DROP ROLE x; --'/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("rejects database name with SQL metacharacters", () => {
|
|
28
|
+
expect(() =>
|
|
29
|
+
userScripts.createOwnerUser("apiuser", "secret", "db'; DROP DATABASE x; --"),
|
|
30
|
+
).toThrow(/Invalid database/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("emits CREATE USER with an escaped password literal for a safe username", () => {
|
|
34
|
+
const { text, values } = userScripts.createOwnerUser("api_user", "s3cret!", "webapi_dev");
|
|
35
|
+
expect(text).toContain("PASSWORD 's3cret!'");
|
|
36
|
+
expect(text).not.toMatch(/PASSWORD\s+\$\d/);
|
|
37
|
+
expect(values).toEqual([]);
|
|
38
|
+
expect(quoteIdent("api_user")).toBe('"api_user"');
|
|
39
|
+
expect(quoteLiteral("a'b")).toBe("'a''b'");
|
|
40
|
+
expect(() => assertSafeSqlIdent('x"; DROP', "username")).toThrow(/Invalid username/);
|
|
41
|
+
});
|
|
42
|
+
});
|