@techstream/quark-core 1.1.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/.turbo/turbo-lint.log +7 -0
- package/.turbo/turbo-test.log +1376 -0
- package/README.md +419 -0
- package/package.json +29 -0
- package/src/auth/index.js +127 -0
- package/src/auth/password.js +9 -0
- package/src/auth.test.js +90 -0
- package/src/authorization.js +235 -0
- package/src/authorization.test.js +314 -0
- package/src/cache.js +137 -0
- package/src/cache.test.js +217 -0
- package/src/csrf.js +118 -0
- package/src/csrf.test.js +157 -0
- package/src/email.js +140 -0
- package/src/email.test.js +259 -0
- package/src/error-reporter.js +266 -0
- package/src/error-reporter.test.js +236 -0
- package/src/errors.js +192 -0
- package/src/errors.test.js +128 -0
- package/src/index.js +32 -0
- package/src/logger.js +182 -0
- package/src/logger.test.js +287 -0
- package/src/mailhog.js +43 -0
- package/src/queue/index.js +214 -0
- package/src/rate-limiter.js +253 -0
- package/src/rate-limiter.test.js +130 -0
- package/src/redis.js +96 -0
- package/src/testing/factories.js +93 -0
- package/src/testing/helpers.js +266 -0
- package/src/testing/index.js +46 -0
- package/src/testing/mocks.js +480 -0
- package/src/testing/testing.test.js +543 -0
- package/src/types.js +74 -0
- package/src/utils.js +219 -0
- package/src/utils.test.js +193 -0
- package/src/validation.js +26 -0
- package/test-imports.mjs +21 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
assertThrows,
|
|
5
|
+
captureConsole,
|
|
6
|
+
createMockPrisma,
|
|
7
|
+
createMockRedis,
|
|
8
|
+
createMockRequest,
|
|
9
|
+
createMockResponse,
|
|
10
|
+
createTestContext,
|
|
11
|
+
createTestPost,
|
|
12
|
+
createTestSession,
|
|
13
|
+
createTestUser,
|
|
14
|
+
waitFor,
|
|
15
|
+
} from "./index.js";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Factories
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
test("Factories", async (t) => {
|
|
22
|
+
await t.test("createTestUser returns correct shape with defaults", () => {
|
|
23
|
+
const user = createTestUser();
|
|
24
|
+
assert.ok(user.id.startsWith("user_"));
|
|
25
|
+
assert.ok(user.email.includes("@example.com"));
|
|
26
|
+
assert.strictEqual(user.name, "Test User");
|
|
27
|
+
assert.strictEqual(user.role, "viewer");
|
|
28
|
+
assert.strictEqual(user.password, null);
|
|
29
|
+
assert.strictEqual(user.image, null);
|
|
30
|
+
assert.strictEqual(user.emailVerified, null);
|
|
31
|
+
assert.ok(user.createdAt instanceof Date);
|
|
32
|
+
assert.ok(user.updatedAt instanceof Date);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await t.test("createTestUser applies overrides", () => {
|
|
36
|
+
const user = createTestUser({
|
|
37
|
+
id: "custom_id",
|
|
38
|
+
name: "Alice",
|
|
39
|
+
role: "admin",
|
|
40
|
+
email: "alice@test.com",
|
|
41
|
+
});
|
|
42
|
+
assert.strictEqual(user.id, "custom_id");
|
|
43
|
+
assert.strictEqual(user.name, "Alice");
|
|
44
|
+
assert.strictEqual(user.role, "admin");
|
|
45
|
+
assert.strictEqual(user.email, "alice@test.com");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await t.test("createTestUser generates unique IDs", () => {
|
|
49
|
+
const a = createTestUser();
|
|
50
|
+
const b = createTestUser();
|
|
51
|
+
assert.notStrictEqual(a.id, b.id);
|
|
52
|
+
assert.notStrictEqual(a.email, b.email);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await t.test("createTestPost returns correct shape with defaults", () => {
|
|
56
|
+
const post = createTestPost();
|
|
57
|
+
assert.ok(post.id.startsWith("post_"));
|
|
58
|
+
assert.strictEqual(post.title, "Test Post");
|
|
59
|
+
assert.strictEqual(post.content, "Test content");
|
|
60
|
+
assert.strictEqual(post.published, false);
|
|
61
|
+
assert.ok(post.authorId.startsWith("user_"));
|
|
62
|
+
assert.ok(post.createdAt instanceof Date);
|
|
63
|
+
assert.ok(post.updatedAt instanceof Date);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await t.test(
|
|
67
|
+
"createTestPost applies overrides including boolean false",
|
|
68
|
+
() => {
|
|
69
|
+
const post = createTestPost({ published: true, title: "Custom" });
|
|
70
|
+
assert.strictEqual(post.published, true);
|
|
71
|
+
assert.strictEqual(post.title, "Custom");
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
await t.test(
|
|
76
|
+
"createTestPost keeps published false when explicitly set",
|
|
77
|
+
() => {
|
|
78
|
+
const post = createTestPost({ published: false });
|
|
79
|
+
assert.strictEqual(post.published, false);
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
await t.test("createTestSession returns session with embedded user", () => {
|
|
84
|
+
const session = createTestSession();
|
|
85
|
+
assert.ok(session.user);
|
|
86
|
+
assert.ok(session.user.id.startsWith("user_"));
|
|
87
|
+
assert.ok(session.user.email.includes("@example.com"));
|
|
88
|
+
assert.strictEqual(session.user.name, "Test User");
|
|
89
|
+
assert.strictEqual(session.user.role, "viewer");
|
|
90
|
+
assert.ok(session.expires);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await t.test("createTestSession applies user overrides", () => {
|
|
94
|
+
const session = createTestSession({
|
|
95
|
+
user: { role: "admin", name: "Admin" },
|
|
96
|
+
});
|
|
97
|
+
assert.strictEqual(session.user.role, "admin");
|
|
98
|
+
assert.strictEqual(session.user.name, "Admin");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await t.test("createTestSession applies top-level overrides", () => {
|
|
102
|
+
const expires = "2099-01-01T00:00:00.000Z";
|
|
103
|
+
const session = createTestSession({ expires });
|
|
104
|
+
assert.strictEqual(session.expires, expires);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Mock Prisma
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
test("Mock Prisma", async (t) => {
|
|
113
|
+
await t.test("records method calls with args", async () => {
|
|
114
|
+
const prisma = createMockPrisma();
|
|
115
|
+
await prisma.user.findUnique({ where: { id: "1" } });
|
|
116
|
+
assert.strictEqual(prisma.calls.length, 1);
|
|
117
|
+
assert.strictEqual(prisma.calls[0].model, "user");
|
|
118
|
+
assert.strictEqual(prisma.calls[0].method, "findUnique");
|
|
119
|
+
assert.deepStrictEqual(prisma.calls[0].args, [{ where: { id: "1" } }]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await t.test("returns null by default for unconfigured methods", async () => {
|
|
123
|
+
const prisma = createMockPrisma();
|
|
124
|
+
const result = await prisma.user.findUnique({ where: { id: "1" } });
|
|
125
|
+
assert.strictEqual(result, null);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await t.test("returns configured values via mockReturn", async () => {
|
|
129
|
+
const prisma = createMockPrisma();
|
|
130
|
+
const fakeUser = { id: "1", name: "Test" };
|
|
131
|
+
prisma.mockReturn("user", "findUnique", fakeUser);
|
|
132
|
+
|
|
133
|
+
const result = await prisma.user.findUnique({ where: { id: "1" } });
|
|
134
|
+
assert.deepStrictEqual(result, fakeUser);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await t.test("supports function return values", async () => {
|
|
138
|
+
const prisma = createMockPrisma();
|
|
139
|
+
prisma.mockReturn("user", "create", (args) => ({
|
|
140
|
+
id: "new_1",
|
|
141
|
+
...args.data,
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
const result = await prisma.user.create({ data: { name: "Alice" } });
|
|
145
|
+
assert.strictEqual(result.id, "new_1");
|
|
146
|
+
assert.strictEqual(result.name, "Alice");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await t.test("supports overrides in constructor", async () => {
|
|
150
|
+
const prisma = createMockPrisma({
|
|
151
|
+
user: { findMany: [{ id: "1" }, { id: "2" }] },
|
|
152
|
+
});
|
|
153
|
+
const result = await prisma.user.findMany();
|
|
154
|
+
assert.deepStrictEqual(result, [{ id: "1" }, { id: "2" }]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await t.test("reset clears calls and return values", async () => {
|
|
158
|
+
const prisma = createMockPrisma();
|
|
159
|
+
prisma.mockReturn("user", "findUnique", { id: "1" });
|
|
160
|
+
await prisma.user.findUnique({ where: { id: "1" } });
|
|
161
|
+
|
|
162
|
+
prisma.reset();
|
|
163
|
+
assert.strictEqual(prisma.calls.length, 0);
|
|
164
|
+
|
|
165
|
+
const result = await prisma.user.findUnique({ where: { id: "1" } });
|
|
166
|
+
assert.strictEqual(result, null);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await t.test("handles multiple models independently", async () => {
|
|
170
|
+
const prisma = createMockPrisma();
|
|
171
|
+
prisma.mockReturn("user", "findMany", []);
|
|
172
|
+
prisma.mockReturn("post", "findMany", [{ id: "p1" }]);
|
|
173
|
+
|
|
174
|
+
const users = await prisma.user.findMany();
|
|
175
|
+
const posts = await prisma.post.findMany();
|
|
176
|
+
|
|
177
|
+
assert.deepStrictEqual(users, []);
|
|
178
|
+
assert.deepStrictEqual(posts, [{ id: "p1" }]);
|
|
179
|
+
assert.strictEqual(prisma.calls.length, 2);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Mock Request
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
test("Mock Request", async (t) => {
|
|
188
|
+
await t.test("has correct defaults", () => {
|
|
189
|
+
const req = createMockRequest();
|
|
190
|
+
assert.strictEqual(req.method, "GET");
|
|
191
|
+
assert.strictEqual(req.url, "http://localhost/api/test");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await t.test("headers.get is case-insensitive", () => {
|
|
195
|
+
const req = createMockRequest({
|
|
196
|
+
headers: { "Content-Type": "application/json" },
|
|
197
|
+
});
|
|
198
|
+
assert.strictEqual(req.headers.get("content-type"), "application/json");
|
|
199
|
+
assert.strictEqual(req.headers.get("Content-Type"), "application/json");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await t.test("returns null for missing headers", () => {
|
|
203
|
+
const req = createMockRequest();
|
|
204
|
+
assert.strictEqual(req.headers.get("x-missing"), null);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await t.test("json() returns body", async () => {
|
|
208
|
+
const body = { name: "Test" };
|
|
209
|
+
const req = createMockRequest({ method: "POST", body });
|
|
210
|
+
const result = await req.json();
|
|
211
|
+
assert.deepStrictEqual(result, body);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await t.test("cookies.get returns cookie object", () => {
|
|
215
|
+
const req = createMockRequest({ cookies: { session: "abc123" } });
|
|
216
|
+
const cookie = req.cookies.get("session");
|
|
217
|
+
assert.deepStrictEqual(cookie, { name: "session", value: "abc123" });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await t.test("cookies.get returns undefined for missing cookie", () => {
|
|
221
|
+
const req = createMockRequest();
|
|
222
|
+
assert.strictEqual(req.cookies.get("missing"), undefined);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await t.test("nextUrl is a URL object", () => {
|
|
226
|
+
const req = createMockRequest({ url: "http://localhost/api/test?q=hello" });
|
|
227
|
+
assert.ok(req.nextUrl instanceof URL);
|
|
228
|
+
assert.strictEqual(req.nextUrl.searchParams.get("q"), "hello");
|
|
229
|
+
assert.strictEqual(req.nextUrl.pathname, "/api/test");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Mock Response
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
test("Mock Response", async (t) => {
|
|
238
|
+
await t.test("has default status 200", () => {
|
|
239
|
+
const res = createMockResponse();
|
|
240
|
+
assert.strictEqual(res.status, 200);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await t.test("json() sets body and content-type", () => {
|
|
244
|
+
const res = createMockResponse();
|
|
245
|
+
const returned = res.json({ success: true });
|
|
246
|
+
assert.deepStrictEqual(res.body, { success: true });
|
|
247
|
+
assert.strictEqual(res.headers.get("content-type"), "application/json");
|
|
248
|
+
assert.strictEqual(returned, res); // chainable
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await t.test("redirect() sets status and location", () => {
|
|
252
|
+
const res = createMockResponse();
|
|
253
|
+
res.redirect("/login");
|
|
254
|
+
assert.strictEqual(res.status, 302);
|
|
255
|
+
assert.strictEqual(res.headers.get("location"), "/login");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await t.test("cookies can be set and retrieved", () => {
|
|
259
|
+
const res = createMockResponse();
|
|
260
|
+
res.cookies.set("token", "abc");
|
|
261
|
+
assert.strictEqual(res.cookies.get("token"), "abc");
|
|
262
|
+
assert.ok(res.cookies.has("token"));
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// Mock Redis
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
test("Mock Redis", async (t) => {
|
|
271
|
+
await t.test("get/set stores and retrieves values", async () => {
|
|
272
|
+
const redis = createMockRedis();
|
|
273
|
+
await redis.set("key", "value");
|
|
274
|
+
const result = await redis.get("key");
|
|
275
|
+
assert.strictEqual(result, "value");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await t.test("get returns null for missing keys", async () => {
|
|
279
|
+
const redis = createMockRedis();
|
|
280
|
+
const result = await redis.get("missing");
|
|
281
|
+
assert.strictEqual(result, null);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await t.test("initialData pre-populates the store", async () => {
|
|
285
|
+
const redis = createMockRedis({ greeting: "hello" });
|
|
286
|
+
const result = await redis.get("greeting");
|
|
287
|
+
assert.strictEqual(result, "hello");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
await t.test("del removes keys and returns count", async () => {
|
|
291
|
+
const redis = createMockRedis({ a: "1", b: "2", c: "3" });
|
|
292
|
+
const count = await redis.del("a", "c");
|
|
293
|
+
assert.strictEqual(count, 2);
|
|
294
|
+
assert.strictEqual(await redis.get("a"), null);
|
|
295
|
+
assert.strictEqual(await redis.get("b"), "2");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await t.test("keys filters by prefix pattern", async () => {
|
|
299
|
+
const redis = createMockRedis({
|
|
300
|
+
"sess:1": "a",
|
|
301
|
+
"sess:2": "b",
|
|
302
|
+
"cache:1": "c",
|
|
303
|
+
});
|
|
304
|
+
const keys = await redis.keys("sess:*");
|
|
305
|
+
assert.deepStrictEqual(keys.sort(), ["sess:1", "sess:2"]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await t.test("incr increments numeric values", async () => {
|
|
309
|
+
const redis = createMockRedis();
|
|
310
|
+
const v1 = await redis.incr("counter");
|
|
311
|
+
const v2 = await redis.incr("counter");
|
|
312
|
+
assert.strictEqual(v1, 1);
|
|
313
|
+
assert.strictEqual(v2, 2);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
await t.test("exists returns count of existing keys", async () => {
|
|
317
|
+
const redis = createMockRedis({ a: "1" });
|
|
318
|
+
assert.strictEqual(await redis.exists("a"), 1);
|
|
319
|
+
assert.strictEqual(await redis.exists("b"), 0);
|
|
320
|
+
assert.strictEqual(await redis.exists("a", "b"), 1);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await t.test("pipeline batches commands", async () => {
|
|
324
|
+
const redis = createMockRedis();
|
|
325
|
+
await redis.set("x", "1");
|
|
326
|
+
const results = await redis.pipeline().get("x").set("y", "2").exec();
|
|
327
|
+
assert.strictEqual(results.length, 2);
|
|
328
|
+
assert.deepStrictEqual(results[0], [null, "1"]);
|
|
329
|
+
assert.deepStrictEqual(results[1], [null, "OK"]);
|
|
330
|
+
assert.strictEqual(await redis.get("y"), "2");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
await t.test("records all method calls", async () => {
|
|
334
|
+
const redis = createMockRedis();
|
|
335
|
+
await redis.set("k", "v");
|
|
336
|
+
await redis.get("k");
|
|
337
|
+
assert.strictEqual(redis.calls.length, 2);
|
|
338
|
+
assert.strictEqual(redis.calls[0].method, "set");
|
|
339
|
+
assert.strictEqual(redis.calls[1].method, "get");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await t.test("reset clears store and calls", async () => {
|
|
343
|
+
const redis = createMockRedis({ a: "1" });
|
|
344
|
+
await redis.get("a");
|
|
345
|
+
redis.reset();
|
|
346
|
+
assert.strictEqual(redis.calls.length, 0);
|
|
347
|
+
assert.strictEqual(await redis.get("a"), null);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await t.test("ping returns PONG", async () => {
|
|
351
|
+
const redis = createMockRedis();
|
|
352
|
+
assert.strictEqual(await redis.ping(), "PONG");
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// captureConsole
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
test("captureConsole", async (t) => {
|
|
361
|
+
await t.test("captures console output and restores", () => {
|
|
362
|
+
const originalLog = console.log;
|
|
363
|
+
const { output, restore } = captureConsole();
|
|
364
|
+
|
|
365
|
+
console.log("hello", "world");
|
|
366
|
+
console.warn("warning");
|
|
367
|
+
console.error("error");
|
|
368
|
+
|
|
369
|
+
assert.strictEqual(output.length, 3);
|
|
370
|
+
assert.strictEqual(output[0].method, "log");
|
|
371
|
+
assert.deepStrictEqual(output[0].args, ["hello", "world"]);
|
|
372
|
+
assert.strictEqual(output[1].method, "warn");
|
|
373
|
+
assert.strictEqual(output[2].method, "error");
|
|
374
|
+
|
|
375
|
+
restore();
|
|
376
|
+
assert.strictEqual(console.log, originalLog);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
await t.test("captures info and debug", () => {
|
|
380
|
+
const { output, restore } = captureConsole();
|
|
381
|
+
console.info("info msg");
|
|
382
|
+
console.debug("debug msg");
|
|
383
|
+
restore();
|
|
384
|
+
|
|
385
|
+
assert.strictEqual(output.length, 2);
|
|
386
|
+
assert.strictEqual(output[0].method, "info");
|
|
387
|
+
assert.strictEqual(output[1].method, "debug");
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// createTestContext
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
test("createTestContext", async (t) => {
|
|
396
|
+
await t.test("saves and restores env variables", async () => {
|
|
397
|
+
const original = process.env.TEST_CTX_VAR;
|
|
398
|
+
const ctx = createTestContext();
|
|
399
|
+
|
|
400
|
+
ctx.setEnv("TEST_CTX_VAR", "modified");
|
|
401
|
+
assert.strictEqual(process.env.TEST_CTX_VAR, "modified");
|
|
402
|
+
|
|
403
|
+
await ctx.cleanup();
|
|
404
|
+
assert.strictEqual(process.env.TEST_CTX_VAR, original);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
await t.test("deletes env var when set to undefined", () => {
|
|
408
|
+
const ctx = createTestContext();
|
|
409
|
+
process.env.TEST_CTX_DEL = "exists";
|
|
410
|
+
ctx.setEnv("TEST_CTX_DEL", undefined);
|
|
411
|
+
assert.strictEqual(process.env.TEST_CTX_DEL, undefined);
|
|
412
|
+
ctx.restoreEnv();
|
|
413
|
+
assert.strictEqual(process.env.TEST_CTX_DEL, "exists");
|
|
414
|
+
delete process.env.TEST_CTX_DEL;
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
await t.test("runs cleanup functions in LIFO order", async () => {
|
|
418
|
+
const ctx = createTestContext();
|
|
419
|
+
const order = [];
|
|
420
|
+
ctx.onCleanup(() => order.push("first"));
|
|
421
|
+
ctx.onCleanup(() => order.push("second"));
|
|
422
|
+
ctx.onCleanup(() => order.push("third"));
|
|
423
|
+
|
|
424
|
+
await ctx.cleanup();
|
|
425
|
+
assert.deepStrictEqual(order, ["third", "second", "first"]);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
await t.test("cleanup runs async cleanup functions", async () => {
|
|
429
|
+
const ctx = createTestContext();
|
|
430
|
+
let cleaned = false;
|
|
431
|
+
ctx.onCleanup(async () => {
|
|
432
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
433
|
+
cleaned = true;
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
await ctx.cleanup();
|
|
437
|
+
assert.ok(cleaned);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// waitFor
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
test("waitFor", async (t) => {
|
|
446
|
+
await t.test("resolves when assertion passes immediately", async () => {
|
|
447
|
+
await waitFor(() => {
|
|
448
|
+
assert.ok(true);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await t.test("resolves when assertion eventually passes", async () => {
|
|
453
|
+
let count = 0;
|
|
454
|
+
await waitFor(
|
|
455
|
+
() => {
|
|
456
|
+
count++;
|
|
457
|
+
if (count < 3) throw new Error("not yet");
|
|
458
|
+
},
|
|
459
|
+
{ timeout: 1000, interval: 10 },
|
|
460
|
+
);
|
|
461
|
+
assert.ok(count >= 3);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
await t.test("throws last error on timeout", async () => {
|
|
465
|
+
await assert.rejects(
|
|
466
|
+
() =>
|
|
467
|
+
waitFor(
|
|
468
|
+
() => {
|
|
469
|
+
throw new Error("always fails");
|
|
470
|
+
},
|
|
471
|
+
{ timeout: 100, interval: 10 },
|
|
472
|
+
),
|
|
473
|
+
{ message: "always fails" },
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
// assertThrows
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
|
|
482
|
+
test("assertThrows", async (t) => {
|
|
483
|
+
await t.test("passes when async function throws expected error", async () => {
|
|
484
|
+
const err = await assertThrows(
|
|
485
|
+
async () => {
|
|
486
|
+
throw new TypeError("bad input");
|
|
487
|
+
},
|
|
488
|
+
TypeError,
|
|
489
|
+
/bad input/,
|
|
490
|
+
);
|
|
491
|
+
assert.ok(err instanceof TypeError);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
await t.test("fails when function does not throw", async () => {
|
|
495
|
+
await assert.rejects(
|
|
496
|
+
() => assertThrows(async () => "ok", Error),
|
|
497
|
+
/Expected function to throw/,
|
|
498
|
+
);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
await t.test("fails when error class does not match", async () => {
|
|
502
|
+
await assert.rejects(
|
|
503
|
+
() =>
|
|
504
|
+
assertThrows(async () => {
|
|
505
|
+
throw new RangeError("oops");
|
|
506
|
+
}, TypeError),
|
|
507
|
+
/Expected error to be instance of TypeError/,
|
|
508
|
+
);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
await t.test("fails when message pattern does not match", async () => {
|
|
512
|
+
await assert.rejects(() =>
|
|
513
|
+
assertThrows(
|
|
514
|
+
async () => {
|
|
515
|
+
throw new Error("actual message");
|
|
516
|
+
},
|
|
517
|
+
Error,
|
|
518
|
+
/completely different/,
|
|
519
|
+
),
|
|
520
|
+
);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
await t.test("works with string message pattern", async () => {
|
|
524
|
+
const err = await assertThrows(
|
|
525
|
+
async () => {
|
|
526
|
+
throw new Error("validation failed: invalid email");
|
|
527
|
+
},
|
|
528
|
+
Error,
|
|
529
|
+
"invalid email",
|
|
530
|
+
);
|
|
531
|
+
assert.ok(err.message.includes("invalid email"));
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
await t.test("works without ErrorClass (just message)", async () => {
|
|
535
|
+
await assertThrows(
|
|
536
|
+
async () => {
|
|
537
|
+
throw new Error("something broke");
|
|
538
|
+
},
|
|
539
|
+
undefined,
|
|
540
|
+
/something broke/,
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
});
|
package/src/types.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @techstream/quark-core - Type Definitions
|
|
3
|
+
* TypeScript types and JSDoc type definitions for better IDE support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} Session
|
|
8
|
+
* @property {Object} user
|
|
9
|
+
* @property {string} user.id - User ID
|
|
10
|
+
* @property {string} user.email - User email
|
|
11
|
+
* @property {string} [user.name] - User name
|
|
12
|
+
* @property {string} [user.image] - User image URL
|
|
13
|
+
* @property {number} expires - Session expiration timestamp
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} AuthConfig
|
|
18
|
+
* @property {string} [secret] - NextAuth secret
|
|
19
|
+
* @property {Array} providers - NextAuth providers
|
|
20
|
+
* @property {Object} session - Session configuration
|
|
21
|
+
* @property {Object} callbacks - NextAuth callbacks
|
|
22
|
+
* @property {string} session.strategy - Session strategy (jwt or database)
|
|
23
|
+
* @property {number} session.maxAge - Max session age in seconds
|
|
24
|
+
* @property {number} session.updateAge - Session update age in seconds
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} QueueConfig
|
|
29
|
+
* @property {Object} redis - Redis connection config
|
|
30
|
+
* @property {string} redis.host - Redis host
|
|
31
|
+
* @property {number} redis.port - Redis port
|
|
32
|
+
* @property {number} redis.db - Redis database number
|
|
33
|
+
* @property {Object} defaultJobOptions - Default options for all jobs
|
|
34
|
+
* @property {number} defaultJobOptions.attempts - Max retry attempts
|
|
35
|
+
* @property {Object} defaultJobOptions.backoff - Backoff strategy
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {Object} JobData
|
|
40
|
+
* @property {string} id - Unique job identifier
|
|
41
|
+
* @property {string} queueName - Name of the queue
|
|
42
|
+
* @property {Object} data - Job payload data
|
|
43
|
+
* @property {string} [state] - Job state (waiting, active, completed, failed)
|
|
44
|
+
* @property {number} [progress] - Job progress percentage (0-100)
|
|
45
|
+
* @property {number} [attempts] - Number of attempts made
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {Object} AppErrorJSON
|
|
50
|
+
* @property {string} name - Error type name
|
|
51
|
+
* @property {string} message - Error message
|
|
52
|
+
* @property {string} code - Machine-readable error code
|
|
53
|
+
* @property {number} statusCode - HTTP status code
|
|
54
|
+
* @property {string} timestamp - ISO timestamp when error occurred
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {Object} DbClientOptions
|
|
59
|
+
* @property {string} [datasourceUrl] - Prisma datasource URL
|
|
60
|
+
* @property {boolean} [errorFormat] - Error format for Prisma
|
|
61
|
+
* @property {Object} [middleware] - Prisma middleware functions
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @typedef {Object} HealthCheckResult
|
|
66
|
+
* @property {boolean} healthy - Overall system health
|
|
67
|
+
* @property {Object} checks - Individual component health checks
|
|
68
|
+
* @property {boolean} checks.database - Database connectivity
|
|
69
|
+
* @property {boolean} checks.redis - Redis connectivity
|
|
70
|
+
* @property {string} timestamp - Health check timestamp
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
// Export as module exports for JavaScript usage
|
|
74
|
+
export {};
|