const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); require('../src/common'); const { buildUsersEditViewModel, buildUsersAddViewModel } = require('../src/web/routes/settings/users/form-view-model'); const rbacData = require('../src/web/lib/auth/rbac-data'); const usersFormTemplate = fs.readFileSync(require.resolve('../src/web/views/settings/users/form.hbs'), 'utf8'); test('user edit view model includes delete confirmation text', () => { const model = buildUsersEditViewModel({ id: 12, inUse: false, roleIds: [] }, 'Saved.', { id: 1 }, []); assert.equal(model.footerDeleteUrl, '/settings/users/12/delete'); assert.equal(model.footerDeleteConfirmMessage, 'Delete this user?'); assert.equal(model.footerDeleteDisabled, false); assert.equal(model.showSaveSecondaryActions, true); assert.equal(model.formAttrs, 'data-async-save data-async-save-close-url="/settings/users" data-async-save-new-url="/settings/users/new"'); }); test('user add view model keeps delete disabled without a confirm message', () => { const model = buildUsersAddViewModel('Saved.', { id: 1 }, [], { name: 'New User', username: 'newuser' }, 'primary'); assert.equal(model.footerDeleteDisabled, true); assert.equal(model.footerDeleteUrl, ''); assert.equal(model.footerDeleteConfirmMessage, undefined); assert.equal(model.formAttrs, 'data-async-save data-async-save-close-url="/settings/users" data-async-save-new-redirect="response-url" data-async-save-new-url="/settings/users/new"'); }); test('user add view model uses success messaging for redirected saves', () => { const model = buildUsersAddViewModel('User created.', { id: 1 }, [], { name: 'New User', username: 'newuser' }); assert.equal(model.messageVariant, 'success'); }); test('user form keeps username editable in the admin section', () => { assert.ok(usersFormTemplate.includes('name="username"')); assert.ok(!usersFormTemplate.includes('{{#if isEdit}}disabled{{else}}required{{/if}}')); assert.ok(usersFormTemplate.includes('required />')); assert.match(usersFormTemplate, /class="btn-group ms-auto" role="group" aria-label=/); }); test('user edit view model carries Better Auth sign-in methods without provider account ids', async () => { const pool = { async query(sql) { if (String(sql).includes('FROM a_users u')) { return [[{ id: 12, name: 'User', username: 'user', email: 'user@example.test', email_verified: 1, two_factor_enabled: 0, role_ids_csv: '' }]]; } if (String(sql).includes('FROM a_accounts')) { return [[{ provider_id: 'credential', account_id: '12' }, { provider_id: 'google', account_id: 'google-subject' }]]; } if (String(sql).includes('FROM a_user_passkeys')) return [[{ id: 'passkey-1', name: 'Work laptop', created_at: new Date() }]]; return [[]]; } }; const user = await rbacData.fetchUserWithRoles(pool, 12); const model = buildUsersEditViewModel(user, '', { id: 1 }, []); assert.deepEqual(model.authMethods, [ { providerId: 'credential', label: 'Password', isCredential: true }, { providerId: 'google', label: 'Google', isCredential: false } ]); assert.equal(model.user.twoFactorEnabled, false); assert.deepEqual(model.passkeys, [{ id: 'passkey-1', name: 'Work laptop', created_at: model.passkeys[0].created_at }]); assert.equal(model.user.canUnlinkPasskeys, true); assert.doesNotMatch(JSON.stringify(model.authMethods), /google-subject/); assert.ok(usersFormTemplate.includes('Sign-in methods')); assert.ok(usersFormTemplate.includes('{{#if isEdit}}')); assert.doesNotMatch(usersFormTemplate, /Linked SSO or OAuth accounts will appear here when configured\./); assert.ok(usersFormTemplate.includes('user.passkeys')); assert.ok(usersFormTemplate.includes('data-admin-passkey-group')); assert.ok(usersFormTemplate.includes('data-admin-passkey-list')); assert.ok(usersFormTemplate.includes('bi-arrow-return-right')); assert.ok(usersFormTemplate.includes('/passkeys/{{id}}/unlink')); assert.ok(usersFormTemplate.includes('list-group-item d-flex align-items-start gap-3 px-0')); }); test('password-only users still see the available two-factor option', () => { const model = buildUsersEditViewModel({ id: 12, inUse: false, roleIds: [], twoFactorEnabled: false, authMethods: [{ providerId: 'credential', label: 'Password' }] }, '', { id: 1 }, []); assert.equal(model.user.twoFactorEnabled, false); assert.ok(usersFormTemplate.includes('Two-step verification')); assert.ok(usersFormTemplate.includes('Not enabled')); }); test('admin edit shows two-factor status as an additional sign-in method', async () => { const pool = { async query(sql) { if (String(sql).includes('FROM a_users u')) { return [[{ id: 13, name: 'User', username: 'user', email: 'user@example.test', email_verified: 1, two_factor_enabled: 1, role_ids_csv: '' }]]; } if (String(sql).includes('FROM a_accounts')) return [[{ provider_id: 'credential', account_id: '13' }]]; return [[]]; } }; const user = await rbacData.fetchUserWithRoles(pool, 13); const model = buildUsersEditViewModel(user, '', { id: 1 }, []); assert.equal(model.user.twoFactorEnabled, true); assert.ok(usersFormTemplate.includes('Enabled')); }); test('user edit view model withholds passkey unlink from the account owner', () => { const user = { id: 12, passkeys: [{ id: 'passkey-1', name: 'Work laptop' }] }; const model = buildUsersEditViewModel(user, '', { id: 12 }, []); assert.equal(model.user.canUnlinkPasskeys, false); assert.equal(model.passkeys.length, 1); });