284 lines
11 KiB
JavaScript
284 lines
11 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
require('../src/common');
|
|
|
|
const registerSettingsRoutes = require('../src/web/routes/settings/routes');
|
|
const { ANNOUNCEMENT_ICON_CATALOG } = require('../src/data/announcement-icons');
|
|
|
|
for (const failReset of [false, true]) {
|
|
test('global login lockout reset ' + (failReset ? 'forwards failures' : 'uses its own permission and audits the action'), async () => {
|
|
const handlers = {};
|
|
const permissions = [];
|
|
const queries = [];
|
|
const resetError = new Error('Reset failed');
|
|
let auditEvent;
|
|
let forwardedError;
|
|
registerSettingsRoutes({
|
|
get() {},
|
|
post(path, ...routeHandlers) { handlers[path] = routeHandlers; }
|
|
}, {
|
|
pages: {},
|
|
pool: {
|
|
async query(sql, params) {
|
|
queries.push({ sql, params });
|
|
if (failReset) throw resetError;
|
|
return [{ affectedRows: 3 }];
|
|
}
|
|
},
|
|
requirePermission(permission) {
|
|
const middleware = (_req, _res, next) => next();
|
|
permissions.push({ permission, middleware });
|
|
return middleware;
|
|
},
|
|
recordRequestAuditEvent: async (_pool, _req, event) => { auditEvent = event; }
|
|
});
|
|
const route = handlers['/settings/system/login-lockouts/reset'];
|
|
assert.equal(permissions.find(entry => entry.middleware === route[0]).permission, 'login-lockouts.allow');
|
|
const response = { redirect(url) { this.redirectedTo = url; } };
|
|
await route[1]({ currentUser: { id: 3 } }, response, error => { forwardedError = error; });
|
|
assert.deepEqual(queries, [{
|
|
sql: 'DELETE FROM a_login_attempts WHERE rate_key LIKE ? OR rate_key LIKE ?',
|
|
params: ['%|/sign-in/%', '%|/two-factor/verify-%']
|
|
}]);
|
|
if (failReset) {
|
|
assert.equal(forwardedError, resetError);
|
|
assert.equal(auditEvent, undefined);
|
|
assert.equal(response.redirectedTo, undefined);
|
|
} else {
|
|
assert.equal(auditEvent.eventType, 'system-settings.login_lockouts_reset');
|
|
assert.equal(auditEvent.actorUserId, 3);
|
|
assert.equal(response.redirectedTo, '/settings/system?message=Sign-in%20and%20two-factor%20request%20rate%20limits%20reset%20for%20all%20clients.#security-sessions');
|
|
}
|
|
});
|
|
}
|
|
|
|
test('login lockout reset permission is separate from settings update in the UI', () => {
|
|
const { renderView } = require('../src/web/view');
|
|
const { PERMISSIONS } = require('../src/rbac');
|
|
assert.ok(PERMISSIONS.some(permission => permission.key === 'login-lockouts.allow'));
|
|
for (const permissionKeys of [['system-settings.read'], ['system-settings.read', 'system-settings.update'], ['system-settings.read', 'login-lockouts.allow']]) {
|
|
const html = renderView('settings/system/index', { currentUser: { id: 3, permissionKeys }, mediaSettings: {} });
|
|
const canReset = permissionKeys.includes('login-lockouts.allow');
|
|
assert.equal(html.includes('id="reset-login-lockouts-form"'), canReset);
|
|
assert.equal(html.includes('form="reset-login-lockouts-form"'), canReset);
|
|
if (canReset) assert.match(html, /data-confirm-message="Clear sign-in and two-factor request rate limits for all clients\?/);
|
|
assert.equal(html.includes('name="session_lifetime_days"'), false);
|
|
assert.equal(html.includes('name="max_active_sessions"'), false);
|
|
assert.equal(html.includes('name="login_max_attempts"'), false);
|
|
assert.equal(html.includes('name="allow_admin_email_verification_bypass"'), true);
|
|
}
|
|
});
|
|
|
|
test('settings route renders the overview', async () => {
|
|
const handlers = {};
|
|
const queries = [];
|
|
const app = {
|
|
get(path, ...routeHandlers) {
|
|
handlers[path] = routeHandlers;
|
|
},
|
|
post(path, ...routeHandlers) {
|
|
handlers['POST ' + path] = routeHandlers;
|
|
}
|
|
};
|
|
|
|
registerSettingsRoutes(app, {
|
|
pages: {
|
|
renderSettingsPage(_settings, _icons, currentUser) {
|
|
return currentUser && currentUser.id === 7 ? 'settings-page' : 'unexpected';
|
|
}
|
|
},
|
|
pool: {
|
|
async query(sql, params) {
|
|
queries.push({ sql, params });
|
|
return [[]];
|
|
}
|
|
},
|
|
requirePermission(permissionKey) {
|
|
assert.ok(['system-settings.read', 'system-settings.update', 'login-lockouts.allow'].includes(permissionKey));
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
}
|
|
});
|
|
|
|
const pageResponse = {
|
|
send(value) {
|
|
this.body = value;
|
|
}
|
|
};
|
|
await handlers['/settings/system'][1]({ currentUser: { id: 7 }, query: {} }, pageResponse, function (error) {
|
|
throw error;
|
|
});
|
|
assert.equal(pageResponse.body, 'settings-page');
|
|
|
|
assert.equal(Object.keys(handlers).length, 3);
|
|
assert.ok(handlers['/settings/system']);
|
|
assert.ok(handlers['POST /settings/system']);
|
|
|
|
const saveResponse = {
|
|
redirect(url) {
|
|
this.redirectedTo = url;
|
|
}
|
|
};
|
|
await handlers['POST /settings/system'][1]({
|
|
body: { 'suggested_icons[]': ['bell-fill'], image_max_mb: '100', video_max_mb: '1024' },
|
|
body: { settings_section: 'icons', 'suggested_icons[]': ['bell-fill'] },
|
|
currentUser: { id: 7 }
|
|
}, saveResponse, function (error) {
|
|
throw error;
|
|
});
|
|
assert.ok(queries.some(function (entry) { return entry.sql && /DELETE FROM o_app_settings/.test(entry.sql); }));
|
|
assert.ok(queries.some(function (entry) { return entry.sql && /INSERT INTO o_app_settings/.test(entry.sql); }));
|
|
queries.filter(function (entry) {
|
|
return entry.sql && /DELETE FROM o_app_settings/.test(entry.sql);
|
|
}).forEach(function (entry) {
|
|
assert.equal((entry.sql.match(/\?/g) || []).length, entry.params.length);
|
|
assert.equal(entry.params.includes('email.invitation_body'), false);
|
|
});
|
|
assert.match(saveResponse.redirectedTo, /^\/settings\/system\?message=/);
|
|
|
|
let tooManyError = null;
|
|
await handlers['POST /settings/system'][1]({
|
|
body: { settings_section: 'icons', suggested_icons: ANNOUNCEMENT_ICON_CATALOG.slice(0, 49).map(function (option) { return option.value; }) },
|
|
currentUser: { id: 7 }
|
|
}, {}, function (error) {
|
|
tooManyError = error;
|
|
});
|
|
assert.equal(tooManyError.statusCode, 400);
|
|
assert.match(tooManyError.message, /48/);
|
|
|
|
const mediaResponse = {
|
|
redirect(url) {
|
|
this.redirectedTo = url;
|
|
}
|
|
};
|
|
await handlers['POST /settings/system'][1]({
|
|
body: { settings_section: 'media', image_max_mb: '100', video_max_mb: '1024', wysiwyg_image_max_mb: '0.5', allowed_mime_types: ['image/png', 'video/mp4'] },
|
|
currentUser: { id: 7 }
|
|
}, mediaResponse, function (error) {
|
|
throw error;
|
|
});
|
|
assert.match(mediaResponse.redirectedTo, /media-uploads/);
|
|
|
|
const securityResponse = {
|
|
redirect(url) {
|
|
this.redirectedTo = url;
|
|
}
|
|
};
|
|
await handlers['POST /settings/system'][1]({
|
|
body: { settings_section: 'security', allow_user_session_revocation: '1', password_min_length: '12', password_min_categories: '3', allow_admin_email_verification_bypass: '1' },
|
|
currentUser: { id: 7 }
|
|
}, securityResponse, function (error) {
|
|
throw error;
|
|
});
|
|
assert.match(securityResponse.redirectedTo, /security-sessions/);
|
|
assert.ok(queries.some(function (entry) {
|
|
return entry.sql && entry.sql.includes('INSERT INTO o_app_settings') && entry.params && entry.params[0] === 'security.password_min_length';
|
|
}));
|
|
assert.ok(queries.some(function (entry) {
|
|
return entry.sql && entry.sql.includes('INSERT INTO o_app_settings') && entry.params && entry.params[0] === 'security.allow_admin_email_verification_bypass' && entry.params[1] === 'true';
|
|
}));
|
|
assert.equal(queries.some(function (entry) {
|
|
return entry.sql && entry.sql.includes('INSERT INTO o_app_settings') && entry.params && ['security.session_lifetime_days', 'security.max_active_sessions', 'security.login_max_attempts', 'security.login_lockout_minutes', 'security.login_rate_limit_scope'].includes(entry.params[0]);
|
|
}), false);
|
|
|
|
const mediaInsertQueries = queries.filter(function (entry) {
|
|
return entry.sql && entry.sql.includes('INSERT INTO o_app_settings');
|
|
});
|
|
const videoInsert = mediaInsertQueries.find(function (entry) {
|
|
return entry.params && entry.params[0] === 'uploads.video_max_bytes';
|
|
});
|
|
assert.equal(Number(videoInsert.params[1]), 1024 * 1024 * 1024);
|
|
});
|
|
|
|
test('email settings can send a test email to the logged-in user', async () => {
|
|
const handlers = {};
|
|
const sentMessages = [];
|
|
const app = {
|
|
get(path, ...routeHandlers) {
|
|
handlers[path] = routeHandlers;
|
|
},
|
|
post(path, ...routeHandlers) {
|
|
handlers['POST ' + path] = routeHandlers;
|
|
}
|
|
};
|
|
|
|
registerSettingsRoutes(app, {
|
|
pages: {},
|
|
pool: {
|
|
async query(sql) {
|
|
if (/SELECT id, setting_key, setting_value FROM o_app_settings/.test(sql)) {
|
|
return [[
|
|
{ setting_key: 'email.smtp_enabled', setting_value: 'true' },
|
|
{ setting_key: 'email.smtp_host', setting_value: 'smtp.example.test' },
|
|
{ setting_key: 'email.from_address', setting_value: 'pulse@example.test' }
|
|
]];
|
|
}
|
|
throw new Error('Unexpected settings write during test email action.');
|
|
}
|
|
},
|
|
requirePermission() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
},
|
|
sendAccountEmail(settings, message) {
|
|
sentMessages.push({ settings, message });
|
|
}
|
|
});
|
|
|
|
const response = {
|
|
redirect(url) {
|
|
this.redirectedTo = url;
|
|
}
|
|
};
|
|
await handlers['POST /settings/system'][1]({
|
|
body: { settings_section: 'email', settings_action: 'test_email' },
|
|
currentUser: { id: 7, email: 'admin@example.test' }
|
|
}, response, function (error) {
|
|
throw error;
|
|
});
|
|
|
|
assert.equal(sentMessages.length, 1);
|
|
assert.equal(sentMessages[0].message.to, 'admin@example.test');
|
|
assert.equal(sentMessages[0].message.subject, 'Pulse Signage SMTP test email');
|
|
assert.match(response.redirectedTo, /Test%20email%20sent%20to%20admin%40example\.test/);
|
|
});
|
|
|
|
test('email settings save one shared OTP expiry value', async () => {
|
|
const handlers = {};
|
|
const queries = [];
|
|
const app = {
|
|
get(path, ...routeHandlers) { handlers[path] = routeHandlers; },
|
|
post(path, ...routeHandlers) { handlers['POST ' + path] = routeHandlers; }
|
|
};
|
|
registerSettingsRoutes(app, {
|
|
pages: {},
|
|
pool: {
|
|
async query(sql, params) {
|
|
queries.push([String(sql), params]);
|
|
if (String(sql).includes('SELECT id, setting_key, setting_value FROM o_app_settings')) return [[]];
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
},
|
|
requirePermission() { return function (_req, _res, next) { next(); }; }
|
|
});
|
|
|
|
const response = { redirect(url) { this.redirectedTo = url; } };
|
|
await handlers['POST /settings/system'][1]({
|
|
body: {
|
|
settings_section: 'email',
|
|
smtp_enabled: '0',
|
|
smtp_port: '587',
|
|
smtp_security: 'starttls',
|
|
otp_expiry_minutes: '12',
|
|
},
|
|
currentUser: { id: 7 }
|
|
}, response, (error) => { throw error; });
|
|
|
|
assert.ok(queries.some(([sql, params]) => sql.includes('INSERT INTO o_app_settings') && params[0] === 'email.otp_expiry_minutes' && params[1] === '12'));
|
|
assert.ok(!queries.some(([, params]) => Array.isArray(params) && ['email.verification_expiry_minutes', 'email.reset_expiry_minutes'].includes(params[0])));
|
|
assert.match(response.redirectedTo, /#email-delivery$/);
|
|
});
|