neon-testing 2.0.1-beta.2 โ 2.0.1-beta.20
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 +34 -3
- package/dist/index.d.ts +67 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +179 -0
- package/dist/index.js.map +1 -0
- package/dist/singleton.d.ts +5 -0
- package/dist/singleton.d.ts.map +1 -0
- package/dist/singleton.js +11 -0
- package/dist/singleton.js.map +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +3 -0
- package/dist/utils.js.map +1 -0
- package/dist/vite-plugin.d.ts +3 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +19 -0
- package/dist/vite-plugin.js.map +1 -0
- package/dist/vitest-setup.d.ts +2 -0
- package/dist/vitest-setup.d.ts.map +1 -0
- package/dist/vitest-setup.js +9 -0
- package/dist/vitest-setup.js.map +1 -0
- package/package.json +13 -28
- package/index.ts +0 -285
- package/singleton.ts +0 -10
- package/utils.ts +0 -2
- package/vite-plugin.ts +0 -22
- package/vitest-setup.ts +0 -12
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Each test file runs against its own isolated PostgreSQL database (Neon branch),
|
|
|
13
13
|
- ๐ **Isolated test environments** - Each test file runs against its own Postgres database with your actual schema and constraints
|
|
14
14
|
- ๐งน **Automatic cleanup** - Neon test branches are created and destroyed automatically
|
|
15
15
|
- ๐ **Debug friendly** - Option to preserve test branches for debugging failed tests
|
|
16
|
-
- ๐ก๏ธ **TypeScript native** -
|
|
16
|
+
- ๐ก๏ธ **TypeScript native** - With JavaScript support
|
|
17
17
|
- ๐ฏ **ESM only** - No CommonJS support
|
|
18
18
|
|
|
19
19
|
## How it works
|
|
@@ -73,7 +73,23 @@ test("database operations", async () => {
|
|
|
73
73
|
|
|
74
74
|
### Recommended usage
|
|
75
75
|
|
|
76
|
-
#### 1.
|
|
76
|
+
#### 1. Plugin setup
|
|
77
|
+
|
|
78
|
+
First, add the Vite plugin to clear any existing `DATABASE_URL` environment variable before tests run, ensuring tests use isolated test databases.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// vitest.config.ts or vite.config.ts
|
|
82
|
+
import { defineConfig } from "vitest/config";
|
|
83
|
+
import { neonTesting } from "neon-testing/utils";
|
|
84
|
+
|
|
85
|
+
export default defineConfig({
|
|
86
|
+
plugins: [neonTesting()],
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This plugin is recommended but not required. Without it, tests might accidentally use your existing `DATABASE_URL` (from `.env` files or environment variables) instead of the isolated test databases that neon-testing creates. This can happen if you forget to call `withNeonTestBranch()` in a test file where database writes happen.
|
|
91
|
+
|
|
92
|
+
#### 2. Configuration
|
|
77
93
|
|
|
78
94
|
Use the `makeNeonTesting` factory to generate a lifecycle function for your tests.
|
|
79
95
|
|
|
@@ -88,7 +104,7 @@ export const withNeonTestBranch = makeNeonTesting({
|
|
|
88
104
|
});
|
|
89
105
|
```
|
|
90
106
|
|
|
91
|
-
####
|
|
107
|
+
#### 3. Enable database testing
|
|
92
108
|
|
|
93
109
|
Then call the exported test lifecycle function in the test files where you need database access.
|
|
94
110
|
|
|
@@ -164,6 +180,21 @@ import { withNeonTestBranch } from "./test-setup";
|
|
|
164
180
|
withNeonTestBranch({ parentBranchId: "br-staging-123" });
|
|
165
181
|
```
|
|
166
182
|
|
|
183
|
+
## CI/CD
|
|
184
|
+
|
|
185
|
+
It is easy to run Neon integration in CI/CD
|
|
186
|
+
|
|
187
|
+
### GitHub Actions
|
|
188
|
+
|
|
189
|
+
[Example](.github/workflows/test.yml)
|
|
190
|
+
|
|
191
|
+
### Vercel
|
|
192
|
+
|
|
193
|
+
Two options:
|
|
194
|
+
|
|
195
|
+
- Add `vitest run` to the `build` script in [package.json](https://github.com/starmode-base/template-tanstack-start/blob/83c784e164b55fd8d59c5b57b907251e5eb03de1/app/package.json#L11l)
|
|
196
|
+
- Add `vitest run` to the _Build Command_ in the Vercel dashboard
|
|
197
|
+
|
|
167
198
|
## Utilities
|
|
168
199
|
|
|
169
200
|
### deleteAllTestBranches()
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export interface NeonTestingOptions {
|
|
2
|
+
/**
|
|
3
|
+
* The Neon API key, this is used to create and teardown test branches
|
|
4
|
+
*
|
|
5
|
+
* https://neon.com/docs/manage/api-keys#creating-api-keys
|
|
6
|
+
*/
|
|
7
|
+
apiKey: string;
|
|
8
|
+
/**
|
|
9
|
+
* The Neon project ID to operate on
|
|
10
|
+
*
|
|
11
|
+
* https://console.neon.tech/app/projects
|
|
12
|
+
*/
|
|
13
|
+
projectId: string;
|
|
14
|
+
/**
|
|
15
|
+
* The parent branch ID for the new branch. If omitted or empty, the branch
|
|
16
|
+
* will be created from the project's default branch.
|
|
17
|
+
*/
|
|
18
|
+
parentBranchId?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Whether to create a schema-only branch (default: false)
|
|
21
|
+
*/
|
|
22
|
+
schemaOnly?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The type of connection to create (pooler is recommended)
|
|
25
|
+
*/
|
|
26
|
+
endpoint?: "pooler" | "direct";
|
|
27
|
+
/**
|
|
28
|
+
* Delete the test branch in afterAll (default: true)
|
|
29
|
+
*
|
|
30
|
+
* Disabling this will leave each test branch in the Neon project after the
|
|
31
|
+
* test suite runs
|
|
32
|
+
*/
|
|
33
|
+
deleteBranch?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Automatically close Neon WebSocket connections opened during tests before
|
|
36
|
+
* deleting the branch (default: false)
|
|
37
|
+
*
|
|
38
|
+
* Suppresses the specific Neon WebSocket "Connection terminated unexpectedly"
|
|
39
|
+
* error that may surface when deleting a branch with open WebSocket
|
|
40
|
+
* connections
|
|
41
|
+
*/
|
|
42
|
+
autoCloseWebSockets?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** Options for overriding test database setup (excludes apiKey) */
|
|
45
|
+
export type NeonTestingOverrides = Omit<Partial<NeonTestingOptions>, "apiKey">;
|
|
46
|
+
/**
|
|
47
|
+
* Factory function that creates a Neon test database setup/teardown function
|
|
48
|
+
* for Vitest test suites.
|
|
49
|
+
*
|
|
50
|
+
* @param apiKey - The Neon API key, this is used to create and teardown test branches
|
|
51
|
+
* @param projectId - The Neon project ID to operate on
|
|
52
|
+
* @param parentBranchId - The parent branch ID for the new branch. If omitted or empty, the branch will be created from the project's default branch.
|
|
53
|
+
* @param schemaOnly - Whether to create a schema-only branch (default: false)
|
|
54
|
+
* @param endpoint - The type of connection to create (pooler is recommended)
|
|
55
|
+
* @param deleteBranch - Delete the test branch in afterAll (default: true). Disabling this will leave each test branch in the Neon project after the test suite runs
|
|
56
|
+
* @returns A setup/teardown function for Vitest test suites
|
|
57
|
+
*
|
|
58
|
+
* Side effects:
|
|
59
|
+
* - Sets the `DATABASE_URL` environment variable to the connection URI for the
|
|
60
|
+
* new branch
|
|
61
|
+
* - Deletes the test branch after the test suite runs
|
|
62
|
+
*/
|
|
63
|
+
export declare function makeNeonTesting(factoryOptions: NeonTestingOptions): {
|
|
64
|
+
(overrides?: NeonTestingOverrides): void;
|
|
65
|
+
deleteAllTestBranches: () => Promise<void>;
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AA8BA,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,QAAQ,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC/B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,mEAAmE;AACnE,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE,QAAQ,CAAC,CAAC;AAE/E;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,cAAc,EAAE,kBAAkB;iBA0BlD,oBAAoB;;EAmGnC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* https://neon.com/docs/reference/typescript-sdk
|
|
3
|
+
*/
|
|
4
|
+
import { createApiClient, EndpointType, } from "@neondatabase/api-client";
|
|
5
|
+
import { afterAll, beforeAll } from "vitest";
|
|
6
|
+
import { neonConfig } from "@neondatabase/serverless";
|
|
7
|
+
/**
|
|
8
|
+
* Creates a PostgreSQL connection URI from connection parameters
|
|
9
|
+
*
|
|
10
|
+
* @param connectionParameters - The connection parameters object
|
|
11
|
+
* @param type - The type of connection to create (pooler or direct)
|
|
12
|
+
* @returns A PostgreSQL connection URI string
|
|
13
|
+
*/
|
|
14
|
+
function createConnectionUri(connectionParameters, type) {
|
|
15
|
+
const { role, password, host, pooler_host, database } = connectionParameters.connection_parameters;
|
|
16
|
+
const hostname = type === "pooler" ? pooler_host : host;
|
|
17
|
+
return `postgresql://${role}:${password}@${hostname}/${database}?sslmode=require`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Factory function that creates a Neon test database setup/teardown function
|
|
21
|
+
* for Vitest test suites.
|
|
22
|
+
*
|
|
23
|
+
* @param apiKey - The Neon API key, this is used to create and teardown test branches
|
|
24
|
+
* @param projectId - The Neon project ID to operate on
|
|
25
|
+
* @param parentBranchId - The parent branch ID for the new branch. If omitted or empty, the branch will be created from the project's default branch.
|
|
26
|
+
* @param schemaOnly - Whether to create a schema-only branch (default: false)
|
|
27
|
+
* @param endpoint - The type of connection to create (pooler is recommended)
|
|
28
|
+
* @param deleteBranch - Delete the test branch in afterAll (default: true). Disabling this will leave each test branch in the Neon project after the test suite runs
|
|
29
|
+
* @returns A setup/teardown function for Vitest test suites
|
|
30
|
+
*
|
|
31
|
+
* Side effects:
|
|
32
|
+
* - Sets the `DATABASE_URL` environment variable to the connection URI for the
|
|
33
|
+
* new branch
|
|
34
|
+
* - Deletes the test branch after the test suite runs
|
|
35
|
+
*/
|
|
36
|
+
export function makeNeonTesting(factoryOptions) {
|
|
37
|
+
const apiClient = createApiClient({ apiKey: factoryOptions.apiKey });
|
|
38
|
+
/**
|
|
39
|
+
* Delete all test branches
|
|
40
|
+
*/
|
|
41
|
+
async function deleteAllTestBranches() {
|
|
42
|
+
const { data } = await apiClient.listProjectBranches({
|
|
43
|
+
projectId: factoryOptions.projectId,
|
|
44
|
+
});
|
|
45
|
+
for (const branch of data.branches) {
|
|
46
|
+
const isTestBranch = data.annotations[branch.id]?.value["integration-test"] === "true";
|
|
47
|
+
if (isTestBranch) {
|
|
48
|
+
await apiClient.deleteProjectBranch(factoryOptions.projectId, branch.id);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const testDbSetup = (
|
|
53
|
+
/** Override any factory options except apiKey */
|
|
54
|
+
overrides) => {
|
|
55
|
+
// Merge factory options with overrides
|
|
56
|
+
const options = { ...factoryOptions, ...overrides };
|
|
57
|
+
// Each test file gets its own branch ID and database client
|
|
58
|
+
let branchId;
|
|
59
|
+
// List of tracked Neon WebSocket connections
|
|
60
|
+
const neonSockets = new Set();
|
|
61
|
+
// Custom WebSocket constructor that tracks Neon WebSocket connections
|
|
62
|
+
class TrackingWebSocket extends WebSocket {
|
|
63
|
+
constructor(url) {
|
|
64
|
+
super(url);
|
|
65
|
+
// Only track Neon WebSocket connections
|
|
66
|
+
if (!url.includes(".neon.tech/"))
|
|
67
|
+
return;
|
|
68
|
+
neonSockets.add(this);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create a new test branch
|
|
73
|
+
*
|
|
74
|
+
* @returns The connection URI for the new branch
|
|
75
|
+
*/
|
|
76
|
+
async function createBranch() {
|
|
77
|
+
const { data } = await apiClient.createProjectBranch(options.projectId, {
|
|
78
|
+
branch: {
|
|
79
|
+
name: `test/${crypto.randomUUID()}`,
|
|
80
|
+
parent_id: options.parentBranchId,
|
|
81
|
+
init_source: options.schemaOnly ? "schema-only" : undefined,
|
|
82
|
+
},
|
|
83
|
+
endpoints: [{ type: EndpointType.ReadWrite }],
|
|
84
|
+
annotation_value: {
|
|
85
|
+
"integration-test": "true",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
branchId = data.branch.id;
|
|
89
|
+
const [connectionUri] = data.connection_uris ?? [];
|
|
90
|
+
if (!connectionUri) {
|
|
91
|
+
throw new Error("No connection URI found");
|
|
92
|
+
}
|
|
93
|
+
return createConnectionUri(connectionUri, options.endpoint ?? "pooler");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Delete the test branch
|
|
97
|
+
*/
|
|
98
|
+
async function deleteBranch() {
|
|
99
|
+
if (!branchId) {
|
|
100
|
+
throw new Error("No branch to delete");
|
|
101
|
+
}
|
|
102
|
+
await apiClient.deleteProjectBranch(options.projectId, branchId);
|
|
103
|
+
branchId = undefined;
|
|
104
|
+
}
|
|
105
|
+
beforeAll(async () => {
|
|
106
|
+
process.env.DATABASE_URL = await withRetry(createBranch, {
|
|
107
|
+
maxRetries: 5,
|
|
108
|
+
baseDelayMs: 1000,
|
|
109
|
+
});
|
|
110
|
+
if (options.autoCloseWebSockets) {
|
|
111
|
+
// Install a custom WebSocket constructor that tracks Neon WebSocket
|
|
112
|
+
// connections and closes them before deleting the branch
|
|
113
|
+
neonConfig.webSocketConstructor = TrackingWebSocket;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
afterAll(async () => {
|
|
117
|
+
process.env.DATABASE_URL = undefined;
|
|
118
|
+
// Close all tracked Neon WebSocket connections before deleting the branch
|
|
119
|
+
if (options.autoCloseWebSockets) {
|
|
120
|
+
// Suppress Neon WebSocket "Connection terminated unexpectedly" error
|
|
121
|
+
process.prependListener("uncaughtException", neonWsErrorHandler);
|
|
122
|
+
// Close tracked Neon WebSocket connections before deleting the branch
|
|
123
|
+
neonSockets.forEach((ws) => ws.close());
|
|
124
|
+
}
|
|
125
|
+
if (options.deleteBranch !== false) {
|
|
126
|
+
await deleteBranch();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
// Attach the utility
|
|
131
|
+
testDbSetup.deleteAllTestBranches = deleteAllTestBranches;
|
|
132
|
+
return testDbSetup;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Error handler: Suppress Neon WebSocket "Connection terminated unexpectedly"
|
|
136
|
+
* error
|
|
137
|
+
*/
|
|
138
|
+
const neonWsErrorHandler = (error) => {
|
|
139
|
+
const isNeonWsClose = error.message.includes("Connection terminated unexpectedly") &&
|
|
140
|
+
error.stack?.includes("@neondatabase/serverless");
|
|
141
|
+
if (isNeonWsClose) {
|
|
142
|
+
// Swallow this specific Neon WS termination error
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// For any other error, detach and rethrow
|
|
146
|
+
throw error;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Reusable API call wrapper with automatic retry on 423 errors with exponential
|
|
150
|
+
* backoff
|
|
151
|
+
*
|
|
152
|
+
* https://neon.com/docs/reference/typescript-sdk#error-handling
|
|
153
|
+
* https://neon.com/docs/changelog/2022-07-20
|
|
154
|
+
*/
|
|
155
|
+
async function withRetry(fn, options) {
|
|
156
|
+
if (!Number.isInteger(options.maxRetries) || options.maxRetries <= 0) {
|
|
157
|
+
throw new Error("maxRetries must be a positive integer");
|
|
158
|
+
}
|
|
159
|
+
if (!Number.isInteger(options.baseDelayMs) || options.baseDelayMs <= 0) {
|
|
160
|
+
throw new Error("baseDelayMs must be a positive integer");
|
|
161
|
+
}
|
|
162
|
+
for (let attempt = 1; attempt <= options.maxRetries; attempt++) {
|
|
163
|
+
try {
|
|
164
|
+
return await fn();
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const status = error?.response?.status;
|
|
168
|
+
if (status === 423 && attempt < options.maxRetries) {
|
|
169
|
+
const delay = options.baseDelayMs * Math.pow(2, attempt - 1);
|
|
170
|
+
console.log(`API call failed with 423, retrying in ${delay}ms (attempt ${attempt}/${options.maxRetries})`);
|
|
171
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
throw new Error("apiCallWithRetry reached unexpected end");
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"singleton.d.ts","sourceRoot":"","sources":["../singleton.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,CAM1D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"singleton.js","sourceRoot":"","sources":["../singleton.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,aAAa,CAAI,OAAgB;IAC/C,IAAI,QAAuB,CAAC;IAC5B,OAAO,GAAG,EAAE;QACV,QAAQ,KAAK,OAAO,EAAE,CAAC;QACvB,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../vite-plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,wBAAgB,WAAW,IAAI,MAAM,CAqBpC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
export function neonTesting() {
|
|
3
|
+
return {
|
|
4
|
+
name: "neon-testing-plugin",
|
|
5
|
+
// Run as late as possible to reduce the risk of other plugins restoring
|
|
6
|
+
// DATABASE_URL after we clear it
|
|
7
|
+
enforce: "post",
|
|
8
|
+
config(user) {
|
|
9
|
+
const setupPath = fileURLToPath(new URL("./vitest-setup.js", import.meta.url));
|
|
10
|
+
return {
|
|
11
|
+
test: {
|
|
12
|
+
// Register the vitest-setup.js file to run after other setup files
|
|
13
|
+
setupFiles: Array.from(new Set([...(user.test?.setupFiles ?? []), setupPath])),
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=vite-plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite-plugin.js","sourceRoot":"","sources":["../vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,MAAM,UAAU,WAAW;IACzB,OAAO;QACL,IAAI,EAAE,qBAAqB;QAC3B,wEAAwE;QACxE,iCAAiC;QACjC,OAAO,EAAE,MAAM;QACf,MAAM,CAAC,IAAI;YACT,MAAM,SAAS,GAAG,aAAa,CAC7B,IAAI,GAAG,CAAC,mBAAmB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAC9C,CAAC;YAEF,OAAO;gBACL,IAAI,EAAE;oBACJ,mEAAmE;oBACnE,UAAU,EAAE,KAAK,CAAC,IAAI,CACpB,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,IAAI,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,CACvD;iBACF;aACF,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vitest-setup.d.ts","sourceRoot":"","sources":["../vitest-setup.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,QAAQ,oBAAqB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const isVitest = process.env.VITEST;
|
|
3
|
+
if (isVitest) {
|
|
4
|
+
if (process.env.DATABASE_URL) {
|
|
5
|
+
console.warn("[neon-testing] Clearing existing DATABASE_URL in test environment");
|
|
6
|
+
}
|
|
7
|
+
delete process.env.DATABASE_URL;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=vitest-setup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vitest-setup.js","sourceRoot":"","sources":["../vitest-setup.ts"],"names":[],"mappings":";AAAA,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;AAEpC,IAAI,QAAQ,EAAE,CAAC;IACb,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;QAC7B,OAAO,CAAC,IAAI,CACV,mEAAmE,CACpE,CAAC;IACJ,CAAC;IAED,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;AAClC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neon-testing",
|
|
3
|
-
"version": "2.0.1-beta.
|
|
3
|
+
"version": "2.0.1-beta.20",
|
|
4
4
|
"description": "A Vitest utility for seamless integration tests with Neon Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
@@ -16,42 +16,27 @@
|
|
|
16
16
|
"type": "module",
|
|
17
17
|
"exports": {
|
|
18
18
|
".": {
|
|
19
|
-
"types": "./index.ts",
|
|
20
|
-
"import": "./index.
|
|
21
|
-
"default": "./index.ts"
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
22
21
|
},
|
|
23
22
|
"./utils": {
|
|
24
|
-
"types": "./utils.ts",
|
|
25
|
-
"import": "./utils.
|
|
26
|
-
"default": "./utils.ts"
|
|
27
|
-
},
|
|
28
|
-
"./vite-plugin": {
|
|
29
|
-
"types": "./vite-plugin.ts",
|
|
30
|
-
"import": "./vite-plugin.ts",
|
|
31
|
-
"default": "./vite-plugin.ts"
|
|
32
|
-
},
|
|
33
|
-
"./vitest-setup": {
|
|
34
|
-
"types": "./vitest-setup.ts",
|
|
35
|
-
"import": "./vitest-setup.ts",
|
|
36
|
-
"default": "./vitest-setup.ts"
|
|
23
|
+
"types": "./dist/utils.d.ts",
|
|
24
|
+
"import": "./dist/utils.js"
|
|
37
25
|
}
|
|
38
26
|
},
|
|
39
27
|
"files": [
|
|
40
|
-
"
|
|
41
|
-
"utils.ts",
|
|
42
|
-
"singleton.ts",
|
|
43
|
-
"vite-plugin.ts",
|
|
44
|
-
"vitest-setup.ts"
|
|
28
|
+
"dist"
|
|
45
29
|
],
|
|
46
30
|
"scripts": {
|
|
31
|
+
"build": "rm -rf dist && tsc",
|
|
47
32
|
"test": "vitest",
|
|
48
33
|
"format": "prettier --write .",
|
|
49
|
-
"
|
|
50
|
-
"release:
|
|
51
|
-
"release:
|
|
52
|
-
"release:
|
|
53
|
-
"
|
|
54
|
-
"postpublish": "git push --tags"
|
|
34
|
+
"check": "bun run build && bunx vitest run && bunx prettier --check .",
|
|
35
|
+
"release:patch": "bun run check && bun pm version patch && bun publish --tag latest",
|
|
36
|
+
"release:minor": "bun run check && bun pm version minor && bun publish --tag latest",
|
|
37
|
+
"release:major": "bun run check && bun pm version major && bun publish --tag latest",
|
|
38
|
+
"release:beta": "bun run check && bun pm version prerelease --preid=beta && bun publish --tag beta",
|
|
39
|
+
"postpublish": "git push --follow-tags"
|
|
55
40
|
},
|
|
56
41
|
"dependencies": {
|
|
57
42
|
"@neondatabase/api-client": "^2.2.0"
|
package/index.ts
DELETED
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* https://neon.com/docs/reference/typescript-sdk
|
|
3
|
-
*/
|
|
4
|
-
import {
|
|
5
|
-
createApiClient,
|
|
6
|
-
EndpointType,
|
|
7
|
-
type ConnectionDetails,
|
|
8
|
-
} from "@neondatabase/api-client";
|
|
9
|
-
import { afterAll, beforeAll } from "vitest";
|
|
10
|
-
import { neonConfig } from "@neondatabase/serverless";
|
|
11
|
-
|
|
12
|
-
export * from "./utils";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Creates a PostgreSQL connection URI from connection parameters
|
|
16
|
-
*
|
|
17
|
-
* @param connectionParameters - The connection parameters object
|
|
18
|
-
* @param type - The type of connection to create (pooler or direct)
|
|
19
|
-
* @returns A PostgreSQL connection URI string
|
|
20
|
-
*/
|
|
21
|
-
function createConnectionUri(
|
|
22
|
-
connectionParameters: ConnectionDetails,
|
|
23
|
-
type: "pooler" | "direct",
|
|
24
|
-
) {
|
|
25
|
-
const { role, password, host, pooler_host, database } =
|
|
26
|
-
connectionParameters.connection_parameters;
|
|
27
|
-
|
|
28
|
-
const hostname = type === "pooler" ? pooler_host : host;
|
|
29
|
-
|
|
30
|
-
return `postgresql://${role}:${password}@${hostname}/${database}?sslmode=require`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface NeonTestingOptions {
|
|
34
|
-
/**
|
|
35
|
-
* The Neon API key, this is used to create and teardown test branches
|
|
36
|
-
*
|
|
37
|
-
* https://neon.com/docs/manage/api-keys#creating-api-keys
|
|
38
|
-
*/
|
|
39
|
-
apiKey: string;
|
|
40
|
-
/**
|
|
41
|
-
* The Neon project ID to operate on
|
|
42
|
-
*
|
|
43
|
-
* https://console.neon.tech/app/projects
|
|
44
|
-
*/
|
|
45
|
-
projectId: string;
|
|
46
|
-
/**
|
|
47
|
-
* The parent branch ID for the new branch. If omitted or empty, the branch
|
|
48
|
-
* will be created from the project's default branch.
|
|
49
|
-
*/
|
|
50
|
-
parentBranchId?: string;
|
|
51
|
-
/**
|
|
52
|
-
* Whether to create a schema-only branch (default: false)
|
|
53
|
-
*/
|
|
54
|
-
schemaOnly?: boolean;
|
|
55
|
-
/**
|
|
56
|
-
* The type of connection to create (pooler is recommended)
|
|
57
|
-
*/
|
|
58
|
-
endpoint?: "pooler" | "direct";
|
|
59
|
-
/**
|
|
60
|
-
* Delete the test branch in afterAll (default: true)
|
|
61
|
-
*
|
|
62
|
-
* Disabling this will leave each test branch in the Neon project after the
|
|
63
|
-
* test suite runs
|
|
64
|
-
*/
|
|
65
|
-
deleteBranch?: boolean;
|
|
66
|
-
/**
|
|
67
|
-
* Automatically close Neon WebSocket connections opened during tests before
|
|
68
|
-
* deleting the branch (default: false)
|
|
69
|
-
*
|
|
70
|
-
* Suppresses the specific Neon WebSocket "Connection terminated unexpectedly"
|
|
71
|
-
* error that may surface when deleting a branch with open WebSocket
|
|
72
|
-
* connections
|
|
73
|
-
*/
|
|
74
|
-
autoCloseWebSockets?: boolean;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Options for overriding test database setup (excludes apiKey) */
|
|
78
|
-
export type NeonTestingOverrides = Omit<Partial<NeonTestingOptions>, "apiKey">;
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Factory function that creates a Neon test database setup/teardown function
|
|
82
|
-
* for Vitest test suites.
|
|
83
|
-
*
|
|
84
|
-
* @param apiKey - The Neon API key, this is used to create and teardown test branches
|
|
85
|
-
* @param projectId - The Neon project ID to operate on
|
|
86
|
-
* @param parentBranchId - The parent branch ID for the new branch. If omitted or empty, the branch will be created from the project's default branch.
|
|
87
|
-
* @param schemaOnly - Whether to create a schema-only branch (default: false)
|
|
88
|
-
* @param endpoint - The type of connection to create (pooler is recommended)
|
|
89
|
-
* @param deleteBranch - Delete the test branch in afterAll (default: true). Disabling this will leave each test branch in the Neon project after the test suite runs
|
|
90
|
-
* @returns A setup/teardown function for Vitest test suites
|
|
91
|
-
*
|
|
92
|
-
* Side effects:
|
|
93
|
-
* - Sets the `DATABASE_URL` environment variable to the connection URI for the
|
|
94
|
-
* new branch
|
|
95
|
-
* - Deletes the test branch after the test suite runs
|
|
96
|
-
*/
|
|
97
|
-
export function makeNeonTesting(factoryOptions: NeonTestingOptions) {
|
|
98
|
-
const apiClient = createApiClient({ apiKey: factoryOptions.apiKey });
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Delete all test branches
|
|
102
|
-
*/
|
|
103
|
-
async function deleteAllTestBranches() {
|
|
104
|
-
const { data } = await apiClient.listProjectBranches({
|
|
105
|
-
projectId: factoryOptions.projectId,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
for (const branch of data.branches) {
|
|
109
|
-
const isTestBranch =
|
|
110
|
-
data.annotations[branch.id]?.value["integration-test"] === "true";
|
|
111
|
-
|
|
112
|
-
if (isTestBranch) {
|
|
113
|
-
await apiClient.deleteProjectBranch(
|
|
114
|
-
factoryOptions.projectId,
|
|
115
|
-
branch.id,
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const testDbSetup = (
|
|
122
|
-
/** Override any factory options except apiKey */
|
|
123
|
-
overrides?: NeonTestingOverrides,
|
|
124
|
-
) => {
|
|
125
|
-
// Merge factory options with overrides
|
|
126
|
-
const options = { ...factoryOptions, ...overrides };
|
|
127
|
-
|
|
128
|
-
// Each test file gets its own branch ID and database client
|
|
129
|
-
let branchId: string | undefined;
|
|
130
|
-
|
|
131
|
-
// List of tracked Neon WebSocket connections
|
|
132
|
-
const neonSockets = new Set<WebSocket>();
|
|
133
|
-
|
|
134
|
-
// Custom WebSocket constructor that tracks Neon WebSocket connections
|
|
135
|
-
class TrackingWebSocket extends WebSocket {
|
|
136
|
-
constructor(url: string) {
|
|
137
|
-
super(url);
|
|
138
|
-
|
|
139
|
-
// Only track Neon WebSocket connections
|
|
140
|
-
if (!url.includes(".neon.tech/")) return;
|
|
141
|
-
|
|
142
|
-
neonSockets.add(this);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Create a new test branch
|
|
148
|
-
*
|
|
149
|
-
* @returns The connection URI for the new branch
|
|
150
|
-
*/
|
|
151
|
-
async function createBranch() {
|
|
152
|
-
const { data } = await apiClient.createProjectBranch(options.projectId, {
|
|
153
|
-
branch: {
|
|
154
|
-
name: `test/${crypto.randomUUID()}`,
|
|
155
|
-
parent_id: options.parentBranchId,
|
|
156
|
-
init_source: options.schemaOnly ? "schema-only" : undefined,
|
|
157
|
-
},
|
|
158
|
-
endpoints: [{ type: EndpointType.ReadWrite }],
|
|
159
|
-
annotation_value: {
|
|
160
|
-
"integration-test": "true",
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
branchId = data.branch.id;
|
|
165
|
-
|
|
166
|
-
const [connectionUri] = data.connection_uris ?? [];
|
|
167
|
-
|
|
168
|
-
if (!connectionUri) {
|
|
169
|
-
throw new Error("No connection URI found");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return createConnectionUri(connectionUri, options.endpoint ?? "pooler");
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Delete the test branch
|
|
177
|
-
*/
|
|
178
|
-
async function deleteBranch() {
|
|
179
|
-
if (!branchId) {
|
|
180
|
-
throw new Error("No branch to delete");
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
await apiClient.deleteProjectBranch(options.projectId, branchId);
|
|
184
|
-
branchId = undefined;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
beforeAll(async () => {
|
|
188
|
-
process.env.DATABASE_URL = await withRetry(createBranch, {
|
|
189
|
-
maxRetries: 5,
|
|
190
|
-
baseDelayMs: 1000,
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
if (options.autoCloseWebSockets) {
|
|
194
|
-
// Install a custom WebSocket constructor that tracks Neon WebSocket
|
|
195
|
-
// connections and closes them before deleting the branch
|
|
196
|
-
neonConfig.webSocketConstructor = TrackingWebSocket;
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
afterAll(async () => {
|
|
201
|
-
process.env.DATABASE_URL = undefined;
|
|
202
|
-
|
|
203
|
-
// Close all tracked Neon WebSocket connections before deleting the branch
|
|
204
|
-
if (options.autoCloseWebSockets) {
|
|
205
|
-
// Suppress Neon WebSocket "Connection terminated unexpectedly" error
|
|
206
|
-
process.prependListener("uncaughtException", neonWsErrorHandler);
|
|
207
|
-
|
|
208
|
-
// Close tracked Neon WebSocket connections before deleting the branch
|
|
209
|
-
neonSockets.forEach((ws) => ws.close());
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (options.deleteBranch !== false) {
|
|
213
|
-
await deleteBranch();
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
// Attach the utility
|
|
219
|
-
testDbSetup.deleteAllTestBranches = deleteAllTestBranches;
|
|
220
|
-
|
|
221
|
-
return testDbSetup;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Error handler: Suppress Neon WebSocket "Connection terminated unexpectedly"
|
|
226
|
-
* error
|
|
227
|
-
*/
|
|
228
|
-
const neonWsErrorHandler = (error: Error) => {
|
|
229
|
-
const isNeonWsClose =
|
|
230
|
-
error.message.includes("Connection terminated unexpectedly") &&
|
|
231
|
-
error.stack?.includes("@neondatabase/serverless");
|
|
232
|
-
|
|
233
|
-
if (isNeonWsClose) {
|
|
234
|
-
// Swallow this specific Neon WS termination error
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// For any other error, detach and rethrow
|
|
239
|
-
throw error;
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Reusable API call wrapper with automatic retry on 423 errors with exponential
|
|
244
|
-
* backoff
|
|
245
|
-
*
|
|
246
|
-
* https://neon.com/docs/reference/typescript-sdk#error-handling
|
|
247
|
-
* https://neon.com/docs/changelog/2022-07-20
|
|
248
|
-
*/
|
|
249
|
-
async function withRetry<T>(
|
|
250
|
-
fn: () => Promise<T>,
|
|
251
|
-
options: {
|
|
252
|
-
maxRetries: number;
|
|
253
|
-
baseDelayMs: number;
|
|
254
|
-
},
|
|
255
|
-
): Promise<T> {
|
|
256
|
-
if (!Number.isInteger(options.maxRetries) || options.maxRetries <= 0) {
|
|
257
|
-
throw new Error("maxRetries must be a positive integer");
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (!Number.isInteger(options.baseDelayMs) || options.baseDelayMs <= 0) {
|
|
261
|
-
throw new Error("baseDelayMs must be a positive integer");
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
for (let attempt = 1; attempt <= options.maxRetries; attempt++) {
|
|
265
|
-
try {
|
|
266
|
-
return await fn();
|
|
267
|
-
} catch (error: any) {
|
|
268
|
-
const status = error?.response?.status;
|
|
269
|
-
|
|
270
|
-
if (status === 423 && attempt < options.maxRetries) {
|
|
271
|
-
const delay = options.baseDelayMs * Math.pow(2, attempt - 1);
|
|
272
|
-
|
|
273
|
-
console.log(
|
|
274
|
-
`API call failed with 423, retrying in ${delay}ms (attempt ${attempt}/${options.maxRetries})`,
|
|
275
|
-
);
|
|
276
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
277
|
-
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
throw error;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
throw new Error("apiCallWithRetry reached unexpected end");
|
|
285
|
-
}
|
package/singleton.ts
DELETED
package/utils.ts
DELETED
package/vite-plugin.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from "node:url";
|
|
2
|
-
import type { Plugin, UserConfig } from "vite";
|
|
3
|
-
|
|
4
|
-
export function neonTesting() {
|
|
5
|
-
return {
|
|
6
|
-
name: "neon-testing-plugin",
|
|
7
|
-
enforce: "pre",
|
|
8
|
-
config(user: any) {
|
|
9
|
-
const setupPath = fileURLToPath(
|
|
10
|
-
new URL("./vitest-setup.ts", import.meta.url),
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
const setup = new Set([...(user.test?.setupFiles ?? []), setupPath]);
|
|
14
|
-
|
|
15
|
-
return {
|
|
16
|
-
test: {
|
|
17
|
-
setupFiles: Array.from(setup),
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
},
|
|
21
|
-
} satisfies Plugin;
|
|
22
|
-
}
|
package/vitest-setup.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
const isVitest =
|
|
2
|
-
process.env.VITEST === "true" || !!process.env.VITEST_WORKER_ID;
|
|
3
|
-
|
|
4
|
-
if (isVitest) {
|
|
5
|
-
if (process.env.DATABASE_URL) {
|
|
6
|
-
console.warn(
|
|
7
|
-
"[neon-testing] Clearing existing DATABASE_URL in test environment",
|
|
8
|
-
);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
delete process.env.DATABASE_URL;
|
|
12
|
-
}
|