diff --git a/apps/portal/README.md b/apps/portal/README.md index fa568b4099..6f4591d74b 100644 --- a/apps/portal/README.md +++ b/apps/portal/README.md @@ -48,6 +48,8 @@ Open [http://localhost:3000](http://localhost:3000) to view it in the browser. The page will reload if you make edits.
You will also see any lint errors in the console. +Start the portal server when developing Ghost by running Ghost (in root folder) via `yarn dev --all` or `yarn dev --portal`. This will host the portal JavaScript files, and makes sure that Ghost uses these locally hosted assets instead of the ones from the CDN. + ### `yarn build` Creates the production single minified bundle for external use in `umd/portal.min.js`.
diff --git a/ghost/members-api/lib/repositories/MemberRepository.js b/ghost/members-api/lib/repositories/MemberRepository.js index bbcd2d315f..3019f331e5 100644 --- a/ghost/members-api/lib/repositories/MemberRepository.js +++ b/ghost/members-api/lib/repositories/MemberRepository.js @@ -18,7 +18,8 @@ const messages = { productNotFound: 'Could not find Product {id}', bulkActionRequiresFilter: 'Cannot perform {action} without a filter or all=true', tierArchived: 'Cannot use archived Tiers', - invalidEmail: 'Invalid Email' + invalidEmail: 'Invalid Email', + invalidNewsletterId: 'Cannot subscribe to invalid newsletter {id}' }; /** @@ -281,7 +282,18 @@ module.exports = class MemberRepository { memberStatusData.status = 'comped'; } - // Subscribe member to default newsletters + //checks for custom signUp forms + if (memberData.newsletters && memberData.newsletters.length > 0) { + const savedNewsletter = await this._newslettersService.browse({filter: `id:'${memberData.newsletters[0].id}'`}); + if (savedNewsletter.length === 0) { + throw new errors.BadRequestError({message: tpl(messages.invalidNewsletterId, {id: memberData.newsletters[0].id})}); + } + if (savedNewsletter[0].status === 'archived') { + memberData.newsletters = []; + } + } + + // Subscribe members to default newsletters if (memberData.subscribed !== false && !memberData.newsletters) { const browseOptions = _.pick(options, 'transacting'); memberData.newsletters = await this.getSubscribeOnSignupNewsletters(browseOptions); diff --git a/ghost/members-api/test/unit/lib/repositories/member.test.js b/ghost/members-api/test/unit/lib/repositories/member.test.js index 0baf70977d..35b639013b 100644 --- a/ghost/members-api/test/unit/lib/repositories/member.test.js +++ b/ghost/members-api/test/unit/lib/repositories/member.test.js @@ -310,4 +310,134 @@ describe('MemberRepository', function () { })).should.be.true(); }); }); + + describe('create', function () { + let memberStub, memberModelStub, newslettersServiceStub; + + beforeEach(function () { + memberStub = { + get: sinon.stub(), + related: sinon.stub() + }; + + memberStub.related + .withArgs('products').returns({ + models: [] + }) + .withArgs('newsletters').returns({ + models: [] + }); + + memberModelStub = { + add: sinon.stub().resolves(memberStub) + }; + + newslettersServiceStub = { + browse: sinon.stub() + }; + }); + + it('subscribes a member to a specified newsletter', async function () { + const newsletter = { + id: 'abc123', + status: 'active' + }; + + newslettersServiceStub.browse + .withArgs({ + filter: `id:'${newsletter.id}'` + }) + .resolves([newsletter]); + + const repo = new MemberRepository({ + Member: memberModelStub, + MemberStatusEvent: { + add: sinon.stub().resolves() + }, + MemberSubscribeEvent: { + add: sinon.stub().resolves() + }, + newslettersService: newslettersServiceStub + }); + + await repo.create({ + email: 'jamie@example.com', + email_disabled: false, + newsletters: [ + {id: newsletter.id} + ] + }); + + newslettersServiceStub.browse.calledOnce.should.be.true(); + memberModelStub.add.calledOnce.should.be.true(); + memberModelStub.add.args[0][0].newsletters.should.eql([ + {id: newsletter.id} + ]); + }); + + it('does not allow a member to be subscribed to an invalid newsletter', async function () { + const INVALID_NEWSLETTER_ID = 'abc123'; + + newslettersServiceStub.browse + .withArgs({ + filter: `id:'${INVALID_NEWSLETTER_ID}'` + }) + .resolves([]); + + const repo = new MemberRepository({ + Member: memberModelStub, + MemberStatusEvent: { + add: sinon.stub().resolves() + }, + MemberSubscribeEvent: { + add: sinon.stub().resolves() + }, + newslettersService: newslettersServiceStub + }); + + await repo.create({ + email: 'jamie@example.com', + email_disabled: false, + newsletters: [ + {id: INVALID_NEWSLETTER_ID} + ] + }).should.be.rejectedWith(`Cannot subscribe to invalid newsletter ${INVALID_NEWSLETTER_ID}`); + }); + + it('does not subscribe a member to an archived newsletter', async function () { + const newsletter = { + id: 'abc123', + status: 'archived' + }; + + newslettersServiceStub.browse + .withArgs({ + filter: `id:'${newsletter.id}'` + }) + .resolves([newsletter]); + + const repo = new MemberRepository({ + Member: memberModelStub, + MemberStatusEvent: { + add: sinon.stub().resolves() + }, + MemberSubscribeEvent: { + add: sinon.stub().resolves() + }, + newslettersService: newslettersServiceStub + }); + + await repo.create({ + email: 'jamie@example.com', + email_disabled: false, + newsletters: [ + {id: newsletter.id} + ] + }); + + newslettersServiceStub.browse.calledOnce.should.be.true(); + memberModelStub.add.calledOnce.should.be.true(); + memberModelStub.add.args[0][0].newsletters.should.eql([]); + }); + }); });