pgsql-test 5.18.0 → 5.19.0
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 +21 -0
- package/esm/test-client.js +76 -0
- package/package.json +3 -3
- package/test-client.d.ts +24 -0
- package/test-client.js +76 -0
package/README.md
CHANGED
|
@@ -131,6 +131,7 @@ The `PgTestClient` returned by `getConnections()` wraps a `pg.Client` and provid
|
|
|
131
131
|
* `beforeEach()` – Begins a transaction and sets a savepoint (called at the start of each test)
|
|
132
132
|
* `afterEach()` – Rolls back to the savepoint and commits the outer transaction (cleans up test state)
|
|
133
133
|
* `setContext({ key: value })` – Sets PostgreSQL config variables (like `role`) to simulate RLS contexts
|
|
134
|
+
* `expectError(run, expected?)` – Asserts `run` fails (message substring/RegExp, or `{ code, message }`) inside a savepoint, so the transaction stays usable afterwards; resolves to the caught error (see [Expected failures](#expected-failures))
|
|
134
135
|
* `checkConstraints()` – Runs the commit-time checks for any pending deferred constraints now, without committing (see [Deferred constraints](#deferred-constraints-under-rollback-isolation))
|
|
135
136
|
* `any`, `one`, `oneOrNone`, `many`, `manyOrNone`, `none`, `result` – Typed query helpers for specific result expectations
|
|
136
137
|
|
|
@@ -604,6 +605,26 @@ This table documents the available options for the `getConnections` function. Th
|
|
|
604
605
|
| `db.prefix` | `string` | `'db-'` | Prefix used when generating test database names |
|
|
605
606
|
| `db.deferredConstraints` | `'off' \| 'check' \| 'immediate'` | `'off'` | How `DEFERRABLE INITIALLY DEFERRED` constraints are handled under rollback isolation (env: `DB_DEFERRED_CONSTRAINTS`). See below. |
|
|
606
607
|
|
|
608
|
+
### Expected failures
|
|
609
|
+
|
|
610
|
+
A failed statement aborts the surrounding transaction, so after a plain `expect(...).rejects` every later query in the test fails with `current transaction is aborted`. Use `expectError()` instead — it wraps the call in a savepoint, always rolls back to it, and throws if the call succeeds or fails differently:
|
|
611
|
+
|
|
612
|
+
```ts
|
|
613
|
+
it('anonymous users cannot write', async () => {
|
|
614
|
+
db.setContext({ role: 'anonymous' });
|
|
615
|
+
await db.expectError(() => db.query(`INSERT INTO app.posts (title) VALUES ('x')`), /permission denied/);
|
|
616
|
+
await db.expectError(() => db.query(`DELETE FROM app.posts`), { code: '42501' });
|
|
617
|
+
|
|
618
|
+
const err = await db.expectError(() => db.query(`SELECT app.transfer(-5)`));
|
|
619
|
+
expect(err.message).toMatch(/^INVALID_AMOUNT/);
|
|
620
|
+
|
|
621
|
+
// still usable
|
|
622
|
+
expect((await db.one(`SELECT count(*) FROM app.posts`)).count).toBe('1');
|
|
623
|
+
});
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
`expected` is a message substring, a `RegExp`, or `{ code?: string; message?: string | RegExp }` (`code` is the SQLSTATE). Run the statement on the same client you call `expectError` on.
|
|
627
|
+
|
|
607
628
|
### Deferred constraints under rollback isolation
|
|
608
629
|
|
|
609
630
|
`beforeEach()`/`afterEach()` isolate tests by rolling back instead of committing. Anything Postgres only does **at COMMIT** therefore never happens inside a test. The one that bites is `DEFERRABLE INITIALLY DEFERRED` constraints (FK / UNIQUE / EXCLUDE / `CONSTRAINT TRIGGER`): a test can leave a dangling deferred foreign key and still pass, because the check that would have failed the commit is discarded with the rollback. This is inherent to every rollback-based test harness, not specific to pgsql-test.
|
package/esm/test-client.js
CHANGED
|
@@ -5,6 +5,37 @@ import { loadSqlFiles } from 'pgsql-seed';
|
|
|
5
5
|
import { deployPgpm } from 'pgsql-seed';
|
|
6
6
|
import { formatPgError } from './utils';
|
|
7
7
|
const IN_FAILED_SQL_TRANSACTION = '25P02';
|
|
8
|
+
const EXPECT_ERROR_SAVEPOINT = 'pgsql_test_expect_error';
|
|
9
|
+
const describeExpected = (expected) => {
|
|
10
|
+
if (expected === undefined)
|
|
11
|
+
return 'an error';
|
|
12
|
+
if (typeof expected === 'string' || expected instanceof RegExp)
|
|
13
|
+
return String(expected);
|
|
14
|
+
return JSON.stringify({
|
|
15
|
+
code: expected.code,
|
|
16
|
+
message: expected.message === undefined ? undefined : String(expected.message)
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
const matchesText = (text, pattern) => {
|
|
20
|
+
if (typeof pattern === 'string')
|
|
21
|
+
return text.includes(pattern);
|
|
22
|
+
pattern.lastIndex = 0;
|
|
23
|
+
return pattern.test(text);
|
|
24
|
+
};
|
|
25
|
+
const describeError = (err) => [err?.code, String(err?.message ?? err).split('\n')[0]].filter(Boolean).join(': ');
|
|
26
|
+
const matchesExpected = (err, expected) => {
|
|
27
|
+
if (expected === undefined)
|
|
28
|
+
return true;
|
|
29
|
+
const message = String(err?.message ?? '');
|
|
30
|
+
if (typeof expected === 'string' || expected instanceof RegExp) {
|
|
31
|
+
return matchesText(message, expected);
|
|
32
|
+
}
|
|
33
|
+
if (expected.code !== undefined && err?.code !== expected.code)
|
|
34
|
+
return false;
|
|
35
|
+
if (expected.message !== undefined && !matchesText(message, expected.message))
|
|
36
|
+
return false;
|
|
37
|
+
return true;
|
|
38
|
+
};
|
|
8
39
|
export class PgTestClient extends PgClient {
|
|
9
40
|
testOpts;
|
|
10
41
|
constructor(config, opts = {}) {
|
|
@@ -61,6 +92,51 @@ export class PgTestClient extends PgClient {
|
|
|
61
92
|
if (violation)
|
|
62
93
|
throw violation;
|
|
63
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Assert that `run` fails, and keep the test transaction usable afterwards.
|
|
97
|
+
*
|
|
98
|
+
* Wraps `run` in a savepoint on this client, rolls back to it whatever happens
|
|
99
|
+
* (so the aborted statement, or an unexpected success, leaves no trace), then
|
|
100
|
+
* releases it. Throws if `run` succeeds or its error does not match `expected`;
|
|
101
|
+
* otherwise returns the caught error for further assertions.
|
|
102
|
+
*
|
|
103
|
+
* `run` must execute on this same client — a savepoint cannot rescue another
|
|
104
|
+
* connection's aborted transaction.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* await db.expectError(() => db.query('INSERT INTO app.secrets ...'), /permission denied/);
|
|
108
|
+
* const err = await db.expectError(() => db.query('...'), { code: '23505' });
|
|
109
|
+
*/
|
|
110
|
+
async expectError(run, expected) {
|
|
111
|
+
await this.savepoint(EXPECT_ERROR_SAVEPOINT);
|
|
112
|
+
let caught;
|
|
113
|
+
let failed = false;
|
|
114
|
+
try {
|
|
115
|
+
await run();
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
failed = true;
|
|
119
|
+
caught = err;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
await this.rollback(EXPECT_ERROR_SAVEPOINT);
|
|
123
|
+
await this.client.query(`RELEASE SAVEPOINT "${EXPECT_ERROR_SAVEPOINT}";`);
|
|
124
|
+
}
|
|
125
|
+
catch (cleanupError) {
|
|
126
|
+
const outcome = failed ? `run() failed with ${describeError(caught)}` : 'run() succeeded';
|
|
127
|
+
throw new Error(`[pgsql-test] expectError: could not roll back its savepoint (${describeError(cleanupError)}); ${outcome}. ` +
|
|
128
|
+
'Did run() end the transaction or run on another client?', { cause: failed ? caught : cleanupError });
|
|
129
|
+
}
|
|
130
|
+
if (!failed) {
|
|
131
|
+
throw new Error(`[pgsql-test] expectError: expected ${describeExpected(expected)}, but the statement succeeded`);
|
|
132
|
+
}
|
|
133
|
+
if (!matchesExpected(caught, expected)) {
|
|
134
|
+
throw new Error(`[pgsql-test] expectError: expected ${describeExpected(expected)}, got ${describeError(caught)}`, {
|
|
135
|
+
cause: caught
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return caught;
|
|
139
|
+
}
|
|
64
140
|
/**
|
|
65
141
|
* Run the commit-time checks for every pending deferred constraint now, without committing.
|
|
66
142
|
* Postgres checks all outstanding deferred constraint events when a constraint switches from
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pgsql-test",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.19.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "pgsql-test offers isolated, role-aware, and rollback-friendly PostgreSQL environments for integration tests — giving developers realistic test coverage without external state pollution",
|
|
6
6
|
"main": "index.js",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
"pg": "^8.21.0",
|
|
69
69
|
"pg-cache": "^3.30.0",
|
|
70
70
|
"pg-env": "^1.33.0",
|
|
71
|
-
"pgsql-client": "^4.17.
|
|
71
|
+
"pgsql-client": "^4.17.1",
|
|
72
72
|
"pgsql-seed": "^3.16.0"
|
|
73
73
|
},
|
|
74
|
-
"gitHead": "
|
|
74
|
+
"gitHead": "0b8a4fa54f658bf3b3a8776b98cc5fb373103ce8"
|
|
75
75
|
}
|
package/test-client.d.ts
CHANGED
|
@@ -17,6 +17,14 @@ export type PgTestClientOpts = PgClientOpts & {
|
|
|
17
17
|
*/
|
|
18
18
|
deferredConstraints?: DeferredConstraintsMode;
|
|
19
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* What an expected error must match: a substring or RegExp of the message, or
|
|
22
|
+
* an object checking the SQLSTATE `code` and/or the `message`.
|
|
23
|
+
*/
|
|
24
|
+
export type ExpectedPgError = string | RegExp | {
|
|
25
|
+
code?: string;
|
|
26
|
+
message?: string | RegExp;
|
|
27
|
+
};
|
|
20
28
|
export declare class PgTestClient extends PgClient {
|
|
21
29
|
protected testOpts: PgTestClientOpts;
|
|
22
30
|
constructor(config: PgConfig, opts?: PgTestClientOpts);
|
|
@@ -33,6 +41,22 @@ export declare class PgTestClient extends PgClient {
|
|
|
33
41
|
private get deferredConstraintsMode();
|
|
34
42
|
beforeEach(): Promise<void>;
|
|
35
43
|
afterEach(): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Assert that `run` fails, and keep the test transaction usable afterwards.
|
|
46
|
+
*
|
|
47
|
+
* Wraps `run` in a savepoint on this client, rolls back to it whatever happens
|
|
48
|
+
* (so the aborted statement, or an unexpected success, leaves no trace), then
|
|
49
|
+
* releases it. Throws if `run` succeeds or its error does not match `expected`;
|
|
50
|
+
* otherwise returns the caught error for further assertions.
|
|
51
|
+
*
|
|
52
|
+
* `run` must execute on this same client — a savepoint cannot rescue another
|
|
53
|
+
* connection's aborted transaction.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* await db.expectError(() => db.query('INSERT INTO app.secrets ...'), /permission denied/);
|
|
57
|
+
* const err = await db.expectError(() => db.query('...'), { code: '23505' });
|
|
58
|
+
*/
|
|
59
|
+
expectError<E = any>(run: () => Promise<unknown>, expected?: ExpectedPgError): Promise<E>;
|
|
36
60
|
/**
|
|
37
61
|
* Run the commit-time checks for every pending deferred constraint now, without committing.
|
|
38
62
|
* Postgres checks all outstanding deferred constraint events when a constraint switches from
|
package/test-client.js
CHANGED
|
@@ -8,6 +8,37 @@ const pgsql_seed_3 = require("pgsql-seed");
|
|
|
8
8
|
const pgsql_seed_4 = require("pgsql-seed");
|
|
9
9
|
const utils_1 = require("./utils");
|
|
10
10
|
const IN_FAILED_SQL_TRANSACTION = '25P02';
|
|
11
|
+
const EXPECT_ERROR_SAVEPOINT = 'pgsql_test_expect_error';
|
|
12
|
+
const describeExpected = (expected) => {
|
|
13
|
+
if (expected === undefined)
|
|
14
|
+
return 'an error';
|
|
15
|
+
if (typeof expected === 'string' || expected instanceof RegExp)
|
|
16
|
+
return String(expected);
|
|
17
|
+
return JSON.stringify({
|
|
18
|
+
code: expected.code,
|
|
19
|
+
message: expected.message === undefined ? undefined : String(expected.message)
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
const matchesText = (text, pattern) => {
|
|
23
|
+
if (typeof pattern === 'string')
|
|
24
|
+
return text.includes(pattern);
|
|
25
|
+
pattern.lastIndex = 0;
|
|
26
|
+
return pattern.test(text);
|
|
27
|
+
};
|
|
28
|
+
const describeError = (err) => [err?.code, String(err?.message ?? err).split('\n')[0]].filter(Boolean).join(': ');
|
|
29
|
+
const matchesExpected = (err, expected) => {
|
|
30
|
+
if (expected === undefined)
|
|
31
|
+
return true;
|
|
32
|
+
const message = String(err?.message ?? '');
|
|
33
|
+
if (typeof expected === 'string' || expected instanceof RegExp) {
|
|
34
|
+
return matchesText(message, expected);
|
|
35
|
+
}
|
|
36
|
+
if (expected.code !== undefined && err?.code !== expected.code)
|
|
37
|
+
return false;
|
|
38
|
+
if (expected.message !== undefined && !matchesText(message, expected.message))
|
|
39
|
+
return false;
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
11
42
|
class PgTestClient extends pgsql_client_1.PgClient {
|
|
12
43
|
testOpts;
|
|
13
44
|
constructor(config, opts = {}) {
|
|
@@ -64,6 +95,51 @@ class PgTestClient extends pgsql_client_1.PgClient {
|
|
|
64
95
|
if (violation)
|
|
65
96
|
throw violation;
|
|
66
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Assert that `run` fails, and keep the test transaction usable afterwards.
|
|
100
|
+
*
|
|
101
|
+
* Wraps `run` in a savepoint on this client, rolls back to it whatever happens
|
|
102
|
+
* (so the aborted statement, or an unexpected success, leaves no trace), then
|
|
103
|
+
* releases it. Throws if `run` succeeds or its error does not match `expected`;
|
|
104
|
+
* otherwise returns the caught error for further assertions.
|
|
105
|
+
*
|
|
106
|
+
* `run` must execute on this same client — a savepoint cannot rescue another
|
|
107
|
+
* connection's aborted transaction.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* await db.expectError(() => db.query('INSERT INTO app.secrets ...'), /permission denied/);
|
|
111
|
+
* const err = await db.expectError(() => db.query('...'), { code: '23505' });
|
|
112
|
+
*/
|
|
113
|
+
async expectError(run, expected) {
|
|
114
|
+
await this.savepoint(EXPECT_ERROR_SAVEPOINT);
|
|
115
|
+
let caught;
|
|
116
|
+
let failed = false;
|
|
117
|
+
try {
|
|
118
|
+
await run();
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
failed = true;
|
|
122
|
+
caught = err;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
await this.rollback(EXPECT_ERROR_SAVEPOINT);
|
|
126
|
+
await this.client.query(`RELEASE SAVEPOINT "${EXPECT_ERROR_SAVEPOINT}";`);
|
|
127
|
+
}
|
|
128
|
+
catch (cleanupError) {
|
|
129
|
+
const outcome = failed ? `run() failed with ${describeError(caught)}` : 'run() succeeded';
|
|
130
|
+
throw new Error(`[pgsql-test] expectError: could not roll back its savepoint (${describeError(cleanupError)}); ${outcome}. ` +
|
|
131
|
+
'Did run() end the transaction or run on another client?', { cause: failed ? caught : cleanupError });
|
|
132
|
+
}
|
|
133
|
+
if (!failed) {
|
|
134
|
+
throw new Error(`[pgsql-test] expectError: expected ${describeExpected(expected)}, but the statement succeeded`);
|
|
135
|
+
}
|
|
136
|
+
if (!matchesExpected(caught, expected)) {
|
|
137
|
+
throw new Error(`[pgsql-test] expectError: expected ${describeExpected(expected)}, got ${describeError(caught)}`, {
|
|
138
|
+
cause: caught
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return caught;
|
|
142
|
+
}
|
|
67
143
|
/**
|
|
68
144
|
* Run the commit-time checks for every pending deferred constraint now, without committing.
|
|
69
145
|
* Postgres checks all outstanding deferred constraint events when a constraint switches from
|