@cortexmemory/cli 0.27.1 → 0.27.3

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.
Files changed (46) hide show
  1. package/dist/commands/convex.js +1 -1
  2. package/dist/commands/convex.js.map +1 -1
  3. package/dist/commands/deploy.d.ts +1 -1
  4. package/dist/commands/deploy.d.ts.map +1 -1
  5. package/dist/commands/deploy.js +771 -144
  6. package/dist/commands/deploy.js.map +1 -1
  7. package/dist/commands/dev.d.ts.map +1 -1
  8. package/dist/commands/dev.js +89 -26
  9. package/dist/commands/dev.js.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/utils/app-template-sync.d.ts +95 -0
  12. package/dist/utils/app-template-sync.d.ts.map +1 -0
  13. package/dist/utils/app-template-sync.js +425 -0
  14. package/dist/utils/app-template-sync.js.map +1 -0
  15. package/dist/utils/deployment-selector.d.ts +21 -0
  16. package/dist/utils/deployment-selector.d.ts.map +1 -1
  17. package/dist/utils/deployment-selector.js +32 -0
  18. package/dist/utils/deployment-selector.js.map +1 -1
  19. package/dist/utils/init/graph-setup.d.ts.map +1 -1
  20. package/dist/utils/init/graph-setup.js +13 -2
  21. package/dist/utils/init/graph-setup.js.map +1 -1
  22. package/package.json +1 -1
  23. package/templates/vercel-ai-quickstart/app/api/auth/check/route.ts +30 -0
  24. package/templates/vercel-ai-quickstart/app/api/auth/login/route.ts +83 -0
  25. package/templates/vercel-ai-quickstart/app/api/auth/register/route.ts +94 -0
  26. package/templates/vercel-ai-quickstart/app/api/auth/setup/route.ts +59 -0
  27. package/templates/vercel-ai-quickstart/app/api/chat/route.ts +83 -2
  28. package/templates/vercel-ai-quickstart/app/api/conversations/route.ts +179 -0
  29. package/templates/vercel-ai-quickstart/app/globals.css +161 -0
  30. package/templates/vercel-ai-quickstart/app/page.tsx +93 -8
  31. package/templates/vercel-ai-quickstart/components/AdminSetup.tsx +139 -0
  32. package/templates/vercel-ai-quickstart/components/AuthProvider.tsx +283 -0
  33. package/templates/vercel-ai-quickstart/components/ChatHistorySidebar.tsx +323 -0
  34. package/templates/vercel-ai-quickstart/components/ChatInterface.tsx +113 -16
  35. package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +202 -0
  36. package/templates/vercel-ai-quickstart/jest.config.js +45 -0
  37. package/templates/vercel-ai-quickstart/lib/cortex.ts +27 -0
  38. package/templates/vercel-ai-quickstart/lib/password.ts +120 -0
  39. package/templates/vercel-ai-quickstart/next.config.js +20 -0
  40. package/templates/vercel-ai-quickstart/package.json +7 -2
  41. package/templates/vercel-ai-quickstart/tests/helpers/mock-cortex.ts +263 -0
  42. package/templates/vercel-ai-quickstart/tests/helpers/setup.ts +48 -0
  43. package/templates/vercel-ai-quickstart/tests/integration/auth.test.ts +455 -0
  44. package/templates/vercel-ai-quickstart/tests/integration/conversations.test.ts +461 -0
  45. package/templates/vercel-ai-quickstart/tests/unit/password.test.ts +228 -0
  46. package/templates/vercel-ai-quickstart/tsconfig.json +1 -1
