Enabled Members for all sites (#12582)

no-issue

This removes all references to the members labs setting, any code that was run conditionally behind this flag now runs unconditionally. 

* Removed usage of Members labs flag
* Removed tests for Members disabled
* Added dynamic keypair generation for when setting is missing
This commit is contained in:
Fabien 'egg' O'Carroll 2021-01-28 18:07:45 +00:00 committed by Daniel Lockyer
parent 26ee648397
commit 73f6fd8c51
No known key found for this signature in database
GPG Key ID: FFBC6FA2A6F6ABC1
24 changed files with 298 additions and 621 deletions

View File

@ -2,7 +2,7 @@
// Usage: `{{ghost_head}}`
//
// Outputs scripts and other assets at the top of a Ghost theme
const {metaData, escapeExpression, SafeString, logging, settingsCache, config, blogIcon, labs, urlUtils} = require('../services/proxy');
const {metaData, escapeExpression, SafeString, logging, settingsCache, config, blogIcon, urlUtils} = require('../services/proxy');
const _ = require('lodash');
const debug = require('ghost-ignition').debug('ghost_head');
const templateStyles = require('./tpl/styles');
@ -167,7 +167,7 @@ module.exports = function ghost_head(options) { // eslint-disable-line camelcase
}
}
if (!_.includes(context, 'amp') && labs.isSet('members')) {
if (!_.includes(context, 'amp')) {
head.push(getMembersHelper());
}
}

View File

