@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.
- package/dist/commands/convex.js +1 -1
- package/dist/commands/convex.js.map +1 -1
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +771 -144
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +89 -26
- package/dist/commands/dev.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/utils/app-template-sync.d.ts +95 -0
- package/dist/utils/app-template-sync.d.ts.map +1 -0
- package/dist/utils/app-template-sync.js +425 -0
- package/dist/utils/app-template-sync.js.map +1 -0
- package/dist/utils/deployment-selector.d.ts +21 -0
- package/dist/utils/deployment-selector.d.ts.map +1 -1
- package/dist/utils/deployment-selector.js +32 -0
- package/dist/utils/deployment-selector.js.map +1 -1
- package/dist/utils/init/graph-setup.d.ts.map +1 -1
- package/dist/utils/init/graph-setup.js +13 -2
- package/dist/utils/init/graph-setup.js.map +1 -1
- package/package.json +1 -1
- package/templates/vercel-ai-quickstart/app/api/auth/check/route.ts +30 -0
- package/templates/vercel-ai-quickstart/app/api/auth/login/route.ts +83 -0
- package/templates/vercel-ai-quickstart/app/api/auth/register/route.ts +94 -0
- package/templates/vercel-ai-quickstart/app/api/auth/setup/route.ts +59 -0
- package/templates/vercel-ai-quickstart/app/api/chat/route.ts +83 -2
- package/templates/vercel-ai-quickstart/app/api/conversations/route.ts +179 -0
- package/templates/vercel-ai-quickstart/app/globals.css +161 -0
- package/templates/vercel-ai-quickstart/app/page.tsx +93 -8
- package/templates/vercel-ai-quickstart/components/AdminSetup.tsx +139 -0
- package/templates/vercel-ai-quickstart/components/AuthProvider.tsx +283 -0
- package/templates/vercel-ai-quickstart/components/ChatHistorySidebar.tsx +323 -0
- package/templates/vercel-ai-quickstart/components/ChatInterface.tsx +113 -16
- package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +202 -0
- package/templates/vercel-ai-quickstart/jest.config.js +45 -0
- package/templates/vercel-ai-quickstart/lib/cortex.ts +27 -0
- package/templates/vercel-ai-quickstart/lib/password.ts +120 -0
- package/templates/vercel-ai-quickstart/next.config.js +20 -0
- package/templates/vercel-ai-quickstart/package.json +7 -2
- package/templates/vercel-ai-quickstart/tests/helpers/mock-cortex.ts +263 -0
- package/templates/vercel-ai-quickstart/tests/helpers/setup.ts +48 -0
- package/templates/vercel-ai-quickstart/tests/integration/auth.test.ts +455 -0
- package/templates/vercel-ai-quickstart/tests/integration/conversations.test.ts +461 -0
- package/templates/vercel-ai-quickstart/tests/unit/password.test.ts +228 -0
- 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
|
+
});
|