@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.
@@ -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 {};