@@ -0,0 +1,461 @@
1
+ /**
2
+ * Integration Tests: Conversations API Routes
3
+ *
4
+ * Tests the conversations API routes with mocked Cortex SDK.
5
+ */
6
+
7
+ import {
8
+ createMockCortex,
9
+ resetMockStores,
10
+ seedTestData,
11
+ type MockCortex,
12
+ } from "../helpers/mock-cortex";
13
+
14
+ // Mock the Cortex SDK module
15
+ let mockCortex: MockCortex;
16
+
17
+ jest.mock("../../lib/cortex", () => ({
18
+ getCortex: () => mockCortex,
19
+ }));
20
+
21
+ // Import route handlers after mocking
22
+ import {
23
+ GET as conversationsGet,
24
+ POST as conversationsPost,
25
+ DELETE as conversationsDelete,
26
+ } from "../../app/api/conversations/route";
27
+
28
+ /**
29
+ * Helper to create a mock Request object
30
+ */
31
+ function createRequest(
32
+ method: string,
33
+ options: {
34
+ body?: Record<string, unknown>;
35
+ searchParams?: Record<string, string>;
36
+ } = {}
37
+ ): Request {
38
+ const url = new URL("http://localhost:3000/api/conversations");
39
+
40
+ if (options.searchParams) {
41
+ Object.entries(options.searchParams).forEach(([key, value]) => {
42
+ url.searchParams.set(key, value);
43
+ });
44
+ }
45
+
46
+ const init: RequestInit = { method };
47
+ if (options.body) {
48
+ init.body = JSON.stringify(options.body);
49
+ init.headers = { "Content-Type": "application/json" };
50
+ }
51
+
52
+ return new Request(url.toString(), init);
53
+ }
54
+
55
+ /**
56
+ * Helper to parse JSON response
57
+ */
58
+ async function parseResponse(response: Response): Promise<{
59
+ status: number;
60
+ data: Record<string, unknown>;
61
+ }> {
62
+ const data = await response.json();
63
+ return { status: response.status, data };
64
+ }
65
+
66
+ describe("Conversations API Routes", () => {
67
+ beforeEach(() => {
68
+ resetMockStores();
69
+ mockCortex = createMockCortex();
70
+ });
71
+
72
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
73
+ // GET /api/conversations
74
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
75
+
76
+ describe("GET /api/conversations", () => {
77
+ it("should return 400 if userId is missing and no conversationId", async () => {
78
+ const request = createRequest("GET");
79
+ const response = await conversationsGet(request);
80
+ const { status, data } = await parseResponse(response);
81
+
82
+ expect(status).toBe(400);
83
+ expect(data.error).toBe("userId is required");
84
+ });
85
+
86
+ it("should return single conversation with messages when conversationId is provided", async () => {
87
+ // Seed conversation with messages
88
+ seedTestData.conversation("conv-with-messages", {
89
+ userId: "testuser",
90
+ title: "Test Conversation",
91
+ messages: [
92
+ { role: "user", content: "Hello" },
93
+ { role: "assistant", content: "Hi there!" },
94
+ ],
95
+ });
96
+
97
+ const request = createRequest("GET", {
98
+ searchParams: { conversationId: "conv-with-messages" },
99
+ });
100
+ const response = await conversationsGet(request);
101
+ const { status, data } = await parseResponse(response);
102
+
103
+ expect(status).toBe(200);
104
+ expect(data.conversation).toBeDefined();
105
+ expect((data.conversation as { id: string }).id).toBe("conv-with-messages");
106
+ expect((data.conversation as { title: string }).title).toBe("Test Conversation");
107
+ expect(data.messages).toBeDefined();
108
+ expect((data.messages as unknown[]).length).toBe(2);
109
+ });
110
+
111
+ it("should return 404 when conversationId is not found", async () => {
112
+ const request = createRequest("GET", {
113
+ searchParams: { conversationId: "nonexistent-conv" },
114
+ });
115
+ const response = await conversationsGet(request);
116
+ const { status, data } = await parseResponse(response);
117
+
118
+ expect(status).toBe(404);
119
+ expect(data.error).toBe("Conversation not found");
120
+ });
121
+
122
+ it("should return empty array for user with no conversations", async () => {
123
+ const request = createRequest("GET", {
124
+ searchParams: { userId: "newuser" },
125
+ });
126
+ const response = await conversationsGet(request);
127
+ const { status, data } = await parseResponse(response);
128
+
129
+ expect(status).toBe(200);
130
+ expect(data.conversations).toEqual([]);
131
+ });
132
+
133
+ it("should return conversations for user", async () => {
134
+ // Seed conversations
135
+ seedTestData.conversation("conv-1", {
136
+ userId: "testuser",
137
+ title: "First Chat",
138
+ });
139
+ seedTestData.conversation("conv-2", {
140
+ userId: "testuser",
141
+ title: "Second Chat",
142
+ });
143
+
144
+ const request = createRequest("GET", {
145
+ searchParams: { userId: "testuser" },
146
+ });
147
+ const response = await conversationsGet(request);
148
+ const { status, data } = await parseResponse(response);
149
+
150
+ expect(status).toBe(200);
151
+ expect((data.conversations as unknown[]).length).toBe(2);
152
+
153
+ const conversations = data.conversations as {
154
+ id: string;
155
+ title: string;
156
+ }[];
157
+ expect(conversations.map((c) => c.id)).toContain("conv-1");
158
+ expect(conversations.map((c) => c.id)).toContain("conv-2");
159
+ });
160
+
161
+ it("should filter conversations by memorySpaceId", async () => {
162
+ // Seed conversations in different memory spaces
163
+ seedTestData.conversation("conv-1", {
164
+ userId: "testuser",
165
+ memorySpaceId: "space-a",
166
+ title: "Space A Chat",
167
+ });
168
+ seedTestData.conversation("conv-2", {
169
+ userId: "testuser",
170
+ memorySpaceId: "space-b",
171
+ title: "Space B Chat",
172
+ });
173
+
174
+ const request = createRequest("GET", {
175
+ searchParams: { userId: "testuser", memorySpaceId: "space-a" },
176
+ });
177
+ const response = await conversationsGet(request);
178
+ const { status, data } = await parseResponse(response);
179
+
180
+ expect(status).toBe(200);
181
+ expect((data.conversations as unknown[]).length).toBe(1);
182
+ expect((data.conversations as { id: string }[])[0].id).toBe("conv-1");
183
+ });
184
+
185
+ it("should not return conversations from other users", async () => {
186
+ // Seed conversations for different users
187
+ seedTestData.conversation("conv-1", {
188
+ userId: "user-a",
189
+ title: "User A Chat",
190
+ });
191
+ seedTestData.conversation("conv-2", {
192
+ userId: "user-b",
193
+ title: "User B Chat",
194
+ });
195
+
196
+ const request = createRequest("GET", {
197
+ searchParams: { userId: "user-a" },
198
+ });
199
+ const response = await conversationsGet(request);
200
+ const { status, data } = await parseResponse(response);
201
+
202
+ expect(status).toBe(200);
203
+ expect((data.conversations as unknown[]).length).toBe(1);
204
+ expect((data.conversations as { id: string }[])[0].id).toBe("conv-1");
205
+ });
206
+
207
+ it("should include conversation metadata in response", async () => {
208
+ seedTestData.conversation("conv-1", {
209
+ userId: "testuser",
210
+ title: "Test Chat Title",
211
+ });
212
+
213
+ const request = createRequest("GET", {
214
+ searchParams: { userId: "testuser" },
215
+ });
216
+ const response = await conversationsGet(request);
217
+ const { status, data } = await parseResponse(response);
218
+
219
+ expect(status).toBe(200);
220
+ const conversation = (data.conversations as Record<string, unknown>[])[0];
221
+ expect(conversation.id).toBe("conv-1");
222
+ expect(conversation.title).toBe("Test Chat Title");
223
+ expect(conversation.createdAt).toBeDefined();
224
+ expect(conversation.updatedAt).toBeDefined();
225
+ expect(conversation.messageCount).toBeDefined();
226
+ });
227
+
228
+ it("should use default memorySpaceId if not provided", async () => {
229
+ const request = createRequest("GET", {
230
+ searchParams: { userId: "testuser" },
231
+ });
232
+ await conversationsGet(request);
233
+
234
+ expect(mockCortex.conversations.list).toHaveBeenCalledWith(
235
+ expect.objectContaining({
236
+ memorySpaceId: "quickstart-demo",
237
+ userId: "testuser",
238
+ })
239
+ );
240
+ });
241
+ });
242
+
243
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
244
+ // POST /api/conversations
245
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
246
+
247
+ describe("POST /api/conversations", () => {
248
+ it("should return 400 if userId is missing", async () => {
249
+ const request = createRequest("POST", {
250
+ body: { title: "New Chat" },
251
+ });
252
+ const response = await conversationsPost(request);
253
+ const { status, data } = await parseResponse(response);
254
+
255
+ expect(status).toBe(400);
256
+ expect(data.error).toBe("userId is required");
257
+ });
258
+
259
+ it("should create conversation with generated ID", async () => {
260
+ const request = createRequest("POST", {
261
+ body: { userId: "testuser" },
262
+ });
263
+ const response = await conversationsPost(request);
264
+ const { status, data } = await parseResponse(response);
265
+
266
+ expect(status).toBe(200);
267
+ expect(data.success).toBe(true);
268
+ expect(data.conversation).toBeDefined();
269
+
270
+ const conversation = data.conversation as { id: string };
271
+ expect(conversation.id).toMatch(/^conv-\d+-[a-z0-9]+$/);
272
+ });
273
+
274
+ it("should create conversation with provided title", async () => {
275
+ const request = createRequest("POST", {
276
+ body: { userId: "testuser", title: "My Custom Title" },
277
+ });
278
+ const response = await conversationsPost(request);
279
+ const { status, data } = await parseResponse(response);
280
+
281
+ expect(status).toBe(200);
282
+ const conversation = data.conversation as { title: string };
283
+ expect(conversation.title).toBe("My Custom Title");
284
+ });
285
+
286
+ it("should use default title if not provided", async () => {
287
+ const request = createRequest("POST", {
288
+ body: { userId: "testuser" },
289
+ });
290
+ const response = await conversationsPost(request);
291
+ const { status, data } = await parseResponse(response);
292
+
293
+ expect(status).toBe(200);
294
+ const conversation = data.conversation as { title: string };
295
+ expect(conversation.title).toBe("New Chat");
296
+ });
297
+
298
+ it("should use provided memorySpaceId", async () => {
299
+ const request = createRequest("POST", {
300
+ body: { userId: "testuser", memorySpaceId: "custom-space" },
301
+ });
302
+ await conversationsPost(request);
303
+
304
+ expect(mockCortex.conversations.create).toHaveBeenCalledWith(
305
+ expect.objectContaining({
306
+ memorySpaceId: "custom-space",
307
+ })
308
+ );
309
+ });
310
+
311
+ it("should use default memorySpaceId if not provided", async () => {
312
+ const request = createRequest("POST", {
313
+ body: { userId: "testuser" },
314
+ });
315
+ await conversationsPost(request);
316
+
317
+ expect(mockCortex.conversations.create).toHaveBeenCalledWith(
318
+ expect.objectContaining({
319
+ memorySpaceId: "quickstart-demo",
320
+ })
321
+ );
322
+ });
323
+
324
+ it("should set conversation type to user-agent", async () => {
325
+ const request = createRequest("POST", {
326
+ body: { userId: "testuser" },
327
+ });
328
+ await conversationsPost(request);
329
+
330
+ expect(mockCortex.conversations.create).toHaveBeenCalledWith(
331
+ expect.objectContaining({
332
+ type: "user-agent",
333
+ })
334
+ );
335
+ });
336
+
337
+ it("should set participants with userId and agentId", async () => {
338
+ const request = createRequest("POST", {
339
+ body: { userId: "testuser" },
340
+ });
341
+ await conversationsPost(request);
342
+
343
+ expect(mockCortex.conversations.create).toHaveBeenCalledWith(
344
+ expect.objectContaining({
345
+ participants: {
346
+ userId: "testuser",
347
+ agentId: "quickstart-assistant",
348
+ },
349
+ })
350
+ );
351
+ });
352
+
353
+ it("should return conversation metadata", async () => {
354
+ const request = createRequest("POST", {
355
+ body: { userId: "testuser", title: "Test Chat" },
356
+ });
357
+ const response = await conversationsPost(request);
358
+ const { status, data } = await parseResponse(response);
359
+
360
+ expect(status).toBe(200);
361
+ const conversation = data.conversation as Record<string, unknown>;
362
+ expect(conversation.id).toBeDefined();
363
+ expect(conversation.title).toBe("Test Chat");
364
+ expect(conversation.createdAt).toBeDefined();
365
+ expect(conversation.updatedAt).toBeDefined();
366
+ expect(conversation.messageCount).toBe(0);
367
+ });
368
+ });
369
+
370
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
371
+ // DELETE /api/conversations
372
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
373
+
374
+ describe("DELETE /api/conversations", () => {
375
+ it("should return 400 if conversationId is missing", async () => {
376
+ const request = createRequest("DELETE");
377
+ const response = await conversationsDelete(request);
378
+ const { status, data } = await parseResponse(response);
379
+
380
+ expect(status).toBe(400);
381
+ expect(data.error).toBe("conversationId is required");
382
+ });
383
+
384
+ it("should delete conversation and return success", async () => {
385
+ // Seed a conversation
386
+ seedTestData.conversation("conv-to-delete", {
387
+ userId: "testuser",
388
+ title: "Delete Me",
389
+ });
390
+
391
+ const request = createRequest("DELETE", {
392
+ searchParams: { conversationId: "conv-to-delete" },
393
+ });
394
+ const response = await conversationsDelete(request);
395
+ const { status, data } = await parseResponse(response);
396
+
397
+ expect(status).toBe(200);
398
+ expect(data.success).toBe(true);
399
+ });
400
+
401
+ it("should call cortex.conversations.delete with correct ID", async () => {
402
+ const request = createRequest("DELETE", {
403
+ searchParams: { conversationId: "conv-123" },
404
+ });
405
+ await conversationsDelete(request);
406
+
407
+ expect(mockCortex.conversations.delete).toHaveBeenCalledWith("conv-123");
408
+ });
409
+ });
410
+
411
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
412
+ // Edge Cases
413
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
414
+
415
+ describe("edge cases", () => {
416
+ it("should handle SDK errors gracefully for GET", async () => {
417
+ mockCortex.conversations.list = jest
418
+ .fn()
419
+ .mockRejectedValue(new Error("SDK error"));
420
+
421
+ const request = createRequest("GET", {
422
+ searchParams: { userId: "testuser" },
423
+ });
424
+ const response = await conversationsGet(request);
425
+ const { status, data } = await parseResponse(response);
426
+
427
+ expect(status).toBe(500);
428
+ expect(data.error).toBe("Failed to fetch conversations");
429
+ });
430
+
431
+ it("should handle SDK errors gracefully for POST", async () => {
432
+ mockCortex.conversations.create = jest
433
+ .fn()
434
+ .mockRejectedValue(new Error("SDK error"));
435
+
436
+ const request = createRequest("POST", {
437
+ body: { userId: "testuser" },
438
+ });
439
+ const response = await conversationsPost(request);
440
+ const { status, data } = await parseResponse(response);
441
+
442
+ expect(status).toBe(500);
443
+ expect(data.error).toBe("Failed to create conversation");
444
+ });
445
+
446
+ it("should handle SDK errors gracefully for DELETE", async () => {
447
+ mockCortex.conversations.delete = jest
448
+ .fn()
449
+ .mockRejectedValue(new Error("SDK error"));
450
+
451
+ const request = createRequest("DELETE", {
452
+ searchParams: { conversationId: "conv-123" },
453
+ });
454
+ const response = await conversationsDelete(request);
455
+ const { status, data } = await parseResponse(response);
456
+
457
+ expect(status).toBe(500);
458
+ expect(data.error).toBe("Failed to delete conversation");
459
+ });
460
+ });
461
+ });
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Unit Tests: Password Utilities
3
+ *
4
+ * Tests password hashing, verification, and session token generation.
5
+ */
6
+
7
+ import {
8
+ hashPassword,
9
+ verifyPassword,
10
+ generateSessionToken,
11
+ } from "../../lib/password";
12
+
13
+ describe("Password Utilities", () => {
14
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15
+ // hashPassword
16
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17
+
18
+ describe("hashPassword", () => {
19
+ it("should return hash in salt:hash format", async () => {
20
+ const hash = await hashPassword("testpassword");
21
+
22
+ expect(hash).toContain(":");
23
+ const [salt, hashPart] = hash.split(":");
24
+ expect(salt).toBeDefined();
25
+ expect(hashPart).toBeDefined();
26
+ expect(salt.length).toBeGreaterThan(0);
27
+ expect(hashPart.length).toBeGreaterThan(0);
28
+ });
29
+
30
+ it("should produce different hashes for same password (unique salts)", async () => {
31
+ const hash1 = await hashPassword("samepassword");
32
+ const hash2 = await hashPassword("samepassword");
33
+
34
+ expect(hash1).not.toBe(hash2);
35
+
36
+ // Salts should be different
37
+ const [salt1] = hash1.split(":");
38
+ const [salt2] = hash2.split(":");
39
+ expect(salt1).not.toBe(salt2);
40
+ });
41
+
42
+ it("should produce base64-encoded output", async () => {
43
+ const hash = await hashPassword("testpassword");
44
+ const [salt, hashPart] = hash.split(":");
45
+
46
+ // Base64 regex pattern
47
+ const base64Pattern = /^[A-Za-z0-9+/]+=*$/;
48
+ expect(salt).toMatch(base64Pattern);
49
+ expect(hashPart).toMatch(base64Pattern);
50
+ });
51
+
52
+ it("should handle empty password", async () => {
53
+ const hash = await hashPassword("");
54
+
55
+ expect(hash).toContain(":");
56
+ const [salt, hashPart] = hash.split(":");
57
+ expect(salt.length).toBeGreaterThan(0);
58
+ expect(hashPart.length).toBeGreaterThan(0);
59
+ });
60
+
61
+ it("should handle special characters in password", async () => {
62
+ const hash = await hashPassword("p@$$w0rd!#$%^&*()");
63
+
64
+ expect(hash).toContain(":");
65
+ const verified = await verifyPassword("p@$$w0rd!#$%^&*()", hash);
66
+ expect(verified).toBe(true);
67
+ });
68
+
69
+ it("should handle unicode characters in password", async () => {
70
+ const hash = await hashPassword("пароль密码🔒");
71
+
72
+ expect(hash).toContain(":");
73
+ const verified = await verifyPassword("пароль密码🔒", hash);
74
+ expect(verified).toBe(true);
75
+ });
76
+
77
+ it("should handle very long passwords", async () => {
78
+ const longPassword = "a".repeat(1000);
79
+ const hash = await hashPassword(longPassword);
80
+
81
+ expect(hash).toContain(":");
82
+ const verified = await verifyPassword(longPassword, hash);
83
+ expect(verified).toBe(true);
84
+ });
85
+ });
86
+
87
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
88
+ // verifyPassword
89
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
90
+
91
+ describe("verifyPassword", () => {
92
+ it("should return true for correct password", async () => {
93
+ const password = "correctpassword";
94
+ const hash = await hashPassword(password);
95
+
96
+ const result = await verifyPassword(password, hash);
97
+
98
+ expect(result).toBe(true);
99
+ });
100
+
101
+ it("should return false for wrong password", async () => {
102
+ const hash = await hashPassword("correctpassword");
103
+
104
+ const result = await verifyPassword("wrongpassword", hash);
105
+
106
+ expect(result).toBe(false);
107
+ });
108
+
109
+ it("should return false for malformed hash (no colon)", async () => {
110
+ const result = await verifyPassword("password", "invalidhashformat");
111
+
112
+ expect(result).toBe(false);
113
+ });
114
+
115
+ it("should return false for malformed hash (empty parts)", async () => {
116
+ const result = await verifyPassword("password", ":");
117
+
118
+ expect(result).toBe(false);
119
+ });
120
+
121
+ it("should return false for malformed hash (only salt)", async () => {
122
+ const result = await verifyPassword("password", "somesalt:");
123
+
124
+ expect(result).toBe(false);
125
+ });
126
+
127
+ it("should return false for malformed hash (only hash)", async () => {
128
+ const result = await verifyPassword("password", ":somehash");
129
+
130
+ expect(result).toBe(false);
131
+ });
132
+
133
+ it("should return false for empty stored hash", async () => {
134
+ const result = await verifyPassword("password", "");
135
+
136
+ expect(result).toBe(false);
137
+ });
138
+
139
+ it("should return false for invalid base64 in salt", async () => {
140
+ const result = await verifyPassword("password", "!!!invalid!!!:validhash");
141
+
142
+ expect(result).toBe(false);
143
+ });
144
+
145
+ it("should be case-sensitive for passwords", async () => {
146
+ const hash = await hashPassword("Password");
147
+
148
+ const resultLower = await verifyPassword("password", hash);
149
+ const resultUpper = await verifyPassword("PASSWORD", hash);
150
+ const resultCorrect = await verifyPassword("Password", hash);
151
+
152
+ expect(resultLower).toBe(false);
153
+ expect(resultUpper).toBe(false);
154
+ expect(resultCorrect).toBe(true);
155
+ });
156
+
157
+ it("should handle whitespace in passwords", async () => {
158
+ const hash = await hashPassword("pass word");
159
+
160
+ const withSpace = await verifyPassword("pass word", hash);
161
+ const withoutSpace = await verifyPassword("password", hash);
162
+
163
+ expect(withSpace).toBe(true);
164
+ expect(withoutSpace).toBe(false);
165
+ });
166
+ });
167
+
168
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
169
+ // generateSessionToken
170
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
171
+
172
+ describe("generateSessionToken", () => {
173
+ it("should return 64-character hex string", () => {
174
+ const token = generateSessionToken();
175
+
176
+ expect(token).toHaveLength(64);
177
+ expect(token).toMatch(/^[0-9a-f]+$/);
178
+ });
179
+
180
+ it("should produce unique tokens on each call", () => {
181
+ const tokens = new Set<string>();
182
+
183
+ for (let i = 0; i < 100; i++) {
184
+ tokens.add(generateSessionToken());
185
+ }
186
+
187
+ expect(tokens.size).toBe(100);
188
+ });
189
+
190
+ it("should be lowercase hex", () => {
191
+ const token = generateSessionToken();
192
+
193
+ expect(token).toBe(token.toLowerCase());
194
+ });
195
+
196
+ it("should not contain non-hex characters", () => {
197
+ const token = generateSessionToken();
198
+
199
+ expect(token).not.toMatch(/[g-zG-Z]/);
200
+ });
201
+ });
202
+
203
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
204
+ // Integration: Hash and Verify Round-Trip
205
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
206
+
207
+ describe("hash and verify round-trip", () => {
208
+ const testPasswords = [
209
+ "simple",
210
+ "with spaces",
211
+ "UPPERCASE",
212
+ "MixedCase123",
213
+ "!@#$%^&*()",
214
+ "12345678",
215
+ "日本語パスワード",
216
+ "emoji🔐🔑🔓",
217
+ ];
218
+
219
+ testPasswords.forEach((password) => {
220
+ it(`should round-trip password: "${password}"`, async () => {
221
+ const hash = await hashPassword(password);
222
+ const verified = await verifyPassword(password, hash);
223
+
224
+ expect(verified).toBe(true);
225
+ });
226
+ });
227
+ });
228
+ });
@@ -29,5 +29,5 @@
29
29
  ".next/types/**/*.ts",
30
30
  ".next/dev/types/**/*.ts"
31
31
  ],
32
- "exclude": ["node_modules", "convex"]
32
+ "exclude": ["node_modules", "convex", "tests", "jest.config.js"]
33
33
  }