@ -1,16 +1,11 @@
const debug = require('ghost-ignition').debug('services:routing:controllers:unsubscribe');
const path = require('path');
const megaService = require('../../../../server/services/mega');
const labsService = require('../../../../server/services/labs');
const helpers = require('../../../services/routing/helpers');
module.exports = async function unsubscribeController(req, res, next) {
module.exports = async function unsubscribeController(req, res) {
debug('unsubscribeController');
if (!labsService.isSet('members')) {
return next();
}
let data = {};
try {

View File

@ -1,5 +1,4 @@
const membersService = require('../../../../../../services/members');
const labs = require('../../../../../../services/labs');
// @TODO: reconsider the location of this - it's part of members and adds a property to the API
const forPost = (attrs, frame) => {
@ -8,21 +7,18 @@ const forPost = (attrs, frame) => {
attrs.access = true;
}
// Handle members being enabled
if (labs.isSet('members')) {
const memberHasAccess = membersService.contentGating.checkPostAccess(attrs, frame.original.context.member);
const memberHasAccess = membersService.contentGating.checkPostAccess(attrs, frame.original.context.member);
if (!memberHasAccess) {
['plaintext', 'html'].forEach((field) => {
if (attrs[field] !== undefined) {
attrs[field] = '';
}
});
}
if (!memberHasAccess) {
['plaintext', 'html'].forEach((field) => {
if (attrs[field] !== undefined) {
attrs[field] = '';
}
});
}
if (!Object.prototype.hasOwnProperty.call(frame.options, 'columns') || (frame.options.columns.includes('access'))) {
attrs.access = memberHasAccess;
}
if (!Object.prototype.hasOwnProperty.call(frame.options, 'columns') || (frame.options.columns.includes('access'))) {
attrs.access = memberHasAccess;
}
return attrs;

View File

@ -1,17 +1,14 @@
const membersService = require('../../../../../../services/members');
const labs = require('../../../../../../services/labs');
const forPost = (attrs, frame) => {
if (labs.isSet('members')) {
const memberHasAccess = membersService.contentGating.checkPostAccess(attrs, frame.original.context.member);
const memberHasAccess = membersService.contentGating.checkPostAccess(attrs, frame.original.context.member);
if (!memberHasAccess) {
['plaintext', 'html'].forEach((field) => {
if (attrs[field] !== undefined) {
attrs[field] = '';
}
});
}
if (!memberHasAccess) {
['plaintext', 'html'].forEach((field) => {
if (attrs[field] !== undefined) {
attrs[field] = '';
}
});
}
return attrs;

View File

@ -1,5 +1,4 @@
const membersService = require('../../../../../../services/members');
const labs = require('../../../../../../services/labs');
// @TODO: reconsider the location of this - it's part of members and adds a property to the API
const forPost = (attrs, frame) => {
@ -8,21 +7,18 @@ const forPost = (attrs, frame) => {
attrs.access = true;
}
// Handle members being enabled
if (labs.isSet('members')) {
const memberHasAccess = membersService.contentGating.checkPostAccess(attrs, frame.original.context.member);
const memberHasAccess = membersService.contentGating.checkPostAccess(attrs, frame.original.context.member);
if (!memberHasAccess) {
['plaintext', 'html'].forEach((field) => {
if (attrs[field] !== undefined) {
attrs[field] = '';
}
});
}
if (!memberHasAccess) {
['plaintext', 'html'].forEach((field) => {
if (attrs[field] !== undefined) {
attrs[field] = '';
}
});
}
if (!Object.prototype.hasOwnProperty.call(frame.options, 'columns') || (frame.options.columns.includes('access'))) {
attrs.access = memberHasAccess;
}
if (!Object.prototype.hasOwnProperty.call(frame.options, 'columns') || (frame.options.columns.includes('access'))) {
attrs.access = memberHasAccess;
}
return attrs;

View File

@ -45,7 +45,7 @@ Post = ghostBookshelf.Model.extend({
defaults: function defaults() {
let visibility = 'public';
if (settingsCache.get('labs') && (settingsCache.get('labs').members === true) && settingsCache.get('default_content_visibility')) {
if (settingsCache.get('default_content_visibility')) {
visibility = settingsCache.get('default_content_visibility');
}

View File

@ -8,7 +8,6 @@ const ghostBookshelf = require('./base');
const {i18n} = require('../lib/common');
const errors = require('@tryghost/errors');
const validation = require('../data/validation');
const settingsCache = require('../services/settings/cache');
const internalContext = {context: {internal: true}};
let Settings;
let defaultSettings;
@ -298,25 +297,6 @@ Settings = ghostBookshelf.Model.extend({
},
permissible: function permissible(modelId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) {
let isEdit = (action === 'edit');
let isOwner;
function isChangingMembers() {
if (unsafeAttrs && unsafeAttrs.key === 'labs') {
let editedValue = JSON.parse(unsafeAttrs.value);
if (editedValue.members !== undefined) {
return editedValue.members !== settingsCache.get('labs').members;
}
}
}
isOwner = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Owner'});
if (isEdit && isChangingMembers()) {
// Only allow owner to toggle members flag
hasUserPermission = isOwner;
}
if (hasUserPermission && hasApiKeyPermission) {
return Promise.resolve();
}

View File

@ -1,4 +1,3 @@
const labs = require('../labs');
const errors = require('@tryghost/errors');
const {i18n} = require('../../lib/common');
@ -9,7 +8,7 @@ const authorize = {
if (hasApiKey) {
return next();
}
if (labs.isSet('members') && hasMember) {
if (hasMember) {
return next();
}
return next(new errors.NoPermissionError({

View File

@ -1,18 +1,11 @@
const jwt = require('express-jwt');
const membersService = require('../../members');
const labs = require('../../labs');
const config = require('../../../../shared/config');
let UNO_MEMBERINO;
module.exports = {
get authenticateMembersToken() {
if (!labs.isSet('members')) {
return function (req, res, next) {
return next();
};
}
if (!UNO_MEMBERINO) {
const url = require('url');
const {protocol, host} = url.parse(config.get('url'));

View File

@ -1,5 +1,6 @@
const {URL} = require('url');
const crypto = require('crypto');
const createKeypair = require('keypair');
const path = require('path');
const COMPLIMENTARY_PLAN = {
@ -230,10 +231,20 @@ class MembersConfigProvider {
this._urlUtils.urlFor('admin', true)
);
let privateKey = this._settingsCache.get('members_private_key');
let publicKey = this._settingsCache.get('members_public_key');
if (!privateKey || !publicKey) {
this._logging.warn('Could not find members_private_key, using dynamically generated keypair');
const keypair = createKeypair({bits: 1024});
privateKey = keypair.private;
publicKey = keypair.public;
}
return {
issuer: membersApiUrl,
publicKey: this._settingsCache.get('members_public_key'),
privateKey: this._settingsCache.get('members_private_key')
publicKey,
privateKey
};
}

View File

@ -1,6 +1,5 @@
const _ = require('lodash');
const logging = require('../../../shared/logging');
const labsService = require('../labs');
const membersService = require('./index');
const urlUtils = require('../../../shared/url-utils');
const ghostVersion = require('../../lib/ghost-version');
@ -10,10 +9,6 @@ const {formattedMemberResponse} = require('./utils');
// @TODO: This piece of middleware actually belongs to the frontend, not to the member app
// Need to figure a way to separate these things (e.g. frontend actually talks to members API)
const loadMemberSession = async function (req, res, next) {
if (!labsService.isSet('members')) {
req.member = null;
return next();
}
try {
const member = await membersService.ssr.getMemberDataFromSession(req, res);
Object.assign(req, {member});

View File

@ -88,31 +88,30 @@ module.exports = function apiRoutes() {
router.del('/tags/:id', mw.authAdminApi, http(apiCanary.tags.destroy));
// ## Members
router.get('/members', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.browse));
router.post('/members', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.add));
router.get('/members', mw.authAdminApi, http(apiCanary.members.browse));
router.post('/members', mw.authAdminApi, http(apiCanary.members.add));
router.get('/members/stats', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.stats));
router.get('/members/stats', mw.authAdminApi, http(apiCanary.members.stats));
router.get('/members/upload', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.exportCSV));
router.get('/members/upload', mw.authAdminApi, http(apiCanary.members.exportCSV));
router.post('/members/upload',
shared.middlewares.labs.members,
mw.authAdminApi,
apiMw.upload.single('membersfile'),
apiMw.upload.validation({type: 'members'}),
http(apiCanary.members.importCSV)
);
router.get('/members/hasActiveStripeSubscriptions', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.hasActiveStripeSubscriptions));
router.get('/members/hasActiveStripeSubscriptions', mw.authAdminApi, http(apiCanary.members.hasActiveStripeSubscriptions));
router.get('/members/stripe_connect', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.membersStripeConnect.auth));
router.get('/members/stripe_connect', mw.authAdminApi, http(apiCanary.membersStripeConnect.auth));
router.get('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.read));
router.put('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.edit));
router.del('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.destroy));
router.get('/members/:id', mw.authAdminApi, http(apiCanary.members.read));
router.put('/members/:id', mw.authAdminApi, http(apiCanary.members.edit));
router.del('/members/:id', mw.authAdminApi, http(apiCanary.members.destroy));
router.put('/members/:id/subscriptions/:subscription_id', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.editSubscription));
router.put('/members/:id/subscriptions/:subscription_id', mw.authAdminApi, http(apiCanary.members.editSubscription));
router.get('/members/:id/signin_urls', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.memberSigninUrls.read));
router.get('/members/:id/signin_urls', mw.authAdminApi, http(apiCanary.memberSigninUrls.read));
// ## Labels
router.get('/labels', mw.authAdminApi, http(apiCanary.labels.browse));

View File

@ -88,31 +88,30 @@ module.exports = function apiRoutes() {
router.del('/tags/:id', mw.authAdminApi, http(api.tags.destroy));
// ## Members
router.get('/members', shared.middlewares.labs.members, mw.authAdminApi, http(api.members.browse));
router.post('/members', shared.middlewares.labs.members, mw.authAdminApi, http(api.members.add));
router.get('/members', mw.authAdminApi, http(api.members.browse));
router.post('/members', mw.authAdminApi, http(api.members.add));
router.get('/members/stats', shared.middlewares.labs.members, mw.authAdminApi, http(api.members.stats));
router.get('/members/stats', mw.authAdminApi, http(api.members.stats));
router.get('/members/upload', shared.middlewares.labs.members, mw.authAdminApi, http(api.members.exportCSV));
router.get('/members/upload', mw.authAdminApi, http(api.members.exportCSV));
router.post('/members/upload',
shared.middlewares.labs.members,
mw.authAdminApi,
apiMw.upload.single('membersfile'),
apiMw.upload.validation({type: 'members'}),
http(api.members.importCSV)
);
router.get('/members/hasActiveStripeSubscriptions', shared.middlewares.labs.members, mw.authAdminApi, http(api.members.hasActiveStripeSubscriptions));
router.get('/members/hasActiveStripeSubscriptions', mw.authAdminApi, http(api.members.hasActiveStripeSubscriptions));
router.get('/members/stripe_connect', shared.middlewares.labs.members, mw.authAdminApi, http(api.membersStripeConnect.auth));
router.get('/members/stripe_connect', mw.authAdminApi, http(api.membersStripeConnect.auth));
router.get('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(api.members.read));
router.put('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(api.members.edit));
router.del('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(api.members.destroy));
router.get('/members/:id', mw.authAdminApi, http(api.members.read));
router.put('/members/:id', mw.authAdminApi, http(api.members.edit));
router.del('/members/:id', mw.authAdminApi, http(api.members.destroy));
router.put('/members/:id/subscriptions/:subscription_id', shared.middlewares.labs.members, mw.authAdminApi, http(api.members.editSubscription));
router.put('/members/:id/subscriptions/:subscription_id', mw.authAdminApi, http(api.members.editSubscription));
router.get('/members/:id/signin_urls', shared.middlewares.labs.members, mw.authAdminApi, http(api.memberSigninUrls.read));
router.get('/members/:id/signin_urls', mw.authAdminApi, http(api.memberSigninUrls.read));
// ## Labels
router.get('/labels', mw.authAdminApi, http(api.labels.browse));

View File

@ -15,9 +15,6 @@ module.exports = function setupMembersApp() {
// send 503 json response in case of maintenance
membersApp.use(shared.middlewares.maintenance);
// Entire app is behind labs flag
membersApp.use(shared.middlewares.labs.members);
// Support CORS for requests from the frontend
const siteUrl = new URL(urlUtils.getSiteUrl());
membersApp.use(cors(siteUrl.origin));

View File

@ -9,6 +9,4 @@ const labs = flag => (req, res, next) => {
}
};
labs.members = labs('members');
module.exports = labs;

View File

@ -16,155 +16,78 @@ describe('Basic Members Routes', function () {
request = supertest.agent(configUtils.config.get('url'));
});
describe('Members enabled', function () {
before(function () {
const originalSettingsCacheGetFn = settingsCache.get;
before(function () {
const originalSettingsCacheGetFn = settingsCache.get;
sinon.stub(settingsCache, 'get').callsFake(function (key, options) {
if (key === 'labs') {
return {members: true};
}
sinon.stub(settingsCache, 'get').callsFake(function (key, options) {
if (key === 'labs') {
return {members: true};
}
return originalSettingsCacheGetFn(key, options);
});
});
after(function () {
sinon.restore();
});
describe('Routes', function () {
it('should error serving webhook endpoint without any parameters', async function () {
await request.post('/members/webhooks/stripe')
.expect(400);
});
it('should error when invalid member token is passed into session', async function () {
await request.get('/members/api/session')
.expect(400);
});
it('should return no content when removing member sessions', async function () {
await request.del('/members/api/session')
.expect(204);
});
it('should error for invalid member token on member data endpoint', async function () {
await request.get('/members/api/member')
.expect(400);
});
it('should serve member site endpoint', async function () {
await request.get('/members/api/site')
.expect(200);
});
it('should error for invalid data on member magic link endpoint', async function () {
await request.post('/members/api/send-magic-link')
.expect(400);
});
it('should error for invalid data on members create checkout session endpoint', async function () {
await request.post('/members/api/create-stripe-checkout-session')
.expect(400);
});
it('should error for invalid data on members create update session endpoint', async function () {
await request.post('/members/api/create-stripe-update-session')
.expect(400);
});
it('should error for invalid data on members subscription endpoint', async function () {
await request.put('/members/api/subscriptions/123')
.expect(400);
});
it('should serve theme 404 on members endpoint', async function () {
await request.get('/members/')
.expect(404)
.expect('Content-Type', 'text/html; charset=utf-8');
});
it('should redirect invalid token on members endpoint', async function () {
await request.get('/members/?token=abc&action=signup')
.expect(302)
.expect('Location', '/?action=signup&success=false');
});
return originalSettingsCacheGetFn(key, options);
});
});
describe('Members disabled', function () {
before(function () {
const originalSettingsCacheGetFn = settingsCache.get;
after(function () {
sinon.restore();
});
sinon.stub(settingsCache, 'get').callsFake(function (key, options) {
if (key === 'labs') {
return {members: false};
}
return originalSettingsCacheGetFn(key, options);
});
describe('Routes', function () {
it('should error serving webhook endpoint without any parameters', async function () {
await request.post('/members/webhooks/stripe')
.expect(400);
});
after(function () {
sinon.restore();
it('should error when invalid member token is passed into session', async function () {
await request.get('/members/api/session')
.expect(400);
});
describe('Routes', function () {
it('should not serve webhook endpoint', async function () {
await request.post('/members/webhooks/stripe')
.expect(404);
});
it('should return no content when removing member sessions', async function () {
await request.del('/members/api/session')
.expect(204);
});
it('should not serve session endpoint', async function () {
await request.get('/members/api/session')
.expect(404);
});
it('should error for invalid member token on member data endpoint', async function () {
await request.get('/members/api/member')
.expect(400);
});
it('should not serve session removal endpoint', async function () {
await request.del('/members/api/session')
.expect(404);
});
it('should serve member site endpoint', async function () {
await request.get('/members/api/site')
.expect(200);
});
it('should not serve member data endpoint', async function () {
await request.get('/members/api/member')
.expect(404);
});
it('should error for invalid data on member magic link endpoint', async function () {
await request.post('/members/api/send-magic-link')
.expect(400);
});
it('should not serve member site endpoint', async function () {
await request.get('/members/api/site')
.expect(404);
});
it('should error for invalid data on members create checkout session endpoint', async function () {
await request.post('/members/api/create-stripe-checkout-session')
.expect(400);
});
it('should not serve member magic link endpoint', async function () {
await request.post('/members/api/send-magic-link')
.expect(404);
});
it('should error for invalid data on members create update session endpoint', async function () {
await request.post('/members/api/create-stripe-update-session')
.expect(400);
});
it('should not serve members create checkout session endpoint', async function () {
await request.post('/members/api/create-stripe-checkout-session')
.expect(404);
});
it('should error for invalid data on members subscription endpoint', async function () {
await request.put('/members/api/subscriptions/123')
.expect(400);
});
it('should not serve members create update session endpoint', async function () {
await request.post('/members/api/create-stripe-update-session')
.expect(404);
});
it('should serve theme 404 on members endpoint', async function () {
await request.get('/members/')
.expect(404)
.expect('Content-Type', 'text/html; charset=utf-8');
});
it('should not serve members subscription endpoint', async function () {
await request.put('/members/api/subscriptions/123')
.expect(404);
});
it('should serve 404 on members endpoint', async function () {
await request.get('/members/')
.expect(404);
});
it('should not redirect members endpoint with token', async function () {
await request.get('/members/?token=abc&action=signup')
.expect(404);
});
it('should redirect invalid token on members endpoint', async function () {
await request.get('/members/?token=abc&action=signup')
.expect(302)
.expect('Location', '/?action=signup&success=false');
});
});
});

View File

@ -363,43 +363,6 @@ describe('Settings API (canary)', function () {
});
});
it('can toggle member setting', function () {
return request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.then(function (res) {
const jsonResponse = res.body;
const settingToChange = {
settings: [
{
key: 'labs',
value: '{"subscribers":false,"members":false}'
}
]
};
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
return request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then(function ({body, headers}) {
const putBody = body;
headers['x-cache-invalidate'].should.eql('/*');
should.exist(putBody);
putBody.settings[0].key.should.eql('labs');
putBody.settings[0].value.should.eql(JSON.stringify({subscribers: false, members: false}));
});
});
});
it('can\'t edit permalinks', function (done) {
const settingToChange = {
settings: [{key: 'permalinks', value: '/:primary_author/:slug/'}]
@ -521,31 +484,6 @@ describe('Settings API (canary)', function () {
return localUtils.doAuth(request);
});
});
it('cannot toggle member setting', function (done) {
const settingToChange = {
settings: [
{
key: 'labs',
value: '{"subscribers":false,"members":true}'
}
]
};
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403)
.end(function (err, res) {
if (err) {
return done(err);
}
done();
});
});
});
describe('As Editor', function () {

View File

@ -368,44 +368,6 @@ describe('Settings API (v2)', function () {
});
});
it('can toggle member setting', function () {
return request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.then(function (res) {
const jsonResponse = res.body;
const changedValue = [];
const settingToChange = {
settings: [
{
key: 'labs',
value: '{"subscribers":false,"members":false}'
}
]
};
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
return request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then(function ({body, headers}) {
const putBody = body;
headers['x-cache-invalidate'].should.eql('/*');
should.exist(putBody);
putBody.settings[0].key.should.eql('labs');
putBody.settings[0].value.should.eql(JSON.stringify({subscribers: false, members: false}));
});
});
});
it('can\'t edit permalinks', function (done) {
const settingToChange = {
settings: [{key: 'permalinks', value: '/:primary_author/:slug/'}]
@ -524,31 +486,6 @@ describe('Settings API (v2)', function () {
return localUtils.doAuth(request);
});
});
it('cannot toggle member setting', function (done) {
const settingToChange = {
settings: [
{
key: 'labs',
value: '{"subscribers":false,"members":true}'
}
]
};
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403)
.end(function (err, res) {
if (err) {
return done(err);
}
done();
});
});
});
describe('As Editor', function () {

View File

@ -408,43 +408,6 @@ describe('Settings API (v3)', function () {
});
});
it('can toggle member setting', function () {
return request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.then(function (res) {
const jsonResponse = res.body;
const settingToChange = {
settings: [
{
key: 'labs',
value: '{"subscribers":false,"members":false}'
}
]
};
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
return request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then(function ({body, headers}) {
const putBody = body;
headers['x-cache-invalidate'].should.eql('/*');
should.exist(putBody);
putBody.settings[0].key.should.eql('labs');
putBody.settings[0].value.should.eql(JSON.stringify({subscribers: false, members: false}));
});
});
});
it('can\'t edit permalinks', function (done) {
const settingToChange = {
settings: [{key: 'permalinks', value: '/:primary_author/:slug/'}]
@ -566,31 +529,6 @@ describe('Settings API (v3)', function () {
return localUtils.doAuth(request);
});
});
it('cannot toggle member setting', function (done) {
const settingToChange = {
settings: [
{
key: 'labs',
value: '{"subscribers":false,"members":true}'
}
]
};
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403)
.end(function (err, res) {
if (err) {
return done(err);
}
done();
});
});
});
describe('As Editor', function () {

View File

@ -39,6 +39,9 @@ describe('Unit: canary/utils/serializers/output/utils/mapper', function () {
it('calls mapper on relations', function () {
const frame = {
original: {
context: {}
},
options: {
withRelated: ['tags', 'authors'],
context: {}

View File

@ -1,137 +1,115 @@
const should = require('should');
const sinon = require('sinon');
const labs = require('../../../../../../../../core/server/services/labs');
const gating = require('../../../../../../../../core/server/api/canary/utils/serializers/output/utils/post-gating');
describe('Unit: canary/utils/serializers/output/utils/post-gating', function () {
describe('for post', function () {
it('does not modify attributes when members is disabled', function () {
it('should NOT hide content attributes when visibility is public', function () {
const attrs = {
visibility: 'public',
plaintext: 'no touching',
html: '<p>I am here to stay</p>'
};
const frame = {
options: {}
options: {},
original: {
context: {}
}
};
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('no touching');
});
describe('labs.members enabled', function () {
before(function () {
sinon.stub(labs, 'isSet').returns(true);
});
it('should hide content attributes when visibility is "members"', function () {
const attrs = {
visibility: 'members',
plaintext: 'no touching. secret stuff',
html: '<p>I am here to stay</p>'
};
it('should NOT hide content attributes when visibility is public', function () {
const attrs = {
visibility: 'public',
plaintext: 'no touching',
html: '<p>I am here to stay</p>'
};
const frame = {
options: {},
original: {
context: {}
}
};
const frame = {
options: {},
original: {
context: {}
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('');
attrs.html.should.eql('');
});
it('should NOT hide content attributes when visibility is "members" and member is present', function () {
const attrs = {
visibility: 'members',
plaintext: 'I see dead people',
html: '<p>What\'s the matter?</p>'
};
const frame = {
options: {},
original: {
context: {
member: {}
}
};
}
};
gating.forPost(attrs, frame);
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('no touching');
});
attrs.plaintext.should.eql('I see dead people');
attrs.html.should.eql('<p>What\'s the matter?</p>');
});
it('should hide content attributes when visibility is "members"', function () {
const attrs = {
visibility: 'members',
plaintext: 'no touching. secret stuff',
html: '<p>I am here to stay</p>'
};
it('should hide content attributes when visibility is "paid" and member has status of "free"', function () {
const attrs = {
visibility: 'paid',
plaintext: 'I see dead people',
html: '<p>What\'s the matter?</p>'
};
const frame = {
options: {},
original: {
context: {}
}
};
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('');
attrs.html.should.eql('');
});
it('should NOT hide content attributes when visibility is "members" and member is present', function () {
const attrs = {
visibility: 'members',
plaintext: 'I see dead people',
html: '<p>What\'s the matter?</p>'
};
const frame = {
options: {},
original: {
context: {
member: {}
const frame = {
options: {},
original: {
context: {
member: {
status: 'free'
}
}
};
}
};
gating.forPost(attrs, frame);
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('I see dead people');
attrs.html.should.eql('<p>What\'s the matter?</p>');
});
attrs.plaintext.should.eql('');
attrs.html.should.eql('');
});
it('should hide content attributes when visibility is "paid" and member has status of "free"', function () {
const attrs = {
visibility: 'paid',
plaintext: 'I see dead people',
html: '<p>What\'s the matter?</p>'
};
it('should NOT hide content attributes when visibility is "paid" and member has status of "paid"', function () {
const attrs = {
visibility: 'paid',
plaintext: 'Secret paid content',
html: '<p>Can read this</p>'
};
const frame = {
options: {},
original: {
context: {
member: {
status: 'free'
}
const frame = {
options: {},
original: {
context: {
member: {
status: 'paid'
}
}
};
}
};
gating.forPost(attrs, frame);
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('');
attrs.html.should.eql('');
});
it('should NOT hide content attributes when visibility is "paid" and member has status of "paid"', function () {
const attrs = {
visibility: 'paid',
plaintext: 'Secret paid content',
html: '<p>Can read this</p>'
};
const frame = {
options: {},
original: {
context: {
member: {
status: 'paid'
}
}
}
};
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('Secret paid content');
attrs.html.should.eql('<p>Can read this</p>');
});
attrs.plaintext.should.eql('Secret paid content');
attrs.html.should.eql('<p>Can read this</p>');
});
});
});

View File

@ -39,6 +39,9 @@ describe('Unit: v2/utils/serializers/output/utils/mapper', function () {
it('calls mapper on relations', function () {
const frame = {
original: {
context: {}
},
options: {
withRelated: ['tags', 'authors'],
context: {}

View File

@ -1,133 +1,132 @@
const should = require('should');
const sinon = require('sinon');
const labs = require('../../../../../../../../core/server/services/labs');
const gating = require('../../../../../../../../core/server/api/v2/utils/serializers/output/utils/post-gating');
describe('Unit: v2/utils/serializers/output/utils/post-gating', function () {
describe('for post', function () {
it('does not modify attributes when members is disabled', function () {
it('should NOT hide content attributes when visibility is public', function () {
const attrs = {
visibility: 'public',
plaintext: 'no touching',
html: '<p>I am here to stay</p>'
};
const frame = {
options: {}
original: {
context: {}
}
};
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('no touching');
});
describe('labs.members enabled', function () {
before(function () {
sinon.stub(labs, 'isSet').returns(true);
});
it('should hide content attributes when visibility is "members"', function () {
const attrs = {
visibility: 'members',
plaintext: 'no touching. secret stuff',
html: '<p>I am here to stay</p>'
};
it('should NOT hide content attributes when visibility is public', function () {
const attrs = {
visibility: 'public',
plaintext: 'no touching',
html: '<p>I am here to stay</p>'
};
const frame = {
original: {
context: {}
}
};
const frame = {
original: {
context: {}
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('');
attrs.html.should.eql('');
});
it('should NOT hide content attributes when visibility is "members" and member is present', function () {
const attrs = {
visibility: 'members',
plaintext: 'I see dead people',
html: '<p>What\'s the matter?</p>'
};
const frame = {
original: {
context: {
member: {}
}
};
}
};
gating.forPost(attrs, frame);
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('no touching');
});
attrs.plaintext.should.eql('I see dead people');
attrs.html.should.eql('<p>What\'s the matter?</p>');
});
it('should hide content attributes when visibility is "members"', function () {
const attrs = {
visibility: 'members',
plaintext: 'no touching. secret stuff',
html: '<p>I am here to stay</p>'
};
it('should hide content attributes when visibility is "paid" and member has no subscription', function () {
const attrs = {
visibility: 'paid',
plaintext: 'I see dead people',
html: '<p>What\'s the matter?</p>'
};
const frame = {
original: {
context: {}
const frame = {
original: {
context: {
member: {}
}
};
}
};
gating.forPost(attrs, frame);
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('');
attrs.html.should.eql('');
});
attrs.plaintext.should.eql('');
attrs.html.should.eql('');
});
it('should NOT hide content attributes when visibility is "members" and member is present', function () {
const attrs = {
visibility: 'members',
plaintext: 'I see dead people',
html: '<p>What\'s the matter?</p>'
};
it('should hide content attributes when visibility is "paid" and member has status of "free"', function () {
const attrs = {
visibility: 'paid',
plaintext: 'I see dead people',
html: '<p>What\'s the matter?</p>'
};
const frame = {
original: {
context: {
member: {}
const frame = {
original: {
context: {
member: {
status: 'free'
}
}
};
}
};
gating.forPost(attrs, frame);
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('I see dead people');
attrs.html.should.eql('<p>What\'s the matter?</p>');
});
attrs.plaintext.should.eql('');
attrs.html.should.eql('');
});
it('should hide content attributes when visibility is "paid" and member has status of "free"', function () {
const attrs = {
visibility: 'paid',
plaintext: 'I see dead people',
html: '<p>What\'s the matter?</p>'
};
it('should NOT hide content attributes when visibility is "paid" and member has status of "paid"', function () {
const attrs = {
visibility: 'paid',
plaintext: 'Secret paid content',
html: '<p>Can read this</p>'
};
const frame = {
original: {
context: {
member: {
status: 'free'
}
const frame = {
options: {},
original: {
context: {
member: {
status: 'paid'
}
}
};
}
};
gating.forPost(attrs, frame);
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('');
attrs.html.should.eql('');
});
it('should NOT hide content attributes when visibility is "paid" and member has status of "paid"', function () {
const attrs = {
visibility: 'paid',
plaintext: 'Secret paid content',
html: '<p>Can read this</p>'
};
const frame = {
options: {},
original: {
context: {
member: {
status: 'paid'
}
}
}
};
gating.forPost(attrs, frame);
attrs.plaintext.should.eql('Secret paid content');
attrs.html.should.eql('<p>Can read this</p>');
});
attrs.plaintext.should.eql('Secret paid content');
attrs.html.should.eql('<p>Can read this</p>');
});
});
});

View File

@ -39,6 +39,9 @@ describe('Unit: v3/utils/serializers/output/utils/mapper', function () {
it('calls mapper on relations', function () {
const frame = {
original: {
context: {}
},
options: {
withRelated: ['tags', 'authors'],
context: {}