apostrophe
Version:
The Apostrophe Content Management System.
545 lines (493 loc) • 15.1 kB
JavaScript
const t = require('../test-lib/test.js');
const assert = require('assert/strict');
describe('user settings', function () {
this.timeout(t.timeout);
let apos;
beforeEach(async function() {
await t.destroy(apos);
apos = null;
});
after(async function() {
return t.destroy(apos);
});
it('should have settings module', async function() {
apos = await t.create({
root: module
});
assert(apos.modules['@apostrophecms/settings'], 'module not found');
assert(apos.settings, 'module alias not found');
assert.deepEqual(apos.settings.options.subforms, {});
assert.deepEqual(apos.settings.options.groups, {});
assert.deepEqual(apos.settings.subforms, []);
assert.deepEqual(apos.settings.userSchema, []);
assert.equal(apos.settings.hasSchema(), false);
});
it('should panic on non-existing or forbidden user field in subforms', async function () {
apos = await t.create({
root: module
});
apos.settings.options.subforms = {
test: {
fields: [ 'nonExistingField' ]
}
};
assert.throws(
apos.settings.initSubforms,
{
message: '[@apostrophecms/settings] The field "nonExistingField" is not a valid user field.'
}
);
const testFields = [ ...apos.settings.systemForbiddenFields ];
for (const field of testFields) {
apos.settings.options.subforms = {
test: {
fields: [ field ]
}
};
assert.throws(
apos.settings.initSubforms,
{
message: `[@apostrophecms/settings] The field "${field}" is forbidden.`
}
);
}
});
// It should process settings schema and register the subset of the user
// schema that is relevant to the configured subforms.
it('should init subforms', async function () {
apos = await t.create({
root: module,
modules: {
'@apostrophecms/i18n': {
options: {
adminLocales: [
{
label: 'English',
value: 'en'
},
{
label: 'French',
value: 'fr'
}
]
}
},
'@apostrophecms/user': {
fields: {
add: {
firstName: {
type: 'string',
label: 'First Name'
},
lastName: {
type: 'string',
label: 'Last Name'
}
}
}
},
'@apostrophecms/settings': {
options: {
subforms: {
name: {
label: 'Name',
fields: [ 'firstName', 'lastName' ],
preview: '{{ firstName }} {{ lastName }}'
},
adminLocale: {
fields: [ 'adminLocale' ]
}
}
// No groups configured
}
}
}
});
// Schema is processed
const schema = apos.settings.userSchema;
assert.equal(apos.settings.hasSchema(), true);
assert.equal(schema.length, 3);
assert(schema.some(field => field.name === 'firstName'));
assert(schema.some(field => field.name === 'lastName'));
assert(schema.some(field => field.name === 'adminLocale'));
const nameSchema = apos.settings.getSubformSchema('name');
assert.equal(nameSchema.length, 2);
assert(nameSchema.some(field => field.name === 'firstName'));
assert(nameSchema.some(field => field.name === 'lastName'));
const adminLocaleSchema = apos.settings.getSubformSchema('adminLocale');
assert.equal(adminLocaleSchema.length, 1);
assert(adminLocaleSchema.some(field => field.name === 'adminLocale'));
// Subforms are initialized
const subforms = apos.settings.subforms;
assert.equal(subforms.length, 2);
const nameSubform = subforms.find(subform => subform.name === 'name');
assert.equal(nameSubform.label, 'Name');
assert.equal(nameSubform.preview, '{{ firstName }} {{ lastName }}');
assert.deepEqual(nameSubform.schema, nameSchema);
assert.deepEqual(nameSubform.group, {
name: 'ungrouped',
label: 'apostrophe:ungrouped'
});
const adminLocaleSubform = subforms.find(subform => subform.name === 'adminLocale');
assert.equal(adminLocaleSubform.label, undefined);
assert.equal(adminLocaleSubform.preview, undefined);
assert.deepEqual(adminLocaleSubform.schema, adminLocaleSchema);
assert.deepEqual(adminLocaleSubform.group, {
name: 'ungrouped',
label: 'apostrophe:ungrouped'
});
// Appropriate browser data is sent
const browserData = apos.settings.getBrowserData(apos.task.getReq({
session: {}
}));
assert.deepEqual(browserData.subforms, subforms);
assert.equal(browserData.action, '/api/v1/@apostrophecms/settings');
});
it('should init groups', async function () {
apos = await createCommonInstance();
const [ first, second, third, fourth ] = apos.settings.subforms;
assert.equal(apos.settings.subforms.length, 4);
assert.equal(first.name, 'name');
assert.deepEqual(first.group, {
name: 'account',
label: 'Account'
});
assert.equal(second.name, 'password');
assert.deepEqual(second.group, {
name: 'account',
label: 'Account'
});
assert.equal(third.name, 'adminLocale');
assert.deepEqual(third.group, {
name: 'preferences',
label: 'Preferences'
});
assert.equal(fourth.name, 'display');
assert.deepEqual(fourth.group, {
name: 'ungrouped',
label: 'apostrophe:ungrouped'
});
});
it('should handle protected subforms', async function () {
apos = await createCommonInstance();
const [ first, second, third, fourth ] = apos.settings.subforms;
assert.equal(apos.settings.subforms.length, 4);
assert.equal(first.name, 'name');
assert.equal(first.protection, 'password');
// verify the explicitly set by the config private flag is removed
assert.equal(typeof first._passwordChangeForm, 'undefined');
assert.deepEqual(first.fields, [ 'firstName', 'lastName' ]);
assert.equal(first.schema.length, 3);
// last field is the current password field
{
const pwdField = first.schema[first.schema.length - 1];
assert.equal(pwdField.type, 'password');
assert.equal(pwdField.name, 'passwordCurrent');
assert.equal(pwdField.required, true);
}
assert.equal(second.name, 'password');
assert.equal(second.protection, 'password');
assert.equal(second._passwordChangeForm, true);
// displayName is removed
assert.deepEqual(second.fields, [ 'password' ]);
assert(second.help);
assert.equal(second.schema[0].name, 'password');
assert.equal(second.schema[0].type, 'password');
assert.equal(second.schema[1].name, 'passwordRepeat');
assert.equal(second.schema[1].type, 'password');
assert.equal(second.schema[2].name, 'passwordCurrent');
assert.equal(second.schema[2].type, 'password');
assert.equal(third.name, 'adminLocale');
assert.equal(!!third.protection, false);
assert.equal(third.schema.length, 1);
assert.equal(fourth.name, 'display');
assert.equal(fourth.protection, 'password');
assert.deepEqual(fourth.fields, [ 'displayName' ]);
assert.equal(fourth.schema.length, 2);
// last field is the current password field
{
const pwdField = fourth.schema[fourth.schema.length - 1];
assert.equal(pwdField.type, 'password');
assert.equal(pwdField.name, 'passwordCurrent');
assert.equal(pwdField.required, true);
}
});
it('should return 404 when settings user data and no configuration', async function () {
apos = await t.create({
root: module
});
const { jar } = await login(apos);
await assert.rejects(
apos.http.get('/api/v1/@apostrophecms/settings', { jar }),
{
status: 404
}
);
});
it('should load settings user data', async function () {
apos = await createCommonInstance();
const { jar, user } = await login(apos);
const result = await apos.http.get('/api/v1/@apostrophecms/settings', {
jar
});
assert(result._id);
assert.deepEqual(result, {
_id: user._id,
adminLocale: '',
displayName: '',
firstName: '',
lastName: ''
});
});
it('should validate when updating settings user data and no configuration', async function () {
apos = await t.create({
root: module
});
{
const { jar } = await login(apos);
await assert.rejects(
apos.http.patch('/api/v1/@apostrophecms/settings/name', {
jar
}),
{
status: 404
}
);
await t.destroy(apos);
}
apos = await createCommonInstance();
{
const { jar } = await login(apos);
await assert.rejects(
apos.http.patch('/api/v1/@apostrophecms/settings/non-existing', {
jar
}),
{
status: 404
}
);
}
});
it('should update settings', async function () {
apos = await createCommonInstance();
const { jar, user } = await login(apos);
const result = await apos.http.patch('/api/v1/@apostrophecms/settings/adminLocale', {
body: {
adminLocale: 'fr'
},
jar
});
assert(result._id);
assert.deepEqual(result, {
_id: user._id,
adminLocale: 'fr'
});
const _user = await apos.user.find(apos.task.getReq(), { _id: user._id }).toObject();
assert.equal(_user.adminLocale, 'fr');
});
it('should change password', async function () {
apos = await createCommonInstance();
const { jar } = await login(apos);
// Passwords do not match
await assert.rejects(
apos.http.patch('/api/v1/@apostrophecms/settings/password', {
body: {
password: 'newpassword',
passwordRepeat: 'doesNotMatch',
passwordCurrent: 'invalid'
},
jar
}),
function (err) {
assert.equal(err.status, 400);
assert.equal(err.body.data.errors[0].path, 'passwordRepeat');
assert.equal(err.body.data.errors[0].message, 'invalid');
return true;
}
);
// Current password is invalid
await assert.rejects(
apos.http.patch('/api/v1/@apostrophecms/settings/password', {
body: {
password: 'newpassword',
passwordRepeat: 'newpassword',
passwordCurrent: 'invalid'
},
jar
}),
function (err) {
assert.equal(err.status, 403);
assert.equal(err.body.data.path, 'passwordCurrent');
assert.equal(err.body.name, 'forbidden');
return true;
}
);
await apos.http.patch('/api/v1/@apostrophecms/settings/password', {
body: {
password: 'newpassword',
passwordRepeat: 'newpassword',
passwordCurrent: 'editor'
},
jar
});
await assert.rejects(t.loginAs(apos, 'editor', 'editor'));
await logout(apos, 'editor', 'editor', jar);
await t.loginAs(apos, 'editor', 'newpassword');
});
it('should password protect subforms', async function () {
apos = await createCommonInstance();
const { jar, user } = await login(apos);
assert(!user.displayName);
// No current password
await assert.rejects(
apos.http.patch('/api/v1/@apostrophecms/settings/display', {
body: {
displayName: 'Hacker'
},
jar
}),
function (err) {
assert.equal(err.status, 403);
assert.equal(err.body.data.path, 'passwordCurrent');
assert.equal(err.body.name, 'forbidden');
return true;
}
);
// Current password is invalid
await assert.rejects(
apos.http.patch('/api/v1/@apostrophecms/settings/display', {
body: {
displayName: 'Editor',
passwordCurrent: 'invalid'
},
jar
}),
function (err) {
assert.equal(err.status, 403);
assert.equal(err.body.data.path, 'passwordCurrent');
assert.equal(err.body.name, 'forbidden');
return true;
}
);
await apos.http.patch('/api/v1/@apostrophecms/settings/display', {
body: {
displayName: 'Editor',
passwordCurrent: 'editor'
},
jar
});
const validateUser = await apos.user
.find(apos.task.getReq(), { _id: user._id })
.toObject();
assert.equal(validateUser.displayName, 'Editor');
});
});
async function createCommonInstance() {
return t.create({
root: module,
modules: {
'@apostrophecms/i18n': {
options: {
adminLocales: [
{
label: 'English',
value: 'en'
},
{
label: 'French',
value: 'fr'
}
]
}
},
'@apostrophecms/user': {
fields: {
add: {
firstName: {
type: 'string',
label: 'First Name'
},
lastName: {
type: 'string',
label: 'Last Name'
},
displayName: {
type: 'string',
label: 'Display Name'
}
}
}
},
'@apostrophecms/settings': {
options: {
subforms: {
display: {
fields: [ 'displayName' ],
// same as `protection: 'password'`
protection: true,
// validate it can't happen
_passwordChangeForm: true
},
adminLocale: {
fields: [ 'adminLocale' ]
},
name: {
label: 'Name',
fields: [ 'firstName', 'lastName' ],
preview: '{{ firstName }} {{ lastName }}',
protection: 'password'
},
password: {
// Ensure that only `password` will be used
fields: [ 'password', 'displayName' ],
// Test system protected fields
protection: false
}
},
groups: {
account: {
label: 'Account',
subforms: [ 'name', 'nonExisting', 'password' ]
},
preferences: {
label: 'Preferences',
subforms: [ 'adminLocale' ]
}
}
}
}
}
});
}
async function login(apos) {
const user = await t.createUser(apos, 'editor', {
username: 'editor',
password: 'editor'
});
const jar = await t.getUserJar(apos, user);
await apos.http.get('/', { jar });
return {
apos,
user,
jar
};
}
async function logout(apos, username, password, jar) {
await apos.http.post(
'/api/v1/@apostrophecms/login/logout',
{
body: {
username,
password,
session: true
},
jar
}
);
await apos.http.get(
'/',
{
jar
}
);
}