@cortexmemory/cli 0.27.1 → 0.27.4
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 +839 -141
- 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 +445 -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 +128 -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 +139 -3
- package/templates/vercel-ai-quickstart/app/api/chat-v6/route.ts +333 -0
- 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 +110 -11
- 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 +117 -17
- package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +202 -0
- package/templates/vercel-ai-quickstart/jest.config.js +52 -0
- package/templates/vercel-ai-quickstart/lib/agents/memory-agent.ts +165 -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/lib/versions.ts +60 -0
- package/templates/vercel-ai-quickstart/next.config.js +20 -0
- package/templates/vercel-ai-quickstart/package.json +11 -2
- package/templates/vercel-ai-quickstart/test-api.mjs +272 -0
- package/templates/vercel-ai-quickstart/tests/e2e/chat-memory-flow.test.ts +454 -0
- 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,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Tests: Auth API Routes
|
|
3
|
+
*
|
|
4
|
+
* Tests the authentication 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
|
+
import { hashPassword } from "../../lib/password";
|
|
14
|
+
|
|
15
|
+
// Mock the Cortex SDK module
|
|
16
|
+
let mockCortex: MockCortex;
|
|
17
|
+
|
|
18
|
+
jest.mock("../../lib/cortex", () => ({
|
|
19
|
+
getCortex: () => mockCortex,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
// Import route handlers after mocking
|
|
23
|
+
import { GET as checkGet } from "../../app/api/auth/check/route";
|
|
24
|
+
import { POST as setupPost } from "../../app/api/auth/setup/route";
|
|
25
|
+
import { POST as registerPost } from "../../app/api/auth/register/route";
|
|
26
|
+
import { POST as loginPost } from "../../app/api/auth/login/route";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Helper to create a mock Request object
|
|
30
|
+
*/
|
|
31
|
+
function createRequest(
|
|
32
|
+
method: string,
|
|
33
|
+
body?: Record<string, unknown>,
|
|
34
|
+
url: string = "http://localhost:3000"
|
|
35
|
+
): Request {
|
|
36
|
+
const init: RequestInit = { method };
|
|
37
|
+
if (body) {
|
|
38
|
+
init.body = JSON.stringify(body);
|
|
39
|
+
init.headers = { "Content-Type": "application/json" };
|
|
40
|
+
}
|
|
41
|
+
return new Request(url, init);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Helper to parse JSON response
|
|
46
|
+
*/
|
|
47
|
+
async function parseResponse(response: Response): Promise<{
|
|
48
|
+
status: number;
|
|
49
|
+
data: Record<string, unknown>;
|
|
50
|
+
}> {
|
|
51
|
+
const data = await response.json();
|
|
52
|
+
return { status: response.status, data };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("Auth API Routes", () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
resetMockStores();
|
|
58
|
+
mockCortex = createMockCortex();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
62
|
+
// GET /api/auth/check
|
|
63
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
64
|
+
|
|
65
|
+
describe("GET /api/auth/check", () => {
|
|
66
|
+
it("should return isSetup: false when admin not configured", async () => {
|
|
67
|
+
const response = await checkGet();
|
|
68
|
+
const { status, data } = await parseResponse(response);
|
|
69
|
+
|
|
70
|
+
expect(status).toBe(200);
|
|
71
|
+
expect(data.isSetup).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should return isSetup: true when admin configured", async () => {
|
|
75
|
+
// Seed admin password hash
|
|
76
|
+
seedTestData.mutable(
|
|
77
|
+
"quickstart-config",
|
|
78
|
+
"admin_password_hash",
|
|
79
|
+
"somehash:value"
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const response = await checkGet();
|
|
83
|
+
const { status, data } = await parseResponse(response);
|
|
84
|
+
|
|
85
|
+
expect(status).toBe(200);
|
|
86
|
+
expect(data.isSetup).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should call cortex.mutable.get with correct namespace and key", async () => {
|
|
90
|
+
await checkGet();
|
|
91
|
+
|
|
92
|
+
expect(mockCortex.mutable.get).toHaveBeenCalledWith(
|
|
93
|
+
"quickstart-config",
|
|
94
|
+
"admin_password_hash"
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
100
|
+
// POST /api/auth/setup
|
|
101
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
102
|
+
|
|
103
|
+
describe("POST /api/auth/setup", () => {
|
|
104
|
+
it("should return 400 if password is missing", async () => {
|
|
105
|
+
const request = createRequest("POST", {});
|
|
106
|
+
const response = await setupPost(request);
|
|
107
|
+
const { status, data } = await parseResponse(response);
|
|
108
|
+
|
|
109
|
+
expect(status).toBe(400);
|
|
110
|
+
expect(data.error).toBe("Password is required");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should return 400 if password is not a string", async () => {
|
|
114
|
+
const request = createRequest("POST", { password: 12345 });
|
|
115
|
+
const response = await setupPost(request);
|
|
116
|
+
const { status, data } = await parseResponse(response);
|
|
117
|
+
|
|
118
|
+
expect(status).toBe(400);
|
|
119
|
+
expect(data.error).toBe("Password is required");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should return 400 if password is less than 4 characters", async () => {
|
|
123
|
+
const request = createRequest("POST", { password: "abc" });
|
|
124
|
+
const response = await setupPost(request);
|
|
125
|
+
const { status, data } = await parseResponse(response);
|
|
126
|
+
|
|
127
|
+
expect(status).toBe(400);
|
|
128
|
+
expect(data.error).toBe("Password must be at least 4 characters");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should return 409 if admin already configured", async () => {
|
|
132
|
+
// Seed existing admin
|
|
133
|
+
seedTestData.mutable(
|
|
134
|
+
"quickstart-config",
|
|
135
|
+
"admin_password_hash",
|
|
136
|
+
"existing:hash"
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const request = createRequest("POST", { password: "newpassword" });
|
|
140
|
+
const response = await setupPost(request);
|
|
141
|
+
const { status, data } = await parseResponse(response);
|
|
142
|
+
|
|
143
|
+
expect(status).toBe(409);
|
|
144
|
+
expect(data.error).toBe("Admin already configured");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should store hashed password and return success", async () => {
|
|
148
|
+
const request = createRequest("POST", { password: "adminpass123" });
|
|
149
|
+
const response = await setupPost(request);
|
|
150
|
+
const { status, data } = await parseResponse(response);
|
|
151
|
+
|
|
152
|
+
expect(status).toBe(200);
|
|
153
|
+
expect(data.success).toBe(true);
|
|
154
|
+
expect(data.message).toBe("Admin password configured successfully");
|
|
155
|
+
|
|
156
|
+
// Verify mutable.set was called with a hashed password
|
|
157
|
+
expect(mockCortex.mutable.set).toHaveBeenCalledWith(
|
|
158
|
+
"quickstart-config",
|
|
159
|
+
"admin_password_hash",
|
|
160
|
+
expect.stringContaining(":") // salt:hash format
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
166
|
+
// POST /api/auth/register
|
|
167
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
168
|
+
|
|
169
|
+
describe("POST /api/auth/register", () => {
|
|
170
|
+
it("should return 400 if username is missing", async () => {
|
|
171
|
+
const request = createRequest("POST", { password: "pass1234" });
|
|
172
|
+
const response = await registerPost(request);
|
|
173
|
+
const { status, data } = await parseResponse(response);
|
|
174
|
+
|
|
175
|
+
expect(status).toBe(400);
|
|
176
|
+
expect(data.error).toBe("Username is required");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should return 400 if password is missing", async () => {
|
|
180
|
+
const request = createRequest("POST", { username: "testuser" });
|
|
181
|
+
const response = await registerPost(request);
|
|
182
|
+
const { status, data } = await parseResponse(response);
|
|
183
|
+
|
|
184
|
+
expect(status).toBe(400);
|
|
185
|
+
expect(data.error).toBe("Password is required");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should return 400 if username is less than 2 characters", async () => {
|
|
189
|
+
const request = createRequest("POST", {
|
|
190
|
+
username: "a",
|
|
191
|
+
password: "pass1234",
|
|
192
|
+
});
|
|
193
|
+
const response = await registerPost(request);
|
|
194
|
+
const { status, data } = await parseResponse(response);
|
|
195
|
+
|
|
196
|
+
expect(status).toBe(400);
|
|
197
|
+
expect(data.error).toBe("Username must be at least 2 characters");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should return 400 if password is less than 4 characters", async () => {
|
|
201
|
+
const request = createRequest("POST", {
|
|
202
|
+
username: "testuser",
|
|
203
|
+
password: "abc",
|
|
204
|
+
});
|
|
205
|
+
const response = await registerPost(request);
|
|
206
|
+
const { status, data } = await parseResponse(response);
|
|
207
|
+
|
|
208
|
+
expect(status).toBe(400);
|
|
209
|
+
expect(data.error).toBe("Password must be at least 4 characters");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("should return 400 for invalid username characters", async () => {
|
|
213
|
+
const request = createRequest("POST", {
|
|
214
|
+
username: "test user@!",
|
|
215
|
+
password: "pass1234",
|
|
216
|
+
});
|
|
217
|
+
const response = await registerPost(request);
|
|
218
|
+
const { status, data } = await parseResponse(response);
|
|
219
|
+
|
|
220
|
+
expect(status).toBe(400);
|
|
221
|
+
expect(data.error).toContain("can only contain");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should return 409 if username already exists", async () => {
|
|
225
|
+
// Seed existing user
|
|
226
|
+
seedTestData.user("existinguser", { displayName: "Existing User" });
|
|
227
|
+
|
|
228
|
+
const request = createRequest("POST", {
|
|
229
|
+
username: "existinguser",
|
|
230
|
+
password: "pass1234",
|
|
231
|
+
});
|
|
232
|
+
const response = await registerPost(request);
|
|
233
|
+
const { status, data } = await parseResponse(response);
|
|
234
|
+
|
|
235
|
+
expect(status).toBe(409);
|
|
236
|
+
expect(data.error).toBe("Username already taken");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should create user and return success with sessionToken", async () => {
|
|
240
|
+
const request = createRequest("POST", {
|
|
241
|
+
username: "NewUser",
|
|
242
|
+
password: "pass1234",
|
|
243
|
+
displayName: "New User Display",
|
|
244
|
+
});
|
|
245
|
+
const response = await registerPost(request);
|
|
246
|
+
const { status, data } = await parseResponse(response);
|
|
247
|
+
|
|
248
|
+
expect(status).toBe(200);
|
|
249
|
+
expect(data.success).toBe(true);
|
|
250
|
+
expect(data.user).toEqual({
|
|
251
|
+
id: "newuser", // lowercase
|
|
252
|
+
displayName: "New User Display",
|
|
253
|
+
});
|
|
254
|
+
expect(data.sessionToken).toBeDefined();
|
|
255
|
+
expect((data.sessionToken as string).length).toBe(64);
|
|
256
|
+
|
|
257
|
+
// Verify user was created with hashed password
|
|
258
|
+
expect(mockCortex.users.update).toHaveBeenCalledWith(
|
|
259
|
+
"newuser",
|
|
260
|
+
expect.objectContaining({
|
|
261
|
+
displayName: "New User Display",
|
|
262
|
+
passwordHash: expect.stringContaining(":"),
|
|
263
|
+
createdAt: expect.any(Number),
|
|
264
|
+
lastLoginAt: expect.any(Number),
|
|
265
|
+
})
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("should normalize username to lowercase", async () => {
|
|
270
|
+
const request = createRequest("POST", {
|
|
271
|
+
username: "TestUser",
|
|
272
|
+
password: "pass1234",
|
|
273
|
+
});
|
|
274
|
+
const response = await registerPost(request);
|
|
275
|
+
const { status, data } = await parseResponse(response);
|
|
276
|
+
|
|
277
|
+
expect(status).toBe(200);
|
|
278
|
+
expect((data.user as { id: string }).id).toBe("testuser");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("should use username as displayName if not provided", async () => {
|
|
282
|
+
const request = createRequest("POST", {
|
|
283
|
+
username: "testuser",
|
|
284
|
+
password: "pass1234",
|
|
285
|
+
});
|
|
286
|
+
const response = await registerPost(request);
|
|
287
|
+
const { status, data } = await parseResponse(response);
|
|
288
|
+
|
|
289
|
+
expect(status).toBe(200);
|
|
290
|
+
expect((data.user as { displayName: string }).displayName).toBe(
|
|
291
|
+
"testuser"
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("should allow underscores and hyphens in username", async () => {
|
|
296
|
+
const request = createRequest("POST", {
|
|
297
|
+
username: "test_user-123",
|
|
298
|
+
password: "pass1234",
|
|
299
|
+
});
|
|
300
|
+
const response = await registerPost(request);
|
|
301
|
+
const { status, data } = await parseResponse(response);
|
|
302
|
+
|
|
303
|
+
expect(status).toBe(200);
|
|
304
|
+
expect((data.user as { id: string }).id).toBe("test_user-123");
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
309
|
+
// POST /api/auth/login
|
|
310
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
311
|
+
|
|
312
|
+
describe("POST /api/auth/login", () => {
|
|
313
|
+
it("should return 400 if username is missing", async () => {
|
|
314
|
+
const request = createRequest("POST", { password: "pass1234" });
|
|
315
|
+
const response = await loginPost(request);
|
|
316
|
+
const { status, data } = await parseResponse(response);
|
|
317
|
+
|
|
318
|
+
expect(status).toBe(400);
|
|
319
|
+
expect(data.error).toBe("Username is required");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("should return 400 if password is missing", async () => {
|
|
323
|
+
const request = createRequest("POST", { username: "testuser" });
|
|
324
|
+
const response = await loginPost(request);
|
|
325
|
+
const { status, data } = await parseResponse(response);
|
|
326
|
+
|
|
327
|
+
expect(status).toBe(400);
|
|
328
|
+
expect(data.error).toBe("Password is required");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("should return 401 for non-existent user", async () => {
|
|
332
|
+
const request = createRequest("POST", {
|
|
333
|
+
username: "nonexistent",
|
|
334
|
+
password: "pass1234",
|
|
335
|
+
});
|
|
336
|
+
const response = await loginPost(request);
|
|
337
|
+
const { status, data } = await parseResponse(response);
|
|
338
|
+
|
|
339
|
+
expect(status).toBe(401);
|
|
340
|
+
expect(data.error).toBe("Invalid username or password");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("should return 401 for wrong password", async () => {
|
|
344
|
+
// Seed user with known password hash
|
|
345
|
+
const passwordHash = await hashPassword("correctpassword");
|
|
346
|
+
seedTestData.user("testuser", {
|
|
347
|
+
displayName: "Test User",
|
|
348
|
+
passwordHash,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const request = createRequest("POST", {
|
|
352
|
+
username: "testuser",
|
|
353
|
+
password: "wrongpassword",
|
|
354
|
+
});
|
|
355
|
+
const response = await loginPost(request);
|
|
356
|
+
const { status, data } = await parseResponse(response);
|
|
357
|
+
|
|
358
|
+
expect(status).toBe(401);
|
|
359
|
+
expect(data.error).toBe("Invalid username or password");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("should return 401 if user has no password hash", async () => {
|
|
363
|
+
// Seed user without password hash
|
|
364
|
+
seedTestData.user("testuser", { displayName: "Test User" });
|
|
365
|
+
|
|
366
|
+
const request = createRequest("POST", {
|
|
367
|
+
username: "testuser",
|
|
368
|
+
password: "anypassword",
|
|
369
|
+
});
|
|
370
|
+
const response = await loginPost(request);
|
|
371
|
+
const { status, data } = await parseResponse(response);
|
|
372
|
+
|
|
373
|
+
expect(status).toBe(401);
|
|
374
|
+
expect(data.error).toBe("Invalid username or password");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("should return success with user data and sessionToken for valid credentials", async () => {
|
|
378
|
+
// Seed user with known password
|
|
379
|
+
const passwordHash = await hashPassword("correctpassword");
|
|
380
|
+
seedTestData.user("testuser", {
|
|
381
|
+
displayName: "Test User Display",
|
|
382
|
+
passwordHash,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const request = createRequest("POST", {
|
|
386
|
+
username: "testuser",
|
|
387
|
+
password: "correctpassword",
|
|
388
|
+
});
|
|
389
|
+
const response = await loginPost(request);
|
|
390
|
+
const { status, data } = await parseResponse(response);
|
|
391
|
+
|
|
392
|
+
expect(status).toBe(200);
|
|
393
|
+
expect(data.success).toBe(true);
|
|
394
|
+
expect(data.user).toEqual({
|
|
395
|
+
id: "testuser",
|
|
396
|
+
displayName: "Test User Display",
|
|
397
|
+
});
|
|
398
|
+
expect(data.sessionToken).toBeDefined();
|
|
399
|
+
expect((data.sessionToken as string).length).toBe(64);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("should update lastLoginAt on successful login", async () => {
|
|
403
|
+
const passwordHash = await hashPassword("correctpassword");
|
|
404
|
+
seedTestData.user("testuser", {
|
|
405
|
+
displayName: "Test User",
|
|
406
|
+
passwordHash,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const request = createRequest("POST", {
|
|
410
|
+
username: "testuser",
|
|
411
|
+
password: "correctpassword",
|
|
412
|
+
});
|
|
413
|
+
await loginPost(request);
|
|
414
|
+
|
|
415
|
+
expect(mockCortex.users.update).toHaveBeenCalledWith("testuser", {
|
|
416
|
+
lastLoginAt: expect.any(Number),
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("should normalize username to lowercase for lookup", async () => {
|
|
421
|
+
const passwordHash = await hashPassword("correctpassword");
|
|
422
|
+
seedTestData.user("testuser", {
|
|
423
|
+
displayName: "Test User",
|
|
424
|
+
passwordHash,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const request = createRequest("POST", {
|
|
428
|
+
username: "TestUser",
|
|
429
|
+
password: "correctpassword",
|
|
430
|
+
});
|
|
431
|
+
const response = await loginPost(request);
|
|
432
|
+
const { status, data } = await parseResponse(response);
|
|
433
|
+
|
|
434
|
+
expect(status).toBe(200);
|
|
435
|
+
expect(data.success).toBe(true);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("should use username as displayName if displayName not set", async () => {
|
|
439
|
+
const passwordHash = await hashPassword("correctpassword");
|
|
440
|
+
seedTestData.user("testuser", { passwordHash }); // No displayName
|
|
441
|
+
|
|
442
|
+
const request = createRequest("POST", {
|
|
443
|
+
username: "testuser",
|
|
444
|
+
password: "correctpassword",
|
|
445
|
+
});
|
|
446
|
+
const response = await loginPost(request);
|
|
447
|
+
const { status, data } = await parseResponse(response);
|
|
448
|
+
|
|
449
|
+
expect(status).toBe(200);
|
|
450
|
+
expect((data.user as { displayName: string }).displayName).toBe(
|
|
451
|
+
"testuser"
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
});
|