@joystick.js/db-canary 0.0.0-canary.2294 → 0.0.0-canary.2296

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,525 @@
1
+ import test from 'ava';
2
+ import {
3
+ setup_initial_admin,
4
+ create_user,
5
+ get_user,
6
+ update_user,
7
+ delete_user,
8
+ list_users,
9
+ verify_credentials,
10
+ reset_user_password,
11
+ get_auth_stats,
12
+ reset_auth_state,
13
+ set_storage_engine
14
+ } from '../../../src/server/lib/user_auth_manager.js';
15
+ import { initialize_database } from '../../../src/server/lib/query_engine.js';
16
+ import { existsSync, unlinkSync } from 'fs';
17
+ import { mkdirSync } from 'fs';
18
+
19
+ let storage_engine;
20
+
21
+ test.beforeEach(async () => {
22
+ await reset_auth_state();
23
+
24
+ // Clean up any existing database files
25
+ try {
26
+ if (existsSync('./.joystick/data/joystickdb_test/data.mdb')) {
27
+ unlinkSync('./.joystick/data/joystickdb_test/data.mdb');
28
+ }
29
+ if (existsSync('./.joystick/data/joystickdb_test/lock.mdb')) {
30
+ unlinkSync('./.joystick/data/joystickdb_test/lock.mdb');
31
+ }
32
+ } catch (error) {
33
+ // Ignore cleanup errors
34
+ }
35
+
36
+ // Create test directory if it doesn't exist
37
+ try {
38
+ mkdirSync('./.joystick/data/joystickdb_test', { recursive: true });
39
+ } catch (error) {
40
+ // Directory might already exist
41
+ }
42
+
43
+ // Create real storage engine for testing
44
+ storage_engine = initialize_database('./.joystick/data/joystickdb_test');
45
+ set_storage_engine(storage_engine);
46
+ });
47
+
48
+ test.afterEach(async () => {
49
+ // Clean up users from admin database
50
+ try {
51
+ const users_result = await list_users();
52
+ if (users_result.success && users_result.users && users_result.users.length > 0) {
53
+ for (const user of users_result.users) {
54
+ await delete_user(user.username);
55
+ }
56
+ }
57
+ } catch (error) {
58
+ // Ignore cleanup errors
59
+ }
60
+
61
+ await reset_auth_state();
62
+
63
+ // NOTE: Don't close database between tests to avoid transaction errors
64
+ // The database will be closed when the test process exits
65
+ });
66
+
67
+ // Initial Admin Setup Tests
68
+ test('setup_initial_admin creates first admin user successfully', async (t) => {
69
+ const result = await setup_initial_admin('admin', 'admin123', 'admin@test.com');
70
+
71
+ t.is(result.success, true);
72
+ t.truthy(result.admin_user);
73
+ t.is(result.admin_user.user.username, 'admin');
74
+ t.is(result.admin_user.user.role, 'admin');
75
+ t.is(result.admin_user.user.active, true);
76
+ t.truthy(result.admin_user.user.created_at);
77
+ t.falsy(result.admin_user.user.password_hash); // Should not return password hash
78
+
79
+ // Verify user can be retrieved
80
+ const user_result = await get_user('admin');
81
+ t.is(user_result.success, true);
82
+ t.is(user_result.user.username, 'admin');
83
+ t.is(user_result.user.role, 'admin');
84
+ });
85
+
86
+ test('setup_initial_admin prevents duplicate admin creation', async (t) => {
87
+ // Create initial admin
88
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
89
+
90
+ // Try to create another admin
91
+ const result = await setup_initial_admin('admin2', 'admin456', 'admin2@test.com');
92
+
93
+ t.is(result.success, false);
94
+ t.truthy(result.error.includes('Initial admin user already exists'));
95
+ });
96
+
97
+ test('setup_initial_admin validates required fields', async (t) => {
98
+ // Missing username
99
+ let result = await setup_initial_admin('', 'admin123', 'admin@test.com');
100
+ t.is(result.success, false);
101
+ t.truthy(result.error.includes('Username, password, and email are required'));
102
+
103
+ // Missing password
104
+ result = await setup_initial_admin('admin', '', 'admin@test.com');
105
+ t.is(result.success, false);
106
+ t.truthy(result.error.includes('Username, password, and email are required'));
107
+
108
+ // Missing email
109
+ result = await setup_initial_admin('admin', 'admin123', '');
110
+ t.is(result.success, false);
111
+ t.truthy(result.error.includes('Username, password, and email are required'));
112
+ });
113
+
114
+ test('setup_initial_admin validates password strength', async (t) => {
115
+ const result = await setup_initial_admin('admin', '123', 'admin@test.com');
116
+
117
+ t.is(result.success, false);
118
+ t.truthy(result.error.includes('Password must be at least 6 characters long'));
119
+ });
120
+
121
+ test('setup_initial_admin validates email format', async (t) => {
122
+ const result = await setup_initial_admin('admin', 'admin123', 'invalid-email');
123
+
124
+ t.is(result.success, false);
125
+ t.truthy(result.error.includes('Invalid email format'));
126
+ });
127
+
128
+ // User Creation Tests
129
+ test('create_user creates regular user successfully', async (t) => {
130
+ // Setup admin first
131
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
132
+
133
+ const result = await create_user('user1', 'user12345678', 'user1@test.com', 'user');
134
+
135
+ t.is(result.success, true);
136
+ t.is(result.user.username, 'user1');
137
+ t.is(result.user.role, 'user');
138
+ t.is(result.user.active, true);
139
+ t.falsy(result.user.password_hash); // Should not return password hash
140
+
141
+ // Verify user can be retrieved
142
+ const user_result = await get_user('user1');
143
+ t.is(user_result.success, true);
144
+ t.is(user_result.user.username, 'user1');
145
+ t.is(user_result.user.role, 'user');
146
+ });
147
+
148
+ test('create_user prevents duplicate usernames', async (t) => {
149
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
150
+ await create_user('user1', 'user12345678', 'user1@test.com');
151
+
152
+ const result = await create_user('user1', 'user456789', 'user1b@test.com');
153
+
154
+ t.is(result.success, false);
155
+ t.truthy(result.error.includes('User already exists'));
156
+ });
157
+
158
+ test('create_user validates input fields', async (t) => {
159
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
160
+
161
+ // Missing username
162
+ let result = await create_user('', 'user12345678', 'user@test.com');
163
+ t.is(result.success, false);
164
+ t.truthy(result.error && (result.error.includes('Username is required') || result.error.includes('Username, password, and email are required') || result.error.includes('required')));
165
+
166
+ // Invalid role
167
+ result = await create_user('user1', 'user12345678', 'user@test.com', 'invalid');
168
+ t.is(result.success, false);
169
+ t.truthy(result.error && (result.error.includes('Role must be either "admin" or "user"') || result.error.includes('invalid role') || result.error.includes('Role')));
170
+ });
171
+
172
+ // User Retrieval Tests
173
+ test('get_user retrieves existing user', async (t) => {
174
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
175
+ await create_user('user1', 'user12345678', 'user1@test.com');
176
+
177
+ const result = await get_user('user1');
178
+
179
+ t.is(result.success, true);
180
+ t.is(result.user.username, 'user1');
181
+ t.is(result.user.role, 'user');
182
+ t.is(result.user.active, true);
183
+ t.falsy(result.user.password_hash); // Should not return password hash
184
+ });
185
+
186
+ test('get_user returns error for non-existent user', async (t) => {
187
+ const result = await get_user('nonexistent');
188
+
189
+ t.is(result.success, false);
190
+ t.truthy(result.error.includes('User not found'));
191
+ });
192
+
193
+ // User Update Tests
194
+ test('update_user updates user fields successfully', async (t) => {
195
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
196
+ await create_user('user1', 'user123', 'user1@test.com');
197
+
198
+ const updates = {
199
+ email: 'user1_new@test.com',
200
+ role: 'admin',
201
+ active: false
202
+ };
203
+
204
+ const result = await update_user('user1', updates);
205
+
206
+ t.is(result.success, true);
207
+ t.truthy(result.message.includes('User updated successfully'));
208
+
209
+ // Verify updates
210
+ const user_result = await get_user('user1');
211
+ t.is(user_result.user.email, 'user1_new@test.com');
212
+ t.is(user_result.user.role, 'admin');
213
+ t.is(user_result.user.active, false);
214
+ });
215
+
216
+ test('update_user validates email format', async (t) => {
217
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
218
+ await create_user('user1', 'user123', 'user1@test.com');
219
+
220
+ const result = await update_user('user1', { email: 'invalid-email' });
221
+
222
+ t.is(result.success, false);
223
+ t.truthy(result.error.includes('Invalid email format'));
224
+ });
225
+
226
+ test('update_user validates role values', async (t) => {
227
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
228
+ await create_user('user1', 'user123', 'user1@test.com');
229
+
230
+ const result = await update_user('user1', { role: 'invalid' });
231
+
232
+ t.is(result.success, false);
233
+ t.truthy(result.error.includes('Role must be either "admin" or "user"'));
234
+ });
235
+
236
+ // Password Reset Tests
237
+ test('reset_user_password updates password successfully', async (t) => {
238
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
239
+ await create_user('user1', 'user123', 'user1@test.com');
240
+
241
+ const result = await reset_user_password('user1', 'new_password123');
242
+
243
+ t.is(result.success, true);
244
+ t.truthy(result.message.includes('Password reset successfully'));
245
+
246
+ // Verify new password works
247
+ const auth_result = await verify_credentials('user1', 'new_password123', '127.0.0.1');
248
+ t.is(auth_result.success, true);
249
+
250
+ // Verify old password no longer works
251
+ const old_auth_result = await verify_credentials('user1', 'user123', '127.0.0.1');
252
+ t.is(old_auth_result.success, false);
253
+ });
254
+
255
+ test('reset_user_password validates password strength', async (t) => {
256
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
257
+ await create_user('user1', 'user123', 'user1@test.com');
258
+
259
+ const result = await reset_user_password('user1', '123');
260
+
261
+ t.is(result.success, false);
262
+ t.truthy(result.error.includes('Password must be at least 6 characters long'));
263
+ });
264
+
265
+ // User Deletion Tests
266
+ test('delete_user removes user successfully', async (t) => {
267
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
268
+ await create_user('user1', 'user123', 'user1@test.com');
269
+
270
+ const result = await delete_user('user1');
271
+
272
+ t.is(result.success, true);
273
+ t.truthy(result.message.includes('User deleted successfully'));
274
+
275
+ // Verify user was deleted
276
+ const get_result = await get_user('user1');
277
+ t.is(get_result.success, false);
278
+ t.truthy(get_result.error.includes('User not found'));
279
+ });
280
+
281
+ test('delete_user returns error for non-existent user', async (t) => {
282
+ const result = await delete_user('nonexistent');
283
+
284
+ t.is(result.success, false);
285
+ t.truthy(result.error.includes('User not found'));
286
+ });
287
+
288
+ // User Listing Tests
289
+ test('list_users returns all users', async (t) => {
290
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
291
+ await create_user('user1', 'user123', 'user1@test.com');
292
+ await create_user('user2', 'user123', 'user2@test.com');
293
+
294
+ const result = await list_users();
295
+
296
+ t.is(result.success, true);
297
+ t.is(result.users.length, 3);
298
+
299
+ const usernames = result.users.map(u => u.username);
300
+ t.true(usernames.includes('admin'));
301
+ t.true(usernames.includes('user1'));
302
+ t.true(usernames.includes('user2'));
303
+
304
+ // Verify no password hashes are returned
305
+ result.users.forEach(user => {
306
+ t.falsy(user.password_hash);
307
+ });
308
+ });
309
+
310
+ test('list_users handles empty user list', async (t) => {
311
+ const result = await list_users();
312
+
313
+ t.is(result.success, true);
314
+ t.is(result.users.length, 0);
315
+ });
316
+
317
+ // Authentication Tests
318
+ test('verify_credentials authenticates valid user successfully', async (t) => {
319
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
320
+
321
+ const result = await verify_credentials('admin', 'admin123', '127.0.0.1');
322
+
323
+ t.is(result.success, true);
324
+ t.is(result.user.username, 'admin');
325
+ t.is(result.user.role, 'admin');
326
+ t.truthy(result.user.last_login);
327
+ });
328
+
329
+ test('verify_credentials rejects invalid password', async (t) => {
330
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
331
+
332
+ const result = await verify_credentials('admin', 'wrong_password', '127.0.0.1');
333
+
334
+ t.is(result.success, false);
335
+ t.truthy(result.error.includes('Invalid credentials'));
336
+ });
337
+
338
+ test('verify_credentials rejects non-existent user', async (t) => {
339
+ const result = await verify_credentials('nonexistent', 'password', '127.0.0.1');
340
+
341
+ t.is(result.success, false);
342
+ t.truthy(result.error.includes('Invalid credentials'));
343
+ });
344
+
345
+ test('verify_credentials rejects inactive user', async (t) => {
346
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
347
+ await create_user('user1', 'user123', 'user1@test.com');
348
+
349
+ // Deactivate user
350
+ await update_user('user1', { active: false });
351
+
352
+ const result = await verify_credentials('user1', 'user123', '127.0.0.1');
353
+
354
+ t.is(result.success, false);
355
+ t.truthy(result.error.includes('Account is disabled'));
356
+ });
357
+
358
+ test('verify_credentials implements rate limiting', async (t) => {
359
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
360
+
361
+ const ip = '192.168.1.100';
362
+
363
+ // Make 5 failed attempts
364
+ for (let i = 0; i < 5; i++) {
365
+ await verify_credentials('admin', 'wrong_password', ip);
366
+ }
367
+
368
+ // 6th attempt should be rate limited
369
+ const result = await verify_credentials('admin', 'wrong_password', ip);
370
+
371
+ t.is(result.success, false);
372
+ t.truthy(result.error.includes('Too many failed attempts'));
373
+ });
374
+
375
+ test('verify_credentials updates last_login on successful authentication', async (t) => {
376
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
377
+
378
+ const before_login = Date.now();
379
+
380
+ const result = await verify_credentials('admin', 'admin123', '127.0.0.1');
381
+
382
+ t.is(result.success, true);
383
+
384
+ // Get user to check last_login was updated
385
+ const user_result = await get_user('admin');
386
+ const last_login = new Date(user_result.user.last_login).getTime();
387
+
388
+ t.true(last_login >= before_login);
389
+ });
390
+
391
+ // Authentication Stats Tests
392
+ test('get_auth_stats returns authentication statistics', async (t) => {
393
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
394
+ await create_user('user1', 'user123', 'user1@test.com');
395
+
396
+ // Make some authentication attempts
397
+ await verify_credentials('admin', 'admin123', '127.0.0.1');
398
+ await verify_credentials('user1', 'wrong_password', '192.168.1.1');
399
+
400
+ const result = await get_auth_stats();
401
+
402
+ t.is(result.configured, true);
403
+ t.is(result.user_based, true);
404
+ t.is(typeof result.user_count, 'number');
405
+ t.is(typeof result.failed_attempts_count, 'number');
406
+ t.is(typeof result.rate_limited_ips, 'number');
407
+ });
408
+
409
+ // Edge Cases and Error Handling
410
+ test('functions handle storage engine errors gracefully', async (t) => {
411
+ // Mock storage engine that throws errors
412
+ const error_storage = {
413
+ get: () => { throw new Error('Storage error'); },
414
+ put: () => { throw new Error('Storage error'); },
415
+ del: () => { throw new Error('Storage error'); },
416
+ getRange: () => { throw new Error('Storage error'); }
417
+ };
418
+
419
+ set_storage_engine(error_storage);
420
+
421
+ const result = await setup_initial_admin('admin', 'admin123', 'admin@test.com');
422
+
423
+ t.is(result.success, false);
424
+ t.truthy(result.error.includes('Storage error'));
425
+ });
426
+
427
+ test('functions validate input parameters', async (t) => {
428
+ // Test with null/undefined parameters
429
+ let result = await create_user(null, 'password', 'email@test.com');
430
+ t.is(result.success, false);
431
+
432
+ result = await get_user(undefined);
433
+ t.is(result.success, false);
434
+
435
+ result = await update_user('', {});
436
+ t.is(result.success, false);
437
+ });
438
+
439
+ test('password hashing is secure and consistent', async (t) => {
440
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
441
+
442
+ // Verify password authentication works (proves hashing works)
443
+ const auth_result1 = await verify_credentials('admin', 'admin123', '127.0.0.1');
444
+ t.is(auth_result1.success, true);
445
+ t.is(auth_result1.user.username, 'admin');
446
+
447
+ // Verify wrong password fails (proves password was hashed, not stored as plaintext)
448
+ const auth_result2 = await verify_credentials('admin', 'wrong_password', '127.0.0.1');
449
+ t.is(auth_result2.success, false);
450
+
451
+ // Create another user with same password to test salt uniqueness
452
+ await create_user('user1', 'admin12345678', 'user1@test.com');
453
+ const auth_result3 = await verify_credentials('user1', 'admin12345678', '127.0.0.1');
454
+ t.is(auth_result3.success, true);
455
+ t.is(auth_result3.user.username, 'user1');
456
+ });
457
+
458
+ test('authentication timing is consistent to prevent timing attacks', async (t) => {
459
+ await setup_initial_admin('admin', 'admin123', 'admin@test.com');
460
+
461
+ // Test authentication with existing vs non-existing user
462
+ const start1 = Date.now();
463
+ await verify_credentials('admin', 'wrong_password', '127.0.0.1');
464
+ const time1 = Date.now() - start1;
465
+
466
+ const start2 = Date.now();
467
+ await verify_credentials('nonexistent', 'wrong_password', '127.0.0.1');
468
+ const time2 = Date.now() - start2;
469
+
470
+ // Times should be roughly similar (within reasonable margin)
471
+ // This is a basic check - in practice, timing attack prevention is more complex
472
+ const time_diff = Math.abs(time1 - time2);
473
+ t.true(time_diff < 1000); // Increased threshold for bcrypt operations
474
+ });
475
+
476
+ // Integration Tests
477
+ test('complete user lifecycle workflow', async (t) => {
478
+ // 1. Setup initial admin
479
+ let result = await setup_initial_admin('admin', 'admin123', 'admin@test.com');
480
+ t.is(result.success, true);
481
+
482
+ // 2. Admin authenticates
483
+ result = await verify_credentials('admin', 'admin123', '127.0.0.1');
484
+ t.is(result.success, true);
485
+ t.is(result.user.role, 'admin');
486
+
487
+ // 3. Admin creates regular user
488
+ result = await create_user('user1', 'user123', 'user1@test.com');
489
+ t.is(result.success, true);
490
+
491
+ // 4. User authenticates
492
+ result = await verify_credentials('user1', 'user123', '127.0.0.1');
493
+ t.is(result.success, true);
494
+ t.is(result.user.role, 'user');
495
+
496
+ // 5. Admin updates user
497
+ result = await update_user('user1', { email: 'user1_updated@test.com' });
498
+ t.is(result.success, true);
499
+
500
+ // 6. Admin resets user password
501
+ result = await reset_user_password('user1', 'new_password123');
502
+ t.is(result.success, true);
503
+
504
+ // 7. User authenticates with new password
505
+ result = await verify_credentials('user1', 'new_password123', '127.0.0.1');
506
+ t.is(result.success, true);
507
+
508
+ // 8. List all users
509
+ result = await list_users();
510
+ t.is(result.success, true);
511
+ t.is(result.users.length, 2);
512
+
513
+ // 9. Get user stats
514
+ result = await get_auth_stats();
515
+ t.is(result.configured, true);
516
+ t.is(result.user_count, 2);
517
+
518
+ // 10. Admin deletes user
519
+ result = await delete_user('user1');
520
+ t.is(result.success, true);
521
+
522
+ // 11. Verify user is deleted
523
+ result = await get_user('user1');
524
+ t.is(result.success, false);
525
+ });