@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,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Testing a POST handler
|
|
3
|
+
*
|
|
4
|
+
* Co-located with the handler: index.post.ts -> index.post.test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
describe,
|
|
9
|
+
test,
|
|
10
|
+
expect,
|
|
11
|
+
mockPost,
|
|
12
|
+
mockGet,
|
|
13
|
+
expectHttpError,
|
|
14
|
+
} from "~/server/test-utils";
|
|
15
|
+
import handler from "./index.post";
|
|
16
|
+
import getHandler from "./[id].get";
|
|
17
|
+
|
|
18
|
+
describe("POST /api/users", () => {
|
|
19
|
+
test("creates user with valid data", async ({ factories: _, db }) => {
|
|
20
|
+
const event = mockPost({}, {
|
|
21
|
+
email: "new@example.com",
|
|
22
|
+
name: "New User",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const result = await handler(event);
|
|
26
|
+
|
|
27
|
+
// Verify response
|
|
28
|
+
expect(result.id).toBeDefined();
|
|
29
|
+
expect(result.email).toBe("new@example.com");
|
|
30
|
+
expect(result.name).toBe("New User");
|
|
31
|
+
|
|
32
|
+
// Verify persisted in database
|
|
33
|
+
const saved = await db
|
|
34
|
+
.selectFrom("user")
|
|
35
|
+
.where("id", "=", result.id)
|
|
36
|
+
.selectAll()
|
|
37
|
+
.executeTakeFirst();
|
|
38
|
+
|
|
39
|
+
expect(saved).toBeDefined();
|
|
40
|
+
expect(saved?.email).toBe("new@example.com");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("creates user with all optional fields", async ({ factories: _ }) => {
|
|
44
|
+
const event = mockPost({}, {
|
|
45
|
+
email: "full@example.com",
|
|
46
|
+
name: "Full User",
|
|
47
|
+
role: "admin",
|
|
48
|
+
metadata: { source: "api", version: 2 },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const result = await handler(event);
|
|
52
|
+
|
|
53
|
+
expect(result.role).toBe("admin");
|
|
54
|
+
expect(result.metadata).toEqual({ source: "api", version: 2 });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("sets default values", async ({ factories: _ }) => {
|
|
58
|
+
const event = mockPost({}, {
|
|
59
|
+
email: "minimal@example.com",
|
|
60
|
+
name: "Minimal",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const result = await handler(event);
|
|
64
|
+
|
|
65
|
+
expect(result.role).toBe("user"); // Default role
|
|
66
|
+
expect(result.created_at).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("throws 400 for missing required email", async ({ factories: _ }) => {
|
|
70
|
+
const event = mockPost({}, {
|
|
71
|
+
name: "No Email",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await expectHttpError(handler(event), { statusCode: 400 });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("throws 400 for missing required name", async ({ factories: _ }) => {
|
|
78
|
+
const event = mockPost({}, {
|
|
79
|
+
email: "test@example.com",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await expectHttpError(handler(event), { statusCode: 400 });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("throws 400 for invalid email format", async ({ factories: _ }) => {
|
|
86
|
+
const event = mockPost({}, {
|
|
87
|
+
email: "not-an-email",
|
|
88
|
+
name: "Test",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await expectHttpError(handler(event), { statusCode: 400 });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("throws 400 for invalid role", async ({ factories: _ }) => {
|
|
95
|
+
const event = mockPost({}, {
|
|
96
|
+
email: "test@example.com",
|
|
97
|
+
name: "Test",
|
|
98
|
+
role: "superadmin", // Not a valid role
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await expectHttpError(handler(event), { statusCode: 400 });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("throws 409 for duplicate email", async ({ factories }) => {
|
|
105
|
+
// Create existing user
|
|
106
|
+
await factories.user({ email: "existing@example.com" });
|
|
107
|
+
|
|
108
|
+
const event = mockPost({}, {
|
|
109
|
+
email: "existing@example.com",
|
|
110
|
+
name: "Duplicate",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await expectHttpError(handler(event), { statusCode: 409 });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("created user appears in GET", async ({ factories: _ }) => {
|
|
117
|
+
// Create user
|
|
118
|
+
const createEvent = mockPost({}, {
|
|
119
|
+
email: "findme@example.com",
|
|
120
|
+
name: "Find Me",
|
|
121
|
+
});
|
|
122
|
+
const created = await handler(createEvent);
|
|
123
|
+
|
|
124
|
+
// Fetch user
|
|
125
|
+
const getEvent = mockGet({ id: created.id });
|
|
126
|
+
const fetched = await getHandler(getEvent);
|
|
127
|
+
|
|
128
|
+
expect(fetched.id).toBe(created.id);
|
|
129
|
+
expect(fetched.email).toBe("findme@example.com");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("POST /api/users - with related data", () => {
|
|
134
|
+
test("creates user with organization", async ({ factories, db }) => {
|
|
135
|
+
const org = await factories.organization({ name: "Acme Corp" });
|
|
136
|
+
|
|
137
|
+
const event = mockPost({}, {
|
|
138
|
+
email: "employee@example.com",
|
|
139
|
+
name: "Employee",
|
|
140
|
+
organizationId: org.id,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const result = await handler(event);
|
|
144
|
+
|
|
145
|
+
expect(result.organization_id).toBe(org.id);
|
|
146
|
+
|
|
147
|
+
// Verify relationship
|
|
148
|
+
const user = await db
|
|
149
|
+
.selectFrom("user")
|
|
150
|
+
.innerJoin("organization", "organization.id", "user.organization_id")
|
|
151
|
+
.where("user.id", "=", result.id)
|
|
152
|
+
.select(["user.id", "organization.name as org_name"])
|
|
153
|
+
.executeTakeFirst();
|
|
154
|
+
|
|
155
|
+
expect(user?.org_name).toBe("Acme Corp");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("throws 404 for non-existent organization", async ({ factories: _ }) => {
|
|
159
|
+
const event = mockPost({}, {
|
|
160
|
+
email: "test@example.com",
|
|
161
|
+
name: "Test",
|
|
162
|
+
organizationId: 999999,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await expectHttpError(handler(event), { statusCode: 404 });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest Setup File
|
|
3
|
+
*
|
|
4
|
+
* Runs before each test file to stub Nuxt auto-imports.
|
|
5
|
+
* Database reset/migrations happen in global-setup.ts (runs once).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { vi } from "vitest";
|
|
9
|
+
|
|
10
|
+
function getTestConnectionString(): string {
|
|
11
|
+
if (process.env.TEST_POSTGRESQL_CONNECTION_STRING) {
|
|
12
|
+
return process.env.TEST_POSTGRESQL_CONNECTION_STRING;
|
|
13
|
+
}
|
|
14
|
+
return "postgresql://localhost/myapp-test";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Stub useRuntimeConfig before importing anything else
|
|
18
|
+
vi.stubGlobal("useRuntimeConfig", () => ({
|
|
19
|
+
postgresql: {
|
|
20
|
+
connectionString: getTestConnectionString(),
|
|
21
|
+
},
|
|
22
|
+
public: {
|
|
23
|
+
environment: "test",
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// Import and run handler mocks (after useRuntimeConfig is stubbed)
|
|
28
|
+
const { setupHandlerMocks } = await import("./index");
|
|
29
|
+
await setupHandlerMocks();
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helpers for testing Nuxt/Nitro API handlers with real PostgreSQL.
|
|
5
|
+
* Auto-import mocks are set up via vitest setupFiles (see setup.ts).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { vi, test as base, expect } from "vitest";
|
|
9
|
+
import { createEvent } from "h3";
|
|
10
|
+
import { IncomingMessage, ServerResponse } from "http";
|
|
11
|
+
import { Socket } from "net";
|
|
12
|
+
import { db } from "../utils/db";
|
|
13
|
+
import type { Transaction } from "kysely";
|
|
14
|
+
import type { DB } from "../db/db";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Task Registry (for async/automation testing)
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
const taskHandlers: Map<string, (opts: { payload: any }) => Promise<any>> = new Map();
|
|
21
|
+
let pendingTasks: Promise<any>[] = [];
|
|
22
|
+
let currentTrx: Transaction<DB> | null = null;
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Global Stubs
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
export async function setupHandlerMocks() {
|
|
29
|
+
vi.stubGlobal("defineEventHandler", (handler: Function) => handler);
|
|
30
|
+
|
|
31
|
+
vi.stubGlobal("getUserSession", async () => ({
|
|
32
|
+
user: { id: 1, firstName: "Test", lastName: "User", role: "admin" },
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.stubGlobal("setUserSession", async () => {});
|
|
36
|
+
|
|
37
|
+
// Default: no-op for automations
|
|
38
|
+
vi.stubGlobal("triggerAutomation", () => {});
|
|
39
|
+
|
|
40
|
+
// Task registration and execution
|
|
41
|
+
vi.stubGlobal("defineTask", (config: { meta: { name: string }; run: Function }) => {
|
|
42
|
+
taskHandlers.set(config.meta.name, config.run as any);
|
|
43
|
+
return config;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
vi.stubGlobal("runTask", (taskName: string, options: { payload: any }) => {
|
|
47
|
+
const handler = taskHandlers.get(taskName);
|
|
48
|
+
if (!handler) {
|
|
49
|
+
return Promise.reject(new Error(`Task handler not found: ${taskName}`));
|
|
50
|
+
}
|
|
51
|
+
const promise = handler(options);
|
|
52
|
+
pendingTasks.push(promise);
|
|
53
|
+
return promise;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
vi.stubGlobal("useDatabase", () => {
|
|
57
|
+
if (!currentTrx) {
|
|
58
|
+
throw new Error("useDatabase called outside of test transaction");
|
|
59
|
+
}
|
|
60
|
+
return currentTrx;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Handle nested transactions
|
|
64
|
+
const KyselyModule = await import("kysely");
|
|
65
|
+
const TransactionClass = (KyselyModule as any).Transaction;
|
|
66
|
+
if (TransactionClass?.prototype) {
|
|
67
|
+
TransactionClass.prototype.transaction = function () {
|
|
68
|
+
const self = this;
|
|
69
|
+
return {
|
|
70
|
+
execute: async <T>(callback: (trx: any) => Promise<T>): Promise<T> => {
|
|
71
|
+
return callback(self);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
vi.stubGlobal("createError", (opts: {
|
|
78
|
+
statusCode: number;
|
|
79
|
+
message?: string;
|
|
80
|
+
statusMessage?: string;
|
|
81
|
+
data?: unknown;
|
|
82
|
+
}) => {
|
|
83
|
+
const error = new Error(opts.message || opts.statusMessage || "") as any;
|
|
84
|
+
error.statusCode = opts.statusCode;
|
|
85
|
+
if (opts.data !== undefined) error.data = opts.data;
|
|
86
|
+
return error;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
vi.stubGlobal("getValidatedRouterParams", async (event: any, validate: Function) => {
|
|
90
|
+
return validate(event.context.params ?? {});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
vi.stubGlobal("getRouterParam", (event: any, param: string) => {
|
|
94
|
+
return event.context.params?.[param];
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
vi.stubGlobal("getValidatedQuery", async (event: any, validate: Function) => {
|
|
98
|
+
return validate(event.context._mockQuery ?? {});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
vi.stubGlobal("getQuery", (event: any) => {
|
|
102
|
+
return event.context._mockQuery ?? {};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
vi.stubGlobal("readValidatedBody", async (event: any, validate: Function) => {
|
|
106
|
+
return validate(event.context._mockBody ?? {});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
vi.stubGlobal("readBody", async (event: any) => event.context._mockBody);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Automation Testing
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
export async function enableAutomationTriggers() {
|
|
117
|
+
await import("../tasks/execute-automation");
|
|
118
|
+
const { triggerAutomation: realTrigger } = await import("../utils/automation");
|
|
119
|
+
|
|
120
|
+
vi.stubGlobal("triggerAutomation", (...args: Parameters<typeof realTrigger>) => {
|
|
121
|
+
const promise = realTrigger(...args);
|
|
122
|
+
pendingTasks.push(promise);
|
|
123
|
+
return promise;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function waitForAutomations(): Promise<void> {
|
|
128
|
+
while (pendingTasks.length > 0) {
|
|
129
|
+
const tasksToWait = [...pendingTasks];
|
|
130
|
+
pendingTasks = [];
|
|
131
|
+
await Promise.allSettled(tasksToWait);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// Mock Event Helpers
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
type Params = Record<string, string | number>;
|
|
140
|
+
|
|
141
|
+
export function createMockEvent(options: {
|
|
142
|
+
method?: string;
|
|
143
|
+
params?: Params;
|
|
144
|
+
body?: unknown;
|
|
145
|
+
query?: Record<string, string>;
|
|
146
|
+
}) {
|
|
147
|
+
const { method = "GET", params = {}, body, query = {} } = options;
|
|
148
|
+
|
|
149
|
+
const socket = new Socket();
|
|
150
|
+
const req = new IncomingMessage(socket);
|
|
151
|
+
req.method = method;
|
|
152
|
+
req.url = "/" + (Object.keys(query).length
|
|
153
|
+
? "?" + new URLSearchParams(query).toString()
|
|
154
|
+
: "");
|
|
155
|
+
|
|
156
|
+
const res = new ServerResponse(req);
|
|
157
|
+
const event = createEvent(req, res);
|
|
158
|
+
|
|
159
|
+
event.context.params = Object.fromEntries(
|
|
160
|
+
Object.entries(params).map(([k, v]) => [k, String(v)])
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (body !== undefined) event.context._mockBody = body;
|
|
164
|
+
if (Object.keys(query).length > 0) event.context._mockQuery = query;
|
|
165
|
+
|
|
166
|
+
return event;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function mockGet(params: Params, query?: Record<string, string>) {
|
|
170
|
+
return createMockEvent({ method: "GET", params, query });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function mockPost(params: Params, body: unknown) {
|
|
174
|
+
return createMockEvent({ method: "POST", params, body });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function mockPatch(params: Params, body: unknown) {
|
|
178
|
+
return createMockEvent({ method: "PATCH", params, body });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function mockDelete(params: Params) {
|
|
182
|
+
return createMockEvent({ method: "DELETE", params });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Assertion Helpers
|
|
187
|
+
// ============================================================================
|
|
188
|
+
|
|
189
|
+
export async function expectHttpError(
|
|
190
|
+
promise: Promise<unknown>,
|
|
191
|
+
expected: { statusCode: number; message?: string }
|
|
192
|
+
) {
|
|
193
|
+
await expect(promise).rejects.toMatchObject(expected);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// Factories
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
function createFactories(trx: Transaction<DB>) {
|
|
201
|
+
return {
|
|
202
|
+
async user(data: Partial<{
|
|
203
|
+
email: string;
|
|
204
|
+
name: string;
|
|
205
|
+
role: string;
|
|
206
|
+
}> = {}) {
|
|
207
|
+
const num = Math.floor(Math.random() * 10000);
|
|
208
|
+
return trx
|
|
209
|
+
.insertInto("user")
|
|
210
|
+
.values({
|
|
211
|
+
email: data.email ?? `test${num}@example.com`,
|
|
212
|
+
name: data.name ?? "Test User",
|
|
213
|
+
role: data.role ?? "user",
|
|
214
|
+
})
|
|
215
|
+
.returningAll()
|
|
216
|
+
.executeTakeFirstOrThrow();
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
async project(data: Partial<{
|
|
220
|
+
name: string;
|
|
221
|
+
ownerId: number;
|
|
222
|
+
status: string;
|
|
223
|
+
}> = {}) {
|
|
224
|
+
let ownerId = data.ownerId;
|
|
225
|
+
if (!ownerId) {
|
|
226
|
+
const owner = await this.user();
|
|
227
|
+
ownerId = owner.id;
|
|
228
|
+
}
|
|
229
|
+
return trx
|
|
230
|
+
.insertInto("project")
|
|
231
|
+
.values({
|
|
232
|
+
name: data.name ?? `Project ${Date.now()}`,
|
|
233
|
+
owner_id: ownerId,
|
|
234
|
+
status: data.status ?? "active",
|
|
235
|
+
})
|
|
236
|
+
.returningAll()
|
|
237
|
+
.executeTakeFirstOrThrow();
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async task(data: {
|
|
241
|
+
projectId: number;
|
|
242
|
+
title?: string;
|
|
243
|
+
status?: string;
|
|
244
|
+
assigneeId?: number | null;
|
|
245
|
+
}) {
|
|
246
|
+
return trx
|
|
247
|
+
.insertInto("task")
|
|
248
|
+
.values({
|
|
249
|
+
project_id: data.projectId,
|
|
250
|
+
title: data.title ?? "Test Task",
|
|
251
|
+
status: data.status ?? "pending",
|
|
252
|
+
assignee_id: data.assigneeId ?? null,
|
|
253
|
+
})
|
|
254
|
+
.returningAll()
|
|
255
|
+
.executeTakeFirstOrThrow();
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
// Add more factories as needed...
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export type Factories = ReturnType<typeof createFactories>;
|
|
263
|
+
|
|
264
|
+
// ============================================================================
|
|
265
|
+
// Test Fixture
|
|
266
|
+
// ============================================================================
|
|
267
|
+
|
|
268
|
+
interface TestFixtures {
|
|
269
|
+
factories: Factories;
|
|
270
|
+
db: Transaction<DB>;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export const test = base.extend<TestFixtures>({
|
|
274
|
+
factories: async ({}, use) => {
|
|
275
|
+
await db.transaction().execute(async (trx) => {
|
|
276
|
+
currentTrx = trx;
|
|
277
|
+
try {
|
|
278
|
+
await use(createFactories(trx));
|
|
279
|
+
throw { __rollback: true };
|
|
280
|
+
} finally {
|
|
281
|
+
currentTrx = null;
|
|
282
|
+
}
|
|
283
|
+
}).catch((e) => {
|
|
284
|
+
if (e && typeof e === "object" && "__rollback" in e) return;
|
|
285
|
+
throw e;
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
db: async ({ factories: _ }, use) => {
|
|
290
|
+
if (!currentTrx) {
|
|
291
|
+
throw new Error("db fixture used outside transaction context");
|
|
292
|
+
}
|
|
293
|
+
await use(currentTrx);
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
export { describe, expect, beforeAll } from "vitest";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
// Enable globals (describe, test, expect without imports)
|
|
7
|
+
globals: true,
|
|
8
|
+
|
|
9
|
+
// Node environment for API testing
|
|
10
|
+
environment: "node",
|
|
11
|
+
|
|
12
|
+
// Global setup: runs once before all tests
|
|
13
|
+
// - Drops all tables
|
|
14
|
+
// - Runs migrations
|
|
15
|
+
globalSetup: ["./server/test-utils/global-setup.ts"],
|
|
16
|
+
|
|
17
|
+
// Setup files: runs before each test file
|
|
18
|
+
// - Stubs Nuxt auto-imports
|
|
19
|
+
setupFiles: ["./server/test-utils/setup.ts"],
|
|
20
|
+
|
|
21
|
+
// Coverage configuration
|
|
22
|
+
coverage: {
|
|
23
|
+
provider: "v8",
|
|
24
|
+
reporter: ["text", "html", "lcov", "json", "json-summary"],
|
|
25
|
+
include: ["server/**/*.ts"],
|
|
26
|
+
exclude: [
|
|
27
|
+
"server/**/*.test.ts",
|
|
28
|
+
"server/test-utils/**",
|
|
29
|
+
"server/db/migrations/**",
|
|
30
|
+
"server/db/db.d.ts",
|
|
31
|
+
],
|
|
32
|
+
reportsDirectory: "./coverage",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
resolve: {
|
|
37
|
+
alias: {
|
|
38
|
+
"~": path.resolve(__dirname),
|
|
39
|
+
"@": path.resolve(__dirname),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|