@gallopsystems/agent-skills 1.0.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 +137 -0
- package/package.json +26 -0
- package/plugins/doctl/.claude-plugin/plugin.json +8 -0
- package/plugins/doctl/skills/doctl/SKILL.md +93 -0
- package/plugins/kysely-postgres/.claude-plugin/plugin.json +8 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/SKILL.md +1101 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/aggregations.ts +167 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/ctes.ts +165 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/expressions.ts +272 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/joins.ts +206 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/json-arrays.ts +398 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/mutations.ts +199 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/orderby-pagination.ts +117 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/relations.ts +176 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/select-where.ts +146 -0
- package/plugins/linear/.claude-plugin/plugin.json +8 -0
- package/plugins/linear/skills/linear/SKILL.md +1040 -0
- package/plugins/linear/skills/linear/bin/linear.mjs +1228 -0
- package/plugins/linear/skills/linear/tech-stack.md +273 -0
- package/plugins/nitro-testing/.claude-plugin/plugin.json +8 -0
- package/plugins/nitro-testing/skills/nitro-testing/SKILL.md +497 -0
- package/plugins/nitro-testing/skills/nitro-testing/async-testing.md +270 -0
- package/plugins/nitro-testing/skills/nitro-testing/ci-setup.md +226 -0
- package/plugins/nitro-testing/skills/nitro-testing/examples/global-setup.ts +90 -0
- package/plugins/nitro-testing/skills/nitro-testing/examples/handler.test.ts +167 -0
- package/plugins/nitro-testing/skills/nitro-testing/examples/setup.ts +29 -0
- package/plugins/nitro-testing/skills/nitro-testing/examples/test-utils-index.ts +297 -0
- package/plugins/nitro-testing/skills/nitro-testing/examples/vitest.config.ts +42 -0
- package/plugins/nitro-testing/skills/nitro-testing/factories.md +278 -0
- package/plugins/nitro-testing/skills/nitro-testing/frontend-testing.md +512 -0
- package/plugins/nitro-testing/skills/nitro-testing/test-utils.md +262 -0
- package/plugins/nitro-testing/skills/nitro-testing/transaction-rollback.md +183 -0
- package/plugins/nitro-testing/skills/nitro-testing/vitest-config.md +236 -0
- package/plugins/nuxt-nitro-api/.claude-plugin/plugin.json +8 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/SKILL.md +260 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/auth-patterns.md +228 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/composables-utils.md +174 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/deep-linking.md +190 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/auth-middleware.ts +32 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/auth-utils.ts +51 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/deep-link-page.vue +61 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/service-util.ts +63 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/sse-endpoint.ts +59 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/validation-endpoint.ts +38 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/fetch-patterns.md +178 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/nitro-tasks.md +243 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/page-structure.md +162 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/server-services.md +238 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/sse.md +221 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/ssr-client.md +166 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/validation.md +131 -0
- package/scripts/link-skills.mjs +252 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# Test Utilities
|
|
2
|
+
|
|
3
|
+
> **Example:** [test-utils-index.ts](./examples/test-utils-index.ts)
|
|
4
|
+
|
|
5
|
+
Mock event creators, global stubs, and assertion helpers for testing Nitro handlers.
|
|
6
|
+
|
|
7
|
+
## Mock Event Helpers
|
|
8
|
+
|
|
9
|
+
Create H3 events for testing handlers:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { createEvent } from "h3";
|
|
13
|
+
import { IncomingMessage, ServerResponse } from "http";
|
|
14
|
+
import { Socket } from "net";
|
|
15
|
+
|
|
16
|
+
type Params = Record<string, string | number>;
|
|
17
|
+
|
|
18
|
+
export function createMockEvent(options: {
|
|
19
|
+
method?: string;
|
|
20
|
+
params?: Params;
|
|
21
|
+
body?: unknown;
|
|
22
|
+
query?: Record<string, string>;
|
|
23
|
+
}) {
|
|
24
|
+
const { method = "GET", params = {}, body, query = {} } = options;
|
|
25
|
+
|
|
26
|
+
const socket = new Socket();
|
|
27
|
+
const req = new IncomingMessage(socket);
|
|
28
|
+
req.method = method;
|
|
29
|
+
req.url = "/" + (Object.keys(query).length
|
|
30
|
+
? "?" + new URLSearchParams(query).toString()
|
|
31
|
+
: "");
|
|
32
|
+
|
|
33
|
+
const res = new ServerResponse(req);
|
|
34
|
+
const event = createEvent(req, res);
|
|
35
|
+
|
|
36
|
+
// Set route params
|
|
37
|
+
event.context.params = Object.fromEntries(
|
|
38
|
+
Object.entries(params).map(([k, v]) => [k, String(v)])
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Set mock body and query for validation functions
|
|
42
|
+
if (body !== undefined) {
|
|
43
|
+
event.context._mockBody = body;
|
|
44
|
+
}
|
|
45
|
+
if (Object.keys(query).length > 0) {
|
|
46
|
+
event.context._mockQuery = query;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return event;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Convenience helpers
|
|
53
|
+
export function mockGet(params: Params, query?: Record<string, string>) {
|
|
54
|
+
return createMockEvent({ method: "GET", params, query });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function mockPost(params: Params, body: unknown) {
|
|
58
|
+
return createMockEvent({ method: "POST", params, body });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function mockPatch(params: Params, body: unknown) {
|
|
62
|
+
return createMockEvent({ method: "PATCH", params, body });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function mockDelete(params: Params) {
|
|
66
|
+
return createMockEvent({ method: "DELETE", params });
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Usage
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// GET /api/users/123?include=profile
|
|
74
|
+
const event = mockGet({ id: 123 }, { include: "profile" });
|
|
75
|
+
|
|
76
|
+
// POST /api/users with JSON body
|
|
77
|
+
const event = mockPost({}, { email: "test@example.com", name: "Test" });
|
|
78
|
+
|
|
79
|
+
// PATCH /api/users/123 with partial update
|
|
80
|
+
const event = mockPatch({ id: 123 }, { status: "active" });
|
|
81
|
+
|
|
82
|
+
// DELETE /api/users/123
|
|
83
|
+
const event = mockDelete({ id: 123 });
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Global Stubs
|
|
87
|
+
|
|
88
|
+
Stub Nuxt auto-imports so handlers work in test environment:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { vi } from "vitest";
|
|
92
|
+
|
|
93
|
+
export async function setupHandlerMocks() {
|
|
94
|
+
// Unwrap defineEventHandler
|
|
95
|
+
vi.stubGlobal("defineEventHandler", (handler: Function) => handler);
|
|
96
|
+
|
|
97
|
+
// Default test user
|
|
98
|
+
vi.stubGlobal("getUserSession", async () => ({
|
|
99
|
+
user: { id: 1, firstName: "Test", lastName: "User", role: "admin" },
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
vi.stubGlobal("setUserSession", async () => {});
|
|
103
|
+
|
|
104
|
+
// Database access (currentTrx set by test fixture)
|
|
105
|
+
vi.stubGlobal("useDatabase", () => {
|
|
106
|
+
if (!currentTrx) {
|
|
107
|
+
throw new Error("useDatabase called outside of test transaction");
|
|
108
|
+
}
|
|
109
|
+
return currentTrx;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Error helper
|
|
113
|
+
vi.stubGlobal("createError", (opts: {
|
|
114
|
+
statusCode: number;
|
|
115
|
+
message?: string;
|
|
116
|
+
statusMessage?: string;
|
|
117
|
+
data?: unknown;
|
|
118
|
+
}) => {
|
|
119
|
+
const error = new Error(opts.message || opts.statusMessage || "") as any;
|
|
120
|
+
error.statusCode = opts.statusCode;
|
|
121
|
+
if (opts.data !== undefined) error.data = opts.data;
|
|
122
|
+
return error;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Route params
|
|
126
|
+
vi.stubGlobal("getRouterParam", (event: any, param: string) => {
|
|
127
|
+
return event.context.params?.[param];
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
vi.stubGlobal("getValidatedRouterParams", async (event: any, validate: Function) => {
|
|
131
|
+
return validate(event.context.params ?? {});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Query params
|
|
135
|
+
vi.stubGlobal("getQuery", (event: any) => {
|
|
136
|
+
return event.context._mockQuery ?? {};
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
vi.stubGlobal("getValidatedQuery", async (event: any, validate: Function) => {
|
|
140
|
+
return validate(event.context._mockQuery ?? {});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Request body
|
|
144
|
+
vi.stubGlobal("readBody", async (event: any) => event.context._mockBody);
|
|
145
|
+
|
|
146
|
+
vi.stubGlobal("readValidatedBody", async (event: any, validate: Function) => {
|
|
147
|
+
return validate(event.context._mockBody ?? {});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Assertion Helpers
|
|
153
|
+
|
|
154
|
+
### expectHttpError
|
|
155
|
+
|
|
156
|
+
Test that handlers throw proper HTTP errors:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
export async function expectHttpError(
|
|
160
|
+
promise: Promise<unknown>,
|
|
161
|
+
expected: { statusCode: number; message?: string }
|
|
162
|
+
) {
|
|
163
|
+
await expect(promise).rejects.toMatchObject(expected);
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Usage
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
test("returns 404 for non-existent user", async ({ factories: _ }) => {
|
|
171
|
+
const event = mockGet({ id: 999999 });
|
|
172
|
+
|
|
173
|
+
await expectHttpError(handler(event), {
|
|
174
|
+
statusCode: 404,
|
|
175
|
+
message: "User not found",
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("returns 400 for invalid input", async ({ factories: _ }) => {
|
|
180
|
+
const event = mockPost({}, { invalidField: "bad" });
|
|
181
|
+
|
|
182
|
+
await expectHttpError(handler(event), { statusCode: 400 });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("returns 401 for unauthenticated request", async ({ factories: _ }) => {
|
|
186
|
+
// Override default session
|
|
187
|
+
vi.mocked(getUserSession).mockResolvedValueOnce({ user: null });
|
|
188
|
+
|
|
189
|
+
const event = mockGet({});
|
|
190
|
+
await expectHttpError(handler(event), { statusCode: 401 });
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Customizing Session Per Test
|
|
195
|
+
|
|
196
|
+
Override the default test user:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import { vi } from "vitest";
|
|
200
|
+
|
|
201
|
+
test("non-admin cannot delete", async ({ factories }) => {
|
|
202
|
+
// Override session for this test
|
|
203
|
+
vi.stubGlobal("getUserSession", async () => ({
|
|
204
|
+
user: { id: 2, role: "user" }, // Not admin
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
const item = await factories.item();
|
|
208
|
+
const event = mockDelete({ id: item.id });
|
|
209
|
+
|
|
210
|
+
await expectHttpError(handler(event), { statusCode: 403 });
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Or use `vi.mocked` for one-off overrides:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
test("user can only see own items", async ({ factories }) => {
|
|
218
|
+
const owner = await factories.user();
|
|
219
|
+
const other = await factories.user();
|
|
220
|
+
const item = await factories.item({ ownerId: owner.id });
|
|
221
|
+
|
|
222
|
+
// Mock as the other user
|
|
223
|
+
vi.mocked(getUserSession).mockResolvedValueOnce({
|
|
224
|
+
user: { id: other.id, role: "user" },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const event = mockGet({ id: item.id });
|
|
228
|
+
await expectHttpError(handler(event), { statusCode: 404 });
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Re-exports for Convenience
|
|
233
|
+
|
|
234
|
+
Export everything from one place:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// server/test-utils/index.ts
|
|
238
|
+
export { describe, expect, beforeAll } from "vitest";
|
|
239
|
+
export { test } from "./fixtures"; // Custom fixture
|
|
240
|
+
export {
|
|
241
|
+
createMockEvent,
|
|
242
|
+
mockGet,
|
|
243
|
+
mockPost,
|
|
244
|
+
mockPatch,
|
|
245
|
+
mockDelete,
|
|
246
|
+
expectHttpError,
|
|
247
|
+
} from "./helpers";
|
|
248
|
+
export { setupHandlerMocks } from "./stubs";
|
|
249
|
+
export type { Factories } from "./factories";
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Then in tests:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import {
|
|
256
|
+
describe,
|
|
257
|
+
test,
|
|
258
|
+
expect,
|
|
259
|
+
mockPost,
|
|
260
|
+
expectHttpError,
|
|
261
|
+
} from "~/server/test-utils";
|
|
262
|
+
```
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Transaction Rollback Pattern
|
|
2
|
+
|
|
3
|
+
> **Example:** [test-utils-index.ts](./examples/test-utils-index.ts)
|
|
4
|
+
|
|
5
|
+
The core isolation pattern: each test runs inside a database transaction that auto-rolls back.
|
|
6
|
+
|
|
7
|
+
## Why This Pattern?
|
|
8
|
+
|
|
9
|
+
| Approach | Speed | Isolation | Real SQL |
|
|
10
|
+
|----------|-------|-----------|----------|
|
|
11
|
+
| Truncate tables | Slow | ✅ | ✅ |
|
|
12
|
+
| Mock database | Fast | ✅ | ❌ |
|
|
13
|
+
| **Transaction rollback** | **Fast** | **✅** | **✅** |
|
|
14
|
+
|
|
15
|
+
Transaction rollback gives you real SQL testing with mock-like speed.
|
|
16
|
+
|
|
17
|
+
## Implementation
|
|
18
|
+
|
|
19
|
+
### Custom Vitest Fixture
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { vi, test as base, expect } from "vitest";
|
|
23
|
+
import { db } from "../utils/db";
|
|
24
|
+
import type { Transaction } from "kysely";
|
|
25
|
+
import type { DB } from "../db/db";
|
|
26
|
+
|
|
27
|
+
// Current test transaction - handlers access via stubbed useDatabase
|
|
28
|
+
let currentTrx: Transaction<DB> | null = null;
|
|
29
|
+
|
|
30
|
+
interface TestFixtures {
|
|
31
|
+
factories: Factories;
|
|
32
|
+
db: Transaction<DB>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const test = base.extend<TestFixtures>({
|
|
36
|
+
// The factories fixture sets up the transaction
|
|
37
|
+
factories: async ({}, use) => {
|
|
38
|
+
await db.transaction().execute(async (trx) => {
|
|
39
|
+
currentTrx = trx;
|
|
40
|
+
try {
|
|
41
|
+
await use(createFactories(trx));
|
|
42
|
+
// Force rollback by throwing
|
|
43
|
+
throw { __rollback: true };
|
|
44
|
+
} finally {
|
|
45
|
+
currentTrx = null;
|
|
46
|
+
}
|
|
47
|
+
}).catch((e) => {
|
|
48
|
+
// Swallow our rollback signal
|
|
49
|
+
if (e && typeof e === "object" && "__rollback" in e) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
throw e;
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// The db fixture exposes the transaction for direct queries
|
|
57
|
+
db: async ({ factories: _ }, use) => {
|
|
58
|
+
if (!currentTrx) {
|
|
59
|
+
throw new Error("db fixture used outside transaction context");
|
|
60
|
+
}
|
|
61
|
+
await use(currentTrx);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export { describe, expect, beforeAll } from "vitest";
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Stubbing useDatabase
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// In setup.ts or as part of setupHandlerMocks()
|
|
72
|
+
vi.stubGlobal("useDatabase", () => {
|
|
73
|
+
if (!currentTrx) {
|
|
74
|
+
throw new Error("useDatabase called outside of test transaction");
|
|
75
|
+
}
|
|
76
|
+
return currentTrx;
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Now any handler code that calls `useDatabase()` gets the test transaction.
|
|
81
|
+
|
|
82
|
+
## Handling Nested Transactions
|
|
83
|
+
|
|
84
|
+
Real code often uses `db.transaction()` for atomic operations. Since tests already run in a transaction, we need to handle nested transactions:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// Patch Transaction prototype to handle nesting
|
|
88
|
+
const KyselyModule = await import("kysely");
|
|
89
|
+
const TransactionClass = (KyselyModule as any).Transaction;
|
|
90
|
+
|
|
91
|
+
if (TransactionClass?.prototype) {
|
|
92
|
+
TransactionClass.prototype.transaction = function () {
|
|
93
|
+
const self = this;
|
|
94
|
+
return {
|
|
95
|
+
execute: async <T>(callback: (trx: any) => Promise<T>): Promise<T> => {
|
|
96
|
+
// Just run callback with same transaction (no nesting)
|
|
97
|
+
return callback(self);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This makes code like this work transparently:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// In production: creates real nested transaction
|
|
108
|
+
// In tests: reuses the test transaction
|
|
109
|
+
async function createOrder(data: OrderData) {
|
|
110
|
+
return db.transaction().execute(async (trx) => {
|
|
111
|
+
const order = await trx.insertInto("order").values(data)...;
|
|
112
|
+
await trx.insertInto("order_item").values(...)...;
|
|
113
|
+
return order;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Usage Pattern
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { describe, test, expect, mockPost } from "~/server/test-utils";
|
|
122
|
+
import handler from "./index.post";
|
|
123
|
+
|
|
124
|
+
describe("POST /api/orders", () => {
|
|
125
|
+
test("creates order with items", async ({ factories, db }) => {
|
|
126
|
+
// Create test data using factories (transaction-bound)
|
|
127
|
+
const user = await factories.user();
|
|
128
|
+
const product = await factories.product({ price: 100 });
|
|
129
|
+
|
|
130
|
+
// Test the handler
|
|
131
|
+
const event = mockPost({}, {
|
|
132
|
+
userId: user.id,
|
|
133
|
+
items: [{ productId: product.id, quantity: 2 }],
|
|
134
|
+
});
|
|
135
|
+
const result = await handler(event);
|
|
136
|
+
|
|
137
|
+
// Verify in database
|
|
138
|
+
const order = await db
|
|
139
|
+
.selectFrom("order")
|
|
140
|
+
.where("id", "=", result.id)
|
|
141
|
+
.selectAll()
|
|
142
|
+
.executeTakeFirst();
|
|
143
|
+
|
|
144
|
+
expect(order?.total).toBe(200);
|
|
145
|
+
|
|
146
|
+
// Verify order items
|
|
147
|
+
const items = await db
|
|
148
|
+
.selectFrom("order_item")
|
|
149
|
+
.where("order_id", "=", result.id)
|
|
150
|
+
.selectAll()
|
|
151
|
+
.execute();
|
|
152
|
+
|
|
153
|
+
expect(items).toHaveLength(1);
|
|
154
|
+
});
|
|
155
|
+
// Transaction rolls back - database unchanged
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Key Points
|
|
160
|
+
|
|
161
|
+
1. **Always destructure `factories`** - Even if unused, it triggers transaction setup
|
|
162
|
+
2. **Use `db` fixture for assertions** - Not the real db import
|
|
163
|
+
3. **Nested transactions work** - Thanks to prototype patching
|
|
164
|
+
4. **No cleanup needed** - Rollback happens automatically
|
|
165
|
+
5. **Tests are isolated** - Can't affect each other
|
|
166
|
+
|
|
167
|
+
## Gotcha: Unused Factories
|
|
168
|
+
|
|
169
|
+
Even if you don't create test data, you need the fixture to set up the transaction:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// ❌ Wrong - no transaction, useDatabase will fail
|
|
173
|
+
test("returns empty list", async () => {
|
|
174
|
+
const result = await handler(mockGet({}));
|
|
175
|
+
expect(result).toEqual([]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ✅ Right - transaction set up via factories fixture
|
|
179
|
+
test("returns empty list", async ({ factories: _ }) => {
|
|
180
|
+
const result = await handler(mockGet({}));
|
|
181
|
+
expect(result).toEqual([]);
|
|
182
|
+
});
|
|
183
|
+
```
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Vitest Configuration
|
|
2
|
+
|
|
3
|
+
> **Example:** [vitest.config.ts](./examples/vitest.config.ts)
|
|
4
|
+
|
|
5
|
+
Configure Vitest for testing Nitro API handlers.
|
|
6
|
+
|
|
7
|
+
## Basic Configuration
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// vitest.config.ts
|
|
11
|
+
import { defineConfig } from "vitest/config";
|
|
12
|
+
import path from "path";
|
|
13
|
+
|
|
14
|
+
export default defineConfig({
|
|
15
|
+
test: {
|
|
16
|
+
// Enable globals (describe, test, expect without imports)
|
|
17
|
+
globals: true,
|
|
18
|
+
|
|
19
|
+
// Node environment (not browser)
|
|
20
|
+
environment: "node",
|
|
21
|
+
|
|
22
|
+
// Run once before all tests - reset DB and run migrations
|
|
23
|
+
globalSetup: ["./server/test-utils/global-setup.ts"],
|
|
24
|
+
|
|
25
|
+
// Run before each test file - set up stubs
|
|
26
|
+
setupFiles: ["./server/test-utils/setup.ts"],
|
|
27
|
+
|
|
28
|
+
// Coverage configuration
|
|
29
|
+
coverage: {
|
|
30
|
+
provider: "v8",
|
|
31
|
+
reporter: ["text", "html", "lcov", "json", "json-summary"],
|
|
32
|
+
include: ["server/**/*.ts"],
|
|
33
|
+
exclude: [
|
|
34
|
+
"server/**/*.test.ts",
|
|
35
|
+
"server/test-utils/**",
|
|
36
|
+
"server/db/migrations/**",
|
|
37
|
+
"server/db/db.d.ts",
|
|
38
|
+
],
|
|
39
|
+
reportsDirectory: "./coverage",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
resolve: {
|
|
44
|
+
alias: {
|
|
45
|
+
"~": path.resolve(__dirname),
|
|
46
|
+
"@": path.resolve(__dirname),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Global Setup (Runs Once)
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// server/test-utils/global-setup.ts
|
|
56
|
+
import { Kysely, PostgresDialect, Migrator, FileMigrationProvider } from "kysely";
|
|
57
|
+
import { Pool } from "pg";
|
|
58
|
+
import path from "path";
|
|
59
|
+
import { promises as fs } from "fs";
|
|
60
|
+
|
|
61
|
+
function getTestConnectionString(): string {
|
|
62
|
+
if (process.env.TEST_POSTGRESQL_CONNECTION_STRING) {
|
|
63
|
+
return process.env.TEST_POSTGRESQL_CONNECTION_STRING;
|
|
64
|
+
}
|
|
65
|
+
return "postgresql://localhost/myapp-test";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function setup() {
|
|
69
|
+
const connectionString = getTestConnectionString();
|
|
70
|
+
const pool = new Pool({ connectionString });
|
|
71
|
+
|
|
72
|
+
// Check database exists
|
|
73
|
+
try {
|
|
74
|
+
await pool.query("SELECT 1");
|
|
75
|
+
} catch (err: any) {
|
|
76
|
+
if (err.code === "3D000") {
|
|
77
|
+
console.error(`
|
|
78
|
+
╭─────────────────────────────────────────────────────╮
|
|
79
|
+
│ Error: Test database does not exist. │
|
|
80
|
+
│ │
|
|
81
|
+
│ Run: createdb myapp-test │
|
|
82
|
+
╰─────────────────────────────────────────────────────╯
|
|
83
|
+
`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Drop all tables and types for clean slate
|
|
90
|
+
await pool.query(`
|
|
91
|
+
DO $$ DECLARE r RECORD;
|
|
92
|
+
BEGIN
|
|
93
|
+
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP
|
|
94
|
+
EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
|
|
95
|
+
END LOOP;
|
|
96
|
+
FOR r IN (SELECT typname FROM pg_type t
|
|
97
|
+
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
98
|
+
WHERE n.nspname = 'public' AND t.typtype = 'e') LOOP
|
|
99
|
+
EXECUTE 'DROP TYPE IF EXISTS ' || quote_ident(r.typname) || ' CASCADE';
|
|
100
|
+
END LOOP;
|
|
101
|
+
END $$;
|
|
102
|
+
`);
|
|
103
|
+
await pool.end();
|
|
104
|
+
|
|
105
|
+
// Run migrations
|
|
106
|
+
const db = new Kysely({
|
|
107
|
+
dialect: new PostgresDialect({
|
|
108
|
+
pool: new Pool({ connectionString }),
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const migrator = new Migrator({
|
|
113
|
+
db,
|
|
114
|
+
provider: new FileMigrationProvider({
|
|
115
|
+
fs,
|
|
116
|
+
path,
|
|
117
|
+
migrationFolder: path.resolve(__dirname, "../db/migrations"),
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const { error, results } = await migrator.migrateToLatest();
|
|
122
|
+
|
|
123
|
+
if (error) {
|
|
124
|
+
console.error("Migration failed:", error);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const applied = results?.filter((r) => r.status === "Success") ?? [];
|
|
129
|
+
console.log(`Test DB ready: ${applied.length} migrations applied`);
|
|
130
|
+
|
|
131
|
+
await db.destroy();
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Setup File (Runs Per Test File)
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// server/test-utils/setup.ts
|
|
139
|
+
import { vi } from "vitest";
|
|
140
|
+
|
|
141
|
+
// Get test database connection
|
|
142
|
+
function getTestConnectionString(): string {
|
|
143
|
+
if (process.env.TEST_POSTGRESQL_CONNECTION_STRING) {
|
|
144
|
+
return process.env.TEST_POSTGRESQL_CONNECTION_STRING;
|
|
145
|
+
}
|
|
146
|
+
return "postgresql://localhost/myapp-test";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Stub useRuntimeConfig before anything else
|
|
150
|
+
vi.stubGlobal("useRuntimeConfig", () => ({
|
|
151
|
+
postgresql: {
|
|
152
|
+
connectionString: getTestConnectionString(),
|
|
153
|
+
},
|
|
154
|
+
public: {
|
|
155
|
+
environment: "test",
|
|
156
|
+
},
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
// Set up handler mocks
|
|
160
|
+
const { setupHandlerMocks } = await import("./index");
|
|
161
|
+
await setupHandlerMocks();
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Package.json Scripts
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"scripts": {
|
|
169
|
+
"test": "vitest",
|
|
170
|
+
"test:watch": "vitest --watch",
|
|
171
|
+
"test:ui": "vitest --ui",
|
|
172
|
+
"test:coverage": "vitest --coverage",
|
|
173
|
+
"test:run": "vitest run"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## TypeScript Configuration
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
// tsconfig.json - ensure vitest types are included
|
|
182
|
+
{
|
|
183
|
+
"compilerOptions": {
|
|
184
|
+
"types": ["vitest/globals"]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Test File Pattern
|
|
190
|
+
|
|
191
|
+
By default, Vitest finds files matching:
|
|
192
|
+
- `**/*.test.ts`
|
|
193
|
+
- `**/*.spec.ts`
|
|
194
|
+
|
|
195
|
+
Co-locate tests with handlers:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
server/
|
|
199
|
+
api/
|
|
200
|
+
users/
|
|
201
|
+
index.get.ts # Handler
|
|
202
|
+
index.get.test.ts # Test
|
|
203
|
+
index.post.ts
|
|
204
|
+
index.post.test.ts
|
|
205
|
+
[id].get.ts
|
|
206
|
+
[id].get.test.ts
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Environment Variables
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# Local development
|
|
213
|
+
# Uses postgresql://localhost/myapp-test by default
|
|
214
|
+
|
|
215
|
+
# CI/CD
|
|
216
|
+
TEST_POSTGRESQL_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/myapp-test
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Coverage Thresholds (Optional)
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// vitest.config.ts
|
|
223
|
+
export default defineConfig({
|
|
224
|
+
test: {
|
|
225
|
+
coverage: {
|
|
226
|
+
// Fail if coverage drops below thresholds
|
|
227
|
+
thresholds: {
|
|
228
|
+
lines: 80,
|
|
229
|
+
functions: 80,
|
|
230
|
+
branches: 70,
|
|
231
|
+
statements: 80,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
```
|