chadstart 1.0.4 → 1.0.6
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/Dockerfile +1 -1
- package/TODO.md +739 -0
- package/admin/index.html +27 -1
- package/{chadstart.example.yml → chadstart.example.yaml} +23 -1
- package/chadstart.schema.json +83 -0
- package/cli/cli.js +124 -35
- package/core/auth.js +160 -3
- package/core/backup.js +191 -0
- package/core/config-loader.js +266 -0
- package/core/db.js +4 -0
- package/core/email.js +170 -0
- package/core/entity-engine.js +4 -0
- package/core/logs.js +179 -0
- package/core/migrations.js +23 -18
- package/core/openapi.js +6 -2
- package/core/yaml-loader.js +8 -53
- package/docs/llm-rules.md +1 -1
- package/package.json +3 -1
- package/server/express-server.js +149 -18
- package/test/backup.test.js +146 -0
- package/test/config-loader.test.js +257 -0
- package/test/email.test.js +362 -0
- package/test/logs.test.js +239 -0
- package/test/verification.test.js +439 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { buildCore } = require('../core/entity-engine');
|
|
8
|
+
const dbModule = require('../core/db');
|
|
9
|
+
const {
|
|
10
|
+
signToken, verifyToken, omitPassword, generateSecureToken,
|
|
11
|
+
} = require('../core/auth');
|
|
12
|
+
const { validateSchema } = require('../core/schema-validator');
|
|
13
|
+
const { generateOpenApiSpec } = require('../core/openapi');
|
|
14
|
+
|
|
15
|
+
// ── Helper: mock Express request/response ──────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function mockReq(headers = {}, body = {}) {
|
|
18
|
+
return { headers, body, user: undefined };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function mockRes() {
|
|
22
|
+
const r = { _status: 200, _body: undefined };
|
|
23
|
+
r.status = (s) => { r._status = s; return r; };
|
|
24
|
+
r.json = (b) => { r._body = b; };
|
|
25
|
+
return r;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── generateSecureToken ─────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
describe('generateSecureToken', () => {
|
|
31
|
+
it('generates a 64-char hex string', () => {
|
|
32
|
+
const t = generateSecureToken();
|
|
33
|
+
assert.strictEqual(typeof t, 'string');
|
|
34
|
+
assert.strictEqual(t.length, 64);
|
|
35
|
+
assert.ok(/^[a-f0-9]+$/.test(t));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('generates unique tokens', () => {
|
|
39
|
+
const a = generateSecureToken();
|
|
40
|
+
const b = generateSecureToken();
|
|
41
|
+
assert.notStrictEqual(a, b);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ── omitPassword (updated to strip internal auth fields) ────────────────
|
|
46
|
+
|
|
47
|
+
describe('omitPassword – extended', () => {
|
|
48
|
+
it('strips password field', () => {
|
|
49
|
+
assert.ok(!('password' in omitPassword({ id: '1', password: 'x', email: 'a@b.com' })));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('strips emailVerificationToken', () => {
|
|
53
|
+
assert.ok(!('emailVerificationToken' in omitPassword({ id: '1', emailVerificationToken: 'tok' })));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('strips passwordResetToken', () => {
|
|
57
|
+
assert.ok(!('passwordResetToken' in omitPassword({ id: '1', passwordResetToken: 'tok' })));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('strips passwordResetExpiry', () => {
|
|
61
|
+
assert.ok(!('passwordResetExpiry' in omitPassword({ id: '1', passwordResetExpiry: '2025-01-01' })));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('preserves emailVerified', () => {
|
|
65
|
+
const u = omitPassword({ id: '1', emailVerified: 1, password: 'x' });
|
|
66
|
+
assert.strictEqual(u.emailVerified, 1);
|
|
67
|
+
assert.ok(!('password' in u));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('preserves other fields', () => {
|
|
71
|
+
const u = omitPassword({ id: '1', email: 'a@b.com', name: 'Test', password: 'x', emailVerificationToken: 'y' });
|
|
72
|
+
assert.strictEqual(u.email, 'a@b.com');
|
|
73
|
+
assert.strictEqual(u.name, 'Test');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ── Entity engine: requireEmailVerification ─────────────────────────────
|
|
78
|
+
|
|
79
|
+
describe('entity-engine: requireEmailVerification', () => {
|
|
80
|
+
it('defaults to false', () => {
|
|
81
|
+
const core = buildCore({ name: 'App', entities: { User: { authenticable: true, properties: ['name'] } } });
|
|
82
|
+
assert.strictEqual(core.entities.User.requireEmailVerification, false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('sets to true when configured', () => {
|
|
86
|
+
const core = buildCore({
|
|
87
|
+
name: 'App',
|
|
88
|
+
entities: { User: { authenticable: true, requireEmailVerification: true, properties: ['name'] } },
|
|
89
|
+
});
|
|
90
|
+
assert.strictEqual(core.entities.User.requireEmailVerification, true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('non-authenticable entities still parse the flag (but it has no effect)', () => {
|
|
94
|
+
const core = buildCore({
|
|
95
|
+
name: 'App',
|
|
96
|
+
entities: { Post: { requireEmailVerification: true, properties: ['title'] } },
|
|
97
|
+
});
|
|
98
|
+
// The flag is stored even on non-auth entities, but only auth routes check it
|
|
99
|
+
assert.strictEqual(core.entities.Post.requireEmailVerification, true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── Schema validation: requireEmailVerification ─────────────────────────
|
|
104
|
+
|
|
105
|
+
describe('schema: requireEmailVerification', () => {
|
|
106
|
+
it('accepts entity with requireEmailVerification: true', () => {
|
|
107
|
+
assert.strictEqual(validateSchema({
|
|
108
|
+
name: 'App',
|
|
109
|
+
entities: { User: { authenticable: true, requireEmailVerification: true, properties: ['name'] } },
|
|
110
|
+
}), true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('accepts entity with requireEmailVerification: false', () => {
|
|
114
|
+
assert.strictEqual(validateSchema({
|
|
115
|
+
name: 'App',
|
|
116
|
+
entities: { User: { authenticable: true, requireEmailVerification: false, properties: ['name'] } },
|
|
117
|
+
}), true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('accepts entity without requireEmailVerification', () => {
|
|
121
|
+
assert.strictEqual(validateSchema({
|
|
122
|
+
name: 'App',
|
|
123
|
+
entities: { User: { authenticable: true, properties: ['name'] } },
|
|
124
|
+
}), true);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ── DB: authenticable columns ───────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
describe('db – verification/reset columns', () => {
|
|
131
|
+
let tmpDb;
|
|
132
|
+
const core = buildCore({
|
|
133
|
+
name: 'T',
|
|
134
|
+
entities: { User: { authenticable: true, properties: ['name'] } },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
before(async () => {
|
|
138
|
+
tmpDb = path.join(os.tmpdir(), `chadstart-verif-${Date.now()}.db`);
|
|
139
|
+
await dbModule.initDb(core, tmpDb);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
after(() => { try { fs.unlinkSync(tmpDb); } catch { /* noop */ } });
|
|
143
|
+
|
|
144
|
+
it('has emailVerified column', () => {
|
|
145
|
+
const cols = dbModule.getDb().pragma('table_info("user")').map((r) => r.name);
|
|
146
|
+
assert.ok(cols.includes('emailVerified'), 'missing emailVerified column');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('has emailVerificationToken column', () => {
|
|
150
|
+
const cols = dbModule.getDb().pragma('table_info("user")').map((r) => r.name);
|
|
151
|
+
assert.ok(cols.includes('emailVerificationToken'), 'missing emailVerificationToken column');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('has passwordResetToken column', () => {
|
|
155
|
+
const cols = dbModule.getDb().pragma('table_info("user")').map((r) => r.name);
|
|
156
|
+
assert.ok(cols.includes('passwordResetToken'), 'missing passwordResetToken column');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('has passwordResetExpiry column', () => {
|
|
160
|
+
const cols = dbModule.getDb().pragma('table_info("user")').map((r) => r.name);
|
|
161
|
+
assert.ok(cols.includes('passwordResetExpiry'), 'missing passwordResetExpiry column');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('emailVerified defaults to 0', async () => {
|
|
165
|
+
const user = await dbModule.create('user', {
|
|
166
|
+
email: 'vertest@example.com',
|
|
167
|
+
password: 'hash',
|
|
168
|
+
});
|
|
169
|
+
assert.strictEqual(user.emailVerified, 0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('can update emailVerified to 1', async () => {
|
|
173
|
+
const user = await dbModule.create('user', {
|
|
174
|
+
email: 'vertest2@example.com',
|
|
175
|
+
password: 'hash',
|
|
176
|
+
});
|
|
177
|
+
const updated = await dbModule.update('user', user.id, { emailVerified: 1 });
|
|
178
|
+
assert.strictEqual(updated.emailVerified, 1);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('can store and query verification token', async () => {
|
|
182
|
+
const token = generateSecureToken();
|
|
183
|
+
const user = await dbModule.create('user', {
|
|
184
|
+
email: 'vertest3@example.com',
|
|
185
|
+
password: 'hash',
|
|
186
|
+
emailVerificationToken: token,
|
|
187
|
+
});
|
|
188
|
+
const found = (await dbModule.findAllSimple('user', { emailVerificationToken: token }))[0];
|
|
189
|
+
assert.ok(found);
|
|
190
|
+
assert.strictEqual(found.id, user.id);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('can store and query password reset token', async () => {
|
|
194
|
+
const token = generateSecureToken();
|
|
195
|
+
const expiry = new Date(Date.now() + 3600000).toISOString();
|
|
196
|
+
const user = await dbModule.create('user', {
|
|
197
|
+
email: 'vertest4@example.com',
|
|
198
|
+
password: 'hash',
|
|
199
|
+
passwordResetToken: token,
|
|
200
|
+
passwordResetExpiry: expiry,
|
|
201
|
+
});
|
|
202
|
+
const found = (await dbModule.findAllSimple('user', { passwordResetToken: token }))[0];
|
|
203
|
+
assert.ok(found);
|
|
204
|
+
assert.strictEqual(found.id, user.id);
|
|
205
|
+
assert.ok(found.passwordResetExpiry);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('can clear tokens by setting to null', async () => {
|
|
209
|
+
const token = generateSecureToken();
|
|
210
|
+
const user = await dbModule.create('user', {
|
|
211
|
+
email: 'vertest5@example.com',
|
|
212
|
+
password: 'hash',
|
|
213
|
+
emailVerificationToken: token,
|
|
214
|
+
});
|
|
215
|
+
await dbModule.update('user', user.id, { emailVerificationToken: null });
|
|
216
|
+
const updated = await dbModule.findById('user', user.id);
|
|
217
|
+
assert.strictEqual(updated.emailVerificationToken, null);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ── OpenAPI: verification/reset endpoints ───────────────────────────────
|
|
222
|
+
|
|
223
|
+
describe('openapi: verification/reset endpoints', () => {
|
|
224
|
+
const core = buildCore({
|
|
225
|
+
name: 'App',
|
|
226
|
+
entities: { User: { authenticable: true, properties: ['name'] } },
|
|
227
|
+
});
|
|
228
|
+
const spec = generateOpenApiSpec(core);
|
|
229
|
+
|
|
230
|
+
it('includes request-verification endpoint', () => {
|
|
231
|
+
assert.ok(spec.paths['/api/auth/user/request-verification']);
|
|
232
|
+
assert.ok(spec.paths['/api/auth/user/request-verification'].post);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('includes confirm-verification endpoint', () => {
|
|
236
|
+
assert.ok(spec.paths['/api/auth/user/confirm-verification']);
|
|
237
|
+
assert.ok(spec.paths['/api/auth/user/confirm-verification'].post);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('includes request-password-reset endpoint', () => {
|
|
241
|
+
assert.ok(spec.paths['/api/auth/user/request-password-reset']);
|
|
242
|
+
assert.ok(spec.paths['/api/auth/user/request-password-reset'].post);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('includes confirm-password-reset endpoint', () => {
|
|
246
|
+
assert.ok(spec.paths['/api/auth/user/confirm-password-reset']);
|
|
247
|
+
assert.ok(spec.paths['/api/auth/user/confirm-password-reset'].post);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('login response includes 403 for email not verified', () => {
|
|
251
|
+
const loginPath = spec.paths['/api/auth/user/login'];
|
|
252
|
+
assert.ok(loginPath.post.responses['403']);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('user schema includes emailVerified', () => {
|
|
256
|
+
assert.ok(spec.components.schemas.User.properties.emailVerified);
|
|
257
|
+
assert.strictEqual(spec.components.schemas.User.properties.emailVerified.type, 'boolean');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('confirm-verification has token in request body', () => {
|
|
261
|
+
const ep = spec.paths['/api/auth/user/confirm-verification'].post;
|
|
262
|
+
assert.ok(ep.requestBody);
|
|
263
|
+
const schema = ep.requestBody.content['application/json'].schema;
|
|
264
|
+
assert.ok(schema.properties.token);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('confirm-password-reset has token and password in request body', () => {
|
|
268
|
+
const ep = spec.paths['/api/auth/user/confirm-password-reset'].post;
|
|
269
|
+
assert.ok(ep.requestBody);
|
|
270
|
+
const schema = ep.requestBody.content['application/json'].schema;
|
|
271
|
+
assert.ok(schema.properties.token);
|
|
272
|
+
assert.ok(schema.properties.password);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Admin entity should also have the endpoints
|
|
276
|
+
it('includes admin verification endpoints', () => {
|
|
277
|
+
assert.ok(spec.paths['/api/auth/admin/request-verification']);
|
|
278
|
+
assert.ok(spec.paths['/api/auth/admin/confirm-verification']);
|
|
279
|
+
assert.ok(spec.paths['/api/auth/admin/request-password-reset']);
|
|
280
|
+
assert.ok(spec.paths['/api/auth/admin/confirm-password-reset']);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ── Integration: full verification + password reset flows ───────────────
|
|
285
|
+
|
|
286
|
+
describe('auth integration – email verification flow', () => {
|
|
287
|
+
let tmpDb;
|
|
288
|
+
const bcrypt = require('bcryptjs');
|
|
289
|
+
const core = buildCore({
|
|
290
|
+
name: 'TestApp',
|
|
291
|
+
entities: { User: { authenticable: true, requireEmailVerification: true, properties: ['name'] } },
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
before(async () => {
|
|
295
|
+
tmpDb = path.join(os.tmpdir(), `chadstart-auth-flow-${Date.now()}.db`);
|
|
296
|
+
await dbModule.initDb(core, tmpDb);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
after(() => { try { fs.unlinkSync(tmpDb); } catch { /* noop */ } });
|
|
300
|
+
|
|
301
|
+
it('signup creates user with emailVerified=0 and generates token', async () => {
|
|
302
|
+
const pw = await bcrypt.hash('testpass', 10);
|
|
303
|
+
const user = await dbModule.create('user', {
|
|
304
|
+
email: 'flow1@example.com',
|
|
305
|
+
password: pw,
|
|
306
|
+
emailVerified: 0,
|
|
307
|
+
emailVerificationToken: generateSecureToken(),
|
|
308
|
+
});
|
|
309
|
+
assert.strictEqual(user.emailVerified, 0);
|
|
310
|
+
assert.ok(user.emailVerificationToken);
|
|
311
|
+
assert.strictEqual(user.emailVerificationToken.length, 64);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('confirm-verification: can verify email with valid token', async () => {
|
|
315
|
+
const token = generateSecureToken();
|
|
316
|
+
const pw = await bcrypt.hash('testpass', 10);
|
|
317
|
+
const user = await dbModule.create('user', {
|
|
318
|
+
email: 'flow2@example.com',
|
|
319
|
+
password: pw,
|
|
320
|
+
emailVerified: 0,
|
|
321
|
+
emailVerificationToken: token,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Simulate confirm-verification
|
|
325
|
+
const found = (await dbModule.findAllSimple('user', { emailVerificationToken: token }))[0];
|
|
326
|
+
assert.ok(found);
|
|
327
|
+
assert.strictEqual(found.id, user.id);
|
|
328
|
+
|
|
329
|
+
await dbModule.update('user', user.id, { emailVerified: 1, emailVerificationToken: null });
|
|
330
|
+
const updated = await dbModule.findById('user', user.id);
|
|
331
|
+
assert.strictEqual(updated.emailVerified, 1);
|
|
332
|
+
assert.strictEqual(updated.emailVerificationToken, null);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('confirm-verification: returns null for invalid token', async () => {
|
|
336
|
+
const found = (await dbModule.findAllSimple('user', { emailVerificationToken: 'nonexistent-token-12345' }))[0];
|
|
337
|
+
assert.ok(!found);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('request-verification: can regenerate token', async () => {
|
|
341
|
+
const token1 = generateSecureToken();
|
|
342
|
+
const pw = await bcrypt.hash('testpass', 10);
|
|
343
|
+
const user = await dbModule.create('user', {
|
|
344
|
+
email: 'flow3@example.com',
|
|
345
|
+
password: pw,
|
|
346
|
+
emailVerified: 0,
|
|
347
|
+
emailVerificationToken: token1,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const token2 = generateSecureToken();
|
|
351
|
+
await dbModule.update('user', user.id, { emailVerificationToken: token2 });
|
|
352
|
+
const updated = await dbModule.findById('user', user.id);
|
|
353
|
+
assert.strictEqual(updated.emailVerificationToken, token2);
|
|
354
|
+
assert.notStrictEqual(token1, token2);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('auth integration – password reset flow', () => {
|
|
359
|
+
let tmpDb;
|
|
360
|
+
const bcrypt = require('bcryptjs');
|
|
361
|
+
const core = buildCore({
|
|
362
|
+
name: 'TestApp',
|
|
363
|
+
entities: { User: { authenticable: true, properties: ['name'] } },
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
before(async () => {
|
|
367
|
+
tmpDb = path.join(os.tmpdir(), `chadstart-reset-flow-${Date.now()}.db`);
|
|
368
|
+
await dbModule.initDb(core, tmpDb);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
after(() => { try { fs.unlinkSync(tmpDb); } catch { /* noop */ } });
|
|
372
|
+
|
|
373
|
+
it('request-password-reset: creates token and expiry', async () => {
|
|
374
|
+
const pw = await bcrypt.hash('oldpass', 10);
|
|
375
|
+
const user = await dbModule.create('user', {
|
|
376
|
+
email: 'reset1@example.com',
|
|
377
|
+
password: pw,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const token = generateSecureToken();
|
|
381
|
+
const expiry = new Date(Date.now() + 3600000).toISOString();
|
|
382
|
+
await dbModule.update('user', user.id, { passwordResetToken: token, passwordResetExpiry: expiry });
|
|
383
|
+
|
|
384
|
+
const updated = await dbModule.findById('user', user.id);
|
|
385
|
+
assert.strictEqual(updated.passwordResetToken, token);
|
|
386
|
+
assert.ok(updated.passwordResetExpiry);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('confirm-password-reset: updates password and clears token', async () => {
|
|
390
|
+
const pw = await bcrypt.hash('oldpass', 10);
|
|
391
|
+
const token = generateSecureToken();
|
|
392
|
+
const expiry = new Date(Date.now() + 3600000).toISOString();
|
|
393
|
+
const user = await dbModule.create('user', {
|
|
394
|
+
email: 'reset2@example.com',
|
|
395
|
+
password: pw,
|
|
396
|
+
passwordResetToken: token,
|
|
397
|
+
passwordResetExpiry: expiry,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Simulate confirm
|
|
401
|
+
const found = (await dbModule.findAllSimple('user', { passwordResetToken: token }))[0];
|
|
402
|
+
assert.ok(found);
|
|
403
|
+
assert.ok(new Date(found.passwordResetExpiry) > new Date());
|
|
404
|
+
|
|
405
|
+
const newPw = await bcrypt.hash('newpass', 10);
|
|
406
|
+
await dbModule.update('user', user.id, {
|
|
407
|
+
password: newPw,
|
|
408
|
+
passwordResetToken: null,
|
|
409
|
+
passwordResetExpiry: null,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const updated = await dbModule.findById('user', user.id);
|
|
413
|
+
assert.strictEqual(updated.passwordResetToken, null);
|
|
414
|
+
assert.strictEqual(updated.passwordResetExpiry, null);
|
|
415
|
+
assert.ok(await bcrypt.compare('newpass', updated.password));
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('confirm-password-reset: rejects expired token', async () => {
|
|
419
|
+
const pw = await bcrypt.hash('oldpass', 10);
|
|
420
|
+
const token = generateSecureToken();
|
|
421
|
+
const expiry = new Date(Date.now() - 1000).toISOString(); // Already expired
|
|
422
|
+
const user = await dbModule.create('user', {
|
|
423
|
+
email: 'reset3@example.com',
|
|
424
|
+
password: pw,
|
|
425
|
+
passwordResetToken: token,
|
|
426
|
+
passwordResetExpiry: expiry,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const found = (await dbModule.findAllSimple('user', { passwordResetToken: token }))[0];
|
|
430
|
+
assert.ok(found);
|
|
431
|
+
// Token found but expired
|
|
432
|
+
assert.ok(new Date(found.passwordResetExpiry) < new Date());
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('confirm-password-reset: returns null for invalid token', async () => {
|
|
436
|
+
const found = (await dbModule.findAllSimple('user', { passwordResetToken: 'nonexistent-token' }))[0];
|
|
437
|
+
assert.ok(!found);
|
|
438
|
+
});
|
|
439
|
+
});
|