Verified<\/span>/);
assert.doesNotMatch(verifiedHtml, /badge|Send code|Resend code/);
const unverifiedHtml = render({ username: 'demo', name: 'Demo', email: 'demo@example.test', email_verified: false });
assert.match(unverifiedHtml, /[\s\S]*name="email"[\s\S]*form="account-email-verification-form">Send code/);
assert.match(unverifiedHtml, /action="\/account\/email\/send-verification"/);
const pendingHtml = render({ username: 'demo', name: 'Demo', email: 'demo@example.test', pending_email: 'new@example.test' });
assert.match(pendingHtml, /Change pending/);
assert.match(pendingHtml, /form="account-email-verification-form">Resend code/);
assert.match(pendingHtml, /action="\/account\/email"/);
assert.match(pendingHtml, /name="otp"/);
assert.doesNotMatch(pendingHtml, /Cancel change|Cancel verification|\/account\/email\/cancel/);
});
test('account page clears pending_email when its Better Auth OTP is missing or expired', async () => {
const handlers = {};
const queries = [];
let renderedUser = null;
const app = {
get(path, ...routeHandlers) { handlers[path] = routeHandlers; },
post() {}
};
const pool = {
async query(sql, params) {
const text = String(sql);
queries.push([text, params]);
if (text.includes('SELECT id, setting_key, setting_value FROM o_app_settings')) {
return [[{ id: 1, setting_key: 'security.allow_user_session_revocation', setting_value: 'false' }]];
}
if (text.includes('UPDATE a_users') && text.includes('NOT EXISTS')) {
return [{ affectedRows: 1 }];
}
return [[]];
}
};
registerAccountRoutes(app, {
pool: pool,
common: {},
pages: {
renderAccountPage(user) { renderedUser = Object.assign({}, user); return 'account'; }
}
});
const req = {
currentUser: { id: 7, username: 'demo', email: 'old@example.test', pending_email: 'new@example.test' },
query: {}
};
const res = { send(value) { this.body = value; }, status() { return this; } };
await handlers['/account'][0](req, res);
assert.ok(queries.some(([sql, params]) => sql.includes('DELETE FROM a_verifications WHERE identifier = ? AND expires_at <= NOW()') && params[0] === 'change-email-otp-old@example.test-new@example.test'));
assert.ok(queries.some(([sql, params]) => sql.includes('UPDATE a_users') && sql.includes('NOT EXISTS') && params[1] === 'new@example.test'));
assert.equal(renderedUser.pending_email, null);
assert.equal(res.body, 'account');
});
test('account page preserves pending_email while a Better Auth OTP is still active', async () => {
const handlers = {};
let renderedUser = null;
const app = {
get(path, ...routeHandlers) { handlers[path] = routeHandlers; },
post() {}
};
const pool = {
async query(sql) {
if (String(sql).includes('SELECT id, setting_key, setting_value FROM o_app_settings')) {
return [[{ id: 1, setting_key: 'security.allow_user_session_revocation', setting_value: 'false' }]];
}
if (String(sql).includes('UPDATE a_users') && String(sql).includes('NOT EXISTS')) {
return [{ affectedRows: 0 }];
}
return [[]];
}
};
registerAccountRoutes(app, {
pool: pool,
common: {},
pages: {
renderAccountPage(user) { renderedUser = Object.assign({}, user); return 'account'; }
}
});
const pendingEmail = 'new@example.test';
const req = {
currentUser: { id: 7, username: 'demo', email: 'old@example.test', pending_email: pendingEmail },
query: {}
};
const res = { send(value) { this.body = value; }, status() { return this; } };
await handlers['/account'][0](req, res);
assert.equal(renderedUser.pending_email, pendingEmail);
});
test('required-password route renders the password-only page and redirects after the requirement clears', async () => {
const handlers = {};
const app = {
get(path, ...routeHandlers) { handlers[path] = routeHandlers; },
post() {}
};
const deps = {
pool: { async query() { return [[]]; } },
common: {},
pages: {
renderRequiredPasswordPage(user, requirements) {
return user.username + ':' + requirements.minimumLength;
}
},
formatDashboardDate: () => '',
getAuditUserId: () => 5,
verifyPassword: () => true,
validatePasswordStrength: validatePasswordStrength,
hashPassword: () => ({ hash: 'hash', salt: 'salt', iterations: 1 }),
createUserSession: async () => 'token',
setSessionCookie: () => {}
};
registerAccountRoutes(app, deps);
const forcedResponse = {
send(value) { this.body = value; },
redirect(url) { this.redirectedTo = url; }
};
await handlers['/account/password'][0]({ currentUser: { id: 5, username: 'demo', mustChangePassword: true } }, forcedResponse, (error) => { throw error; });
const regularResponse = {
send(value) { this.body = value; },
redirect(url) { this.redirectedTo = url; }
};
await handlers['/account/password'][0]({ currentUser: { id: 5, username: 'demo', mustChangePassword: false } }, regularResponse, (error) => { throw error; });
assert.equal(forcedResponse.body, 'demo:10');
assert.equal(regularResponse.redirectedTo, '/account');
});
test('account password update delegates to Better Auth and signs out', async () => {
const handlers = {};
let changePasswordRequest = null;
const app = {
post(path, ...routeHandlers) {
handlers[path] = routeHandlers;
},
get() {}
};
const deps = {
pool: {
async query(sql) {
if (sql.includes('UPDATE a_users SET must_change_password = 0')) {
return [{ affectedRows: 1 }];
}
if (sql.includes('DELETE FROM a_sessions WHERE user_id = ?')) {
return [{ affectedRows: 1 }];
}
return [[]];
}
},
common: {},
pages: {},
betterAuth: {
api: {
async changePassword(request) { changePasswordRequest = request; }
}
},
fromNodeHeaders(headers) { return headers; },
formatDashboardDate: () => '',
getAuditUserId: () => 9,
validatePasswordStrength: validatePasswordStrength,
clearSessionCookie() {}
};
registerAccountRoutes(app, deps);
const routeHandlers = handlers['/account/password'];
assert.equal(Array.isArray(routeHandlers), true);
const req = {
body: {
current_password: 'current-password',
new_password: 'NewPassword!1',
confirm_password: 'NewPassword!1'
},
headers: { cookie: 'digital_signage_session=signed-token' },
currentUser: { id: 5 }
};
const res = {
redirect(url) {
this.redirectedTo = url;
},
status() {
return this;
},
send() {
return this;
}
};
await routeHandlers[0](req, res, () => {});
assert.deepEqual(changePasswordRequest.body, {
currentPassword: 'current-password',
newPassword: 'NewPassword!1',
revokeOtherSessions: true
});
assert.deepEqual(changePasswordRequest.headers, req.headers);
assert.equal(res.redirectedTo, '/login?message=Password%20updated.%20Sign%20in%20again.');
});
test('account profile verifies the current password through Better Auth', async () => {
const handlers = {};
let verificationRequest = null;
const app = {
post(path, ...routeHandlers) { handlers[path] = routeHandlers; },
get() {}
};
registerAccountRoutes(app, {
pool: {
async query(sql) {
if (sql.includes('SELECT id, username, name, email, pending_email FROM a_users')) {
return [[{ id: 5, username: 'demo', name: 'Demo', email: 'demo@example.test', pending_email: null }]];
}
return [{ affectedRows: 1 }];
}
},
common: {
fetchDuplicateName: async () => null,
validateMaxLength(value) { return String(value || '').trim(); }
},
pages: {},
betterAuth: {
api: {
async verifyPassword(request) {
verificationRequest = request;
return { status: true };
}
}
},
fromNodeHeaders(headers) { return { cookie: headers.cookie }; },
formatDashboardDate: () => '',
getAuditUserId: () => 5
});
const req = {
body: { name: 'Demo', username: 'demo', email: 'demo@example.test', current_password: 'current-password' },
headers: { cookie: 'auth-session=session-token' },
currentUser: { id: 5 }
};
const res = { redirect(url) { this.redirectedTo = url; } };
await handlers['/account/profile'][0](req, res, error => { throw error; });
assert.deepEqual(verificationRequest, {
body: { password: 'current-password' },
headers: { cookie: 'auth-session=session-token' }
});
assert.equal(res.redirectedTo, '/account?message=Account%20details%20updated.');
});