0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-08 02:52:39 -05:00

Fixed bulk unsubscribe and updated member import tests (#14610)

refs https://github.com/TryGhost/Team/issues/1567

**Changes in members-api**
- Compare changes: https://github.com/TryGhost/Members/compare/%40tryghost/members-api%406.1.0...%40tryghost/members-api%406.2.2
- Fixed bulk unsubscribe
- Deletes the newsletter relations instead of setting subscribed to false

**Test fail fix**
refs https://github.com/TryGhost/Ghost/pull/14621
refs https://ghost.slack.com/archives/C02G9E68C/p1651126990299689?thread_ts=1651072733.859939&cid=C02G9E68C

- Events didn't always have the same created_at as created members
- This caused a test to fail randomly in the main repo

**Changes**
- Added required helpers for members-api package
- Version bumps of other packages are only tooling related

**Tests**
- Tests if member import still works with the legacy `subscribed` flag
- Updated member importer to use multipleNewsletters flag
- Dropped legacy members tests
This commit is contained in:
Simon Backx 2022-04-28 09:50:05 +02:00 committed by GitHub
parent ad2903a196
commit efdc42c257
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 122 additions and 791 deletions

View file

@ -373,6 +373,18 @@ const Member = ghostBookshelf.Model.extend({
return query;
},
getNewsletterRelations(data, unfilteredOptions = {}) {
const query = ghostBookshelf.knex('members_newsletters')
.select('id')
.whereIn('member_id', data.memberIds);
if (unfilteredOptions.transacting) {
query.transacting(unfilteredOptions.transacting);
}
return query;
},
fetchAllSubscribed(unfilteredOptions = {}) {
// we use raw queries instead of model relationships because model hydration is expensive
const query = ghostBookshelf.knex('members_newsletters')

View file

@ -67,11 +67,11 @@
"@tryghost/custom-theme-settings-service": "0.3.2",
"@tryghost/database-info": "0.3.3",
"@tryghost/debug": "0.1.16",
"@tryghost/domain-events": "0.1.9",
"@tryghost/domain-events": "0.1.10",
"@tryghost/email-analytics-provider-mailgun": "1.0.8",
"@tryghost/email-analytics-service": "1.0.6",
"@tryghost/errors": "1.2.12",
"@tryghost/express-dynamic-redirects": "0.2.8",
"@tryghost/express-dynamic-redirects": "0.2.9",
"@tryghost/helpers": "1.1.64",
"@tryghost/image-transform": "1.0.30",
"@tryghost/job-manager": "0.8.22",
@ -82,14 +82,14 @@
"@tryghost/kg-mobiledoc-html-renderer": "5.3.5",
"@tryghost/limit-service": "1.1.0",
"@tryghost/logging": "2.1.8",
"@tryghost/magic-link": "1.0.21",
"@tryghost/member-events": "0.4.1",
"@tryghost/members-api": "6.1.0",
"@tryghost/magic-link": "1.0.22",
"@tryghost/member-events": "0.4.2",
"@tryghost/members-api": "6.2.2",
"@tryghost/members-events-service": "0.3.3",
"@tryghost/members-importer": "0.5.8",
"@tryghost/members-offers": "0.11.1",
"@tryghost/members-ssr": "1.0.23",
"@tryghost/members-stripe-service": "0.10.0",
"@tryghost/members-importer": "0.5.9",
"@tryghost/members-offers": "0.11.2",
"@tryghost/members-ssr": "1.0.24",
"@tryghost/members-stripe-service": "0.10.1",
"@tryghost/metrics": "1.0.11",
"@tryghost/minifier": "0.1.13",
"@tryghost/mw-api-version-mismatch": "0.1.1",
@ -111,7 +111,7 @@
"@tryghost/update-check-service": "0.3.2",
"@tryghost/url-utils": "2.1.0",
"@tryghost/validator": "0.1.24",
"@tryghost/verification-trigger": "0.2.0",
"@tryghost/verification-trigger": "0.2.1",
"@tryghost/version": "0.1.14",
"@tryghost/version-notifications-data-service": "0.1.0",
"@tryghost/vhost-middleware": "1.0.23",

View file

@ -1,709 +0,0 @@
const path = require('path');
const should = require('should');
const supertest = require('supertest');
const sinon = require('sinon');
const testUtils = require('../../utils');
const localUtils = require('./utils');
const config = require('../../../core/shared/config');
const Papa = require('papaparse');
const {mockManager} = require('../../utils/e2e-framework');
describe('Legacy Members API', function () {
let request;
beforeEach(function () {
mockManager.mockLabsDisabled('multipleProducts');
});
afterEach(function () {
mockManager.restore();
});
before(async function () {
await localUtils.startGhost();
request = supertest.agent(config.get('url'));
await localUtils.doAuth(request, 'members', 'members:emails');
});
it('Can browse', async function () {
const res = await request
.get(localUtils.API.getApiQuery('members/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.members);
jsonResponse.members.should.have.length(8);
localUtils.API.checkResponse(jsonResponse.members[0], 'member', 'subscriptions');
testUtils.API.isISO8601(jsonResponse.members[0].created_at).should.be.true();
jsonResponse.members[0].created_at.should.be.an.instanceof(String);
jsonResponse.meta.pagination.should.have.property('page', 1);
jsonResponse.meta.pagination.should.have.property('limit', 15);
jsonResponse.meta.pagination.should.have.property('pages', 1);
jsonResponse.meta.pagination.should.have.property('total', 8);
jsonResponse.meta.pagination.should.have.property('next', null);
jsonResponse.meta.pagination.should.have.property('prev', null);
});
it('Can browse with filter', async function () {
const res = await request
.get(localUtils.API.getApiQuery('members/?filter=label:label-1'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.members);
jsonResponse.members.should.have.length(1);
localUtils.API.checkResponse(jsonResponse, 'members');
localUtils.API.checkResponse(jsonResponse.members[0], 'member', 'subscriptions');
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
});
it('Can browse with search', async function () {
const res = await request
.get(localUtils.API.getApiQuery('members/?search=member1'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.members);
jsonResponse.members.should.have.length(1);
jsonResponse.members[0].email.should.equal('member1@test.com');
localUtils.API.checkResponse(jsonResponse, 'members');
localUtils.API.checkResponse(jsonResponse.members[0], 'member', 'subscriptions');
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
});
it('Can filter by paid status', async function () {
const res = await request
.get(localUtils.API.getApiQuery('members/?filter=status:paid'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.members);
jsonResponse.members.should.have.length(5);
jsonResponse.members[0].email.should.equal('with-product@test.com');
jsonResponse.members[1].email.should.equal('vip-paid@test.com');
localUtils.API.checkResponse(jsonResponse, 'members');
localUtils.API.checkResponse(jsonResponse.members[0], 'member', 'subscriptions');
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
});
it('Can read', async function () {
const res = await request
.get(localUtils.API.getApiQuery(`members/${testUtils.DataGenerator.Content.members[0].id}/`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.members);
jsonResponse.members.should.have.length(1);
localUtils.API.checkResponse(jsonResponse.members[0], 'member', ['subscriptions', 'products']);
});
it('Can read and include email_recipients', async function () {
const res = await request
.get(localUtils.API.getApiQuery(`members/${testUtils.DataGenerator.Content.members[0].id}/?include=email_recipients`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.members);
jsonResponse.members.should.have.length(1);
localUtils.API.checkResponse(jsonResponse.members[0], 'member', ['subscriptions', 'email_recipients', 'products']);
jsonResponse.members[0].email_recipients.length.should.equal(1);
localUtils.API.checkResponse(jsonResponse.members[0].email_recipients[0], 'email_recipient', ['email']);
localUtils.API.checkResponse(jsonResponse.members[0].email_recipients[0].email, 'email');
});
it('Can add', async function () {
const member = {
name: 'test',
email: 'memberTestAdd@test.com',
note: 'test note',
subscribed: false,
labels: ['test-label']
};
const res = await request
.post(localUtils.API.getApiQuery(`members/`))
.send({members: [member]})
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201);
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.members);
jsonResponse.members.should.have.length(1);
jsonResponse.members[0].name.should.equal(member.name);
jsonResponse.members[0].email.should.equal(member.email);
jsonResponse.members[0].note.should.equal(member.note);
jsonResponse.members[0].subscribed.should.equal(member.subscribed);
testUtils.API.isISO8601(jsonResponse.members[0].created_at).should.be.true();
jsonResponse.members[0].labels.length.should.equal(1);
jsonResponse.members[0].labels[0].name.should.equal('test-label');
should.exist(res.headers.location);
res.headers.location.should.equal(`http://127.0.0.1:2369${localUtils.API.getApiQuery('members/')}${res.body.members[0].id}/`);
await request
.post(localUtils.API.getApiQuery(`members/`))
.send({members: [member]})
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(422);
});
it('Can add complimentary subscription', async function () {
const stripeService = require('../../../core/server/services/stripe');
stripeService.api._configured = true;
const fakePrice = {
id: 'price_1',
product: '',
active: true,
nickname: 'Complimentary',
unit_amount: 0,
currency: 'USD',
type: 'recurring',
recurring: {
interval: 'year'
}
};
const fakeSubscription = {
id: 'sub_1',
customer: 'cus_1',
status: 'active',
cancel_at_period_end: false,
metadata: {},
current_period_end: Date.now() / 1000,
start_date: Date.now() / 1000,
plan: fakePrice,
items: {
data: [{
price: fakePrice
}]
}
};
sinon.stub(stripeService.api, 'createCustomer').callsFake(async function (data) {
return {
id: 'cus_1',
email: data.email
};
});
sinon.stub(stripeService.api, 'createPrice').resolves(fakePrice);
sinon.stub(stripeService.api, 'createSubscription').resolves(fakeSubscription);
sinon.stub(stripeService.api, 'getSubscription').resolves(fakeSubscription);
const initialMember = {
name: 'Name',
email: 'compedtest@test.com',
subscribed: true
};
const compedPayload = {
comped: true
};
const res = await request
.post(localUtils.API.getApiQuery(`members/`))
.send({members: [initialMember]})
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201);
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.members);
jsonResponse.members.should.have.length(1);
should.exist(res.headers.location);
res.headers.location.should.equal(`http://127.0.0.1:2369${localUtils.API.getApiQuery('members/')}${res.body.members[0].id}/`);
const newMember = jsonResponse.members[0];
const res2 = await request
.put(localUtils.API.getApiQuery(`members/${newMember.id}/`))
.send({members: [compedPayload]})
.set('Origin', config.get('url'))
.expect(({body}) => {
body.should.have.property('members');
body.members.should.have.length(1);
})
.expect(200);
should.not.exist(res2.headers['x-cache-invalidate']);
const jsonResponse2 = res2.body;
localUtils.API.checkResponse(jsonResponse2.members[0], 'member', ['subscriptions', 'products']);
const member = jsonResponse2.members[0];
should.equal(member.status, 'comped');
should.equal(member.subscriptions.length, 1);
stripeService.api._configured = false;
});
it('Can edit by id', async function () {
const memberToChange = {
name: 'change me',
email: 'member2Change@test.com',
note: 'initial note',
subscribed: true
};
const memberChanged = {
name: 'changed',
email: 'cantChangeMe@test.com',
note: 'edited note',
subscribed: false
};
const res = await request
.post(localUtils.API.getApiQuery(`members/`))
.send({members: [memberToChange]})
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201);
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.members);
jsonResponse.members.should.have.length(1);
should.exist(res.headers.location);
res.headers.location.should.equal(`http://127.0.0.1:2369${localUtils.API.getApiQuery('members/')}${res.body.members[0].id}/`);
const newMember = jsonResponse.members[0];
const res2 = await request
.put(localUtils.API.getApiQuery(`members/${newMember.id}/`))
.send({members: [memberChanged]})
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
should.not.exist(res2.headers['x-cache-invalidate']);
const jsonResponse2 = res2.body;
should.exist(jsonResponse2);
should.exist(jsonResponse2.members);
jsonResponse2.members.should.have.length(1);
localUtils.API.checkResponse(jsonResponse2.members[0], 'member', ['subscriptions', 'products']);
jsonResponse2.members[0].name.should.equal(memberChanged.name);
jsonResponse2.members[0].email.should.equal(memberChanged.email);
jsonResponse2.members[0].email.should.not.equal(memberToChange.email);
jsonResponse2.members[0].note.should.equal(memberChanged.note);
jsonResponse2.members[0].subscribed.should.equal(memberChanged.subscribed);
});
it('Can destroy', async function () {
const member = {
name: 'test',
email: 'memberTestDestroy@test.com'
};
const res = await request
.post(localUtils.API.getApiQuery(`members/`))
.send({members: [member]})
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201);
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.members);
const newMember = jsonResponse.members[0];
await request
.delete(localUtils.API.getApiQuery(`members/${newMember.id}`))
.set('Origin', config.get('url'))
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(204);
await request
.get(localUtils.API.getApiQuery(`members/${newMember.id}/`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404);
});
it('Can export CSV', async function () {
const res = await request
.get(localUtils.API.getApiQuery(`members/upload/`))
.set('Origin', config.get('url'))
.expect('Content-Type', /text\/csv/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
should.not.exist(res.headers['x-cache-invalidate']);
res.headers['content-disposition'].should.match(/Attachment;\sfilename="members/);
res.text.should.match(/id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at/);
const csv = Papa.parse(res.text, {header: true});
should.exist(csv.data.find(row => row.name === 'Mr Egg'));
should.exist(csv.data.find(row => row.name === 'Egon Spengler'));
should.exist(csv.data.find(row => row.name === 'Ray Stantz'));
should.exist(csv.data.find(row => row.email === 'member2@test.com'));
});
it('Can export a filtered CSV', async function () {
const res = await request
.get(localUtils.API.getApiQuery(`members/upload/?search=Egg`))
.set('Origin', config.get('url'))
.expect('Content-Type', /text\/csv/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
should.not.exist(res.headers['x-cache-invalidate']);
res.headers['content-disposition'].should.match(/Attachment;\sfilename="members/);
res.text.should.match(/id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at/);
const csv = Papa.parse(res.text, {header: true});
should.exist(csv.data.find(row => row.name === 'Mr Egg'));
should.not.exist(csv.data.find(row => row.name === 'Egon Spengler'));
should.not.exist(csv.data.find(row => row.name === 'Ray Stantz'));
should.not.exist(csv.data.find(row => row.email === 'member2@test.com'));
});
it('Can import CSV', async function () {
const res = await request
.post(localUtils.API.getApiQuery(`members/upload/`))
.attach('membersfile', path.join(__dirname, '/../../utils/fixtures/csv/valid-members-import.csv'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201);
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.meta);
should.exist(jsonResponse.meta.stats);
jsonResponse.meta.stats.imported.should.equal(2);
jsonResponse.meta.stats.invalid.length.should.equal(0);
jsonResponse.meta.import_label.name.should.match(/^Import \d{4}-\d{2}-\d{2} \d{2}:\d{2}$/);
const importLabel = jsonResponse.meta.import_label;
// check that members had the auto-generated label attached
const res2 = await request.get(localUtils.API.getApiQuery(`members/?filter=label:${importLabel.slug}`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
const jsonResponse2 = res2.body;
should.exist(jsonResponse2);
should.exist(jsonResponse2.members);
jsonResponse2.members.should.have.length(2);
const importedMember1 = jsonResponse2.members.find(m => m.email === 'jbloggs@example.com');
should.exist(importedMember1);
importedMember1.name.should.equal('joe');
should(importedMember1.note).equal(null);
importedMember1.subscribed.should.equal(true);
importedMember1.labels.length.should.equal(1);
testUtils.API.isISO8601(importedMember1.created_at).should.be.true();
importedMember1.comped.should.equal(false);
importedMember1.subscriptions.should.not.be.undefined();
importedMember1.subscriptions.length.should.equal(0);
const importedMember2 = jsonResponse2.members.find(m => m.email === 'test@example.com');
should.exist(importedMember2);
importedMember2.name.should.equal('test');
should(importedMember2.note).equal('test note');
importedMember2.subscribed.should.equal(false);
importedMember2.labels.length.should.equal(2);
testUtils.API.isISO8601(importedMember2.created_at).should.be.true();
importedMember2.created_at.should.equal('1991-10-02T20:30:31.000Z');
importedMember2.comped.should.equal(false);
importedMember2.subscriptions.should.not.be.undefined();
importedMember2.subscriptions.length.should.equal(0);
});
it('Can fetch member counts stats', async function () {
const res = await request
.get(localUtils.API.getApiQuery('members/stats/count/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.total);
should.exist(jsonResponse.resource);
should.exist(jsonResponse.data);
const data = jsonResponse.data;
// 2 from above posts, 2 from above import
data[data.length - 1].free.should.equal(4);
data[data.length - 1].paid.should.equal(0);
// 1 from the comped test
data[data.length - 1].comped.should.equal(1);
});
it('Can import CSV and bulk destroy via auto-added label', function () {
// HACK: mock dates otherwise we'll often get unexpected members appearing
// from previous tests with the same import label due to auto-generated
// import labels only including minutes
sinon.stub(Date, 'now').returns(new Date('2021-03-30T17:21:00.000Z'));
// import our dummy data for deletion
return request
.post(localUtils.API.getApiQuery(`members/upload/`))
.attach('membersfile', path.join(__dirname, '/../../utils/fixtures/csv/valid-members-for-bulk-delete.csv'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.meta);
should.exist(jsonResponse.meta.stats);
should.exist(jsonResponse.meta.import_label);
jsonResponse.meta.stats.imported.should.equal(8);
return jsonResponse.meta.import_label;
})
.then((importLabel) => {
// check that the import worked by checking browse response with filter
return request.get(localUtils.API.getApiQuery(`members/?filter=label:${importLabel.slug}`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.members);
jsonResponse.members.should.have.length(8);
})
.then(() => importLabel);
})
.then((importLabel) => {
// perform the bulk delete
return request
.del(localUtils.API.getApiQuery(`members/?filter=label:'${importLabel.slug}'`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.meta);
should.exist(jsonResponse.meta.stats);
should.exist(jsonResponse.meta.stats.successful);
should.equal(jsonResponse.meta.stats.successful, 8);
})
.then(() => importLabel);
})
.then((importLabel) => {
// check that the bulk delete worked by checking browse response with filter
return request.get(localUtils.API.getApiQuery(`members/?filter=label:${importLabel.slug}`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.members);
jsonResponse.members.should.have.length(0);
});
});
});
it('Can bulk unsubscribe members with filter', async function () {
// import our dummy data for deletion
await request
.post(localUtils.API.getApiQuery(`members/upload/`))
.attach('membersfile', path.join(__dirname, '/../../utils/fixtures/csv/members-for-bulk-unsubscribe.csv'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private);
const browseResponse = await request
.get(localUtils.API.getApiQuery('members/?filter=label:bulk-unsubscribe-test'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
browseResponse.body.members.should.have.length(8);
const allMembersSubscribed = browseResponse.body.members.every((member) => {
return member.subscribed;
});
should.ok(allMembersSubscribed);
const bulkUnsubscribeResponse = await request
.put(localUtils.API.getApiQuery('members/bulk/?filter=label:bulk-unsubscribe-test'))
.set('Origin', config.get('url'))
.send({
bulk: {
action: 'unsubscribe'
}
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
should.exist(bulkUnsubscribeResponse.body.bulk);
should.exist(bulkUnsubscribeResponse.body.bulk.meta);
should.exist(bulkUnsubscribeResponse.body.bulk.meta.stats);
should.exist(bulkUnsubscribeResponse.body.bulk.meta.stats.successful);
should.equal(bulkUnsubscribeResponse.body.bulk.meta.stats.successful, 8);
const postUnsubscribeBrowseResponse = await request
.get(localUtils.API.getApiQuery('members/?filter=label:bulk-unsubscribe-test'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
postUnsubscribeBrowseResponse.body.members.should.have.length(8);
const allMembersUnsubscribed = postUnsubscribeBrowseResponse.body.members.every((member) => {
return !member.subscribed;
});
should.ok(allMembersUnsubscribed);
});
it('Can bulk add and remove labels to members with filter', async function () {
// import our dummy data for deletion
await request
.post(localUtils.API.getApiQuery('members/upload/'))
.attach('membersfile', path.join(__dirname, '/../../utils/fixtures/csv/members-for-bulk-add-labels.csv'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private);
const newLabelResponse = await request
.post(localUtils.API.getApiQuery('labels'))
.set('Origin', config.get('url'))
.send({
labels: [{
name: 'Awesome Label For Testing Bulk Add'
}]
});
const labelToAdd = newLabelResponse.body.labels[0];
const bulkAddLabelResponse = await request
.put(localUtils.API.getApiQuery('members/bulk/?filter=label:bulk-add-labels-test'))
.set('Origin', config.get('url'))
.send({
bulk: {
action: 'addLabel',
meta: {
label: labelToAdd
}
}
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
should.exist(bulkAddLabelResponse.body.bulk);
should.exist(bulkAddLabelResponse.body.bulk.meta);
should.exist(bulkAddLabelResponse.body.bulk.meta.stats);
should.exist(bulkAddLabelResponse.body.bulk.meta.stats.successful);
should.equal(bulkAddLabelResponse.body.bulk.meta.stats.successful, 8);
const postLabelAddBrowseResponse = await request
.get(localUtils.API.getApiQuery(`members/?filter=label:${labelToAdd.slug}`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
postLabelAddBrowseResponse.body.members.should.have.length(8);
const labelToRemove = newLabelResponse.body.labels[0];
const bulkRemoveLabelResponse = await request
.put(localUtils.API.getApiQuery('members/bulk/?filter=label:bulk-add-labels-test'))
.set('Origin', config.get('url'))
.send({
bulk: {
action: 'removeLabel',
meta: {
label: labelToRemove
}
}
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
should.exist(bulkRemoveLabelResponse.body.bulk);
should.exist(bulkRemoveLabelResponse.body.bulk.meta);
should.exist(bulkRemoveLabelResponse.body.bulk.meta.stats);
should.exist(bulkRemoveLabelResponse.body.bulk.meta.stats.successful);
should.equal(bulkRemoveLabelResponse.body.bulk.meta.stats.successful, 8);
const postLabelRemoveBrowseResponse = await request
.get(localUtils.API.getApiQuery(`members/?filter=label:${labelToRemove.slug}`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
postLabelRemoveBrowseResponse.body.members.should.have.length(0);
});
});

View file

@ -7,18 +7,28 @@ const localUtils = require('./utils');
const config = require('../../../core/shared/config');
const {mockManager} = require('../../utils/e2e-framework');
const models = require('../../../core/server/models');
let request;
async function getNewsletters() {
return (await models.Newsletter.findAll({filter: 'status:active'})).models;
}
describe('Members Importer API', function () {
let newsletters;
before(async function () {
await localUtils.startGhost();
request = supertest.agent(config.get('url'));
await localUtils.doAuth(request, 'members');
await localUtils.doAuth(request, 'newsletters', 'members:newsletters');
newsletters = await getNewsletters();
});
beforeEach(function () {
mockManager.mockLabsEnabled('multipleProducts');
mockManager.mockLabsEnabled('multipleNewsletters');
});
afterEach(function () {
@ -26,6 +36,9 @@ describe('Members Importer API', function () {
});
it('Can import CSV', async function () {
const filteredNewsletters = newsletters.filter(n => n.get('subscribe_on_signup'));
filteredNewsletters.length.should.be.greaterThan(0, 'For this test to work, we need at least one newsletter fixture with subscribe_on_signup = true');
const res = await request
.post(localUtils.API.getApiQuery(`members/upload/`))
.attach('membersfile', path.join(__dirname, '/../../utils/fixtures/csv/valid-members-import.csv'))
@ -64,6 +77,7 @@ describe('Members Importer API', function () {
importedMember1.name.should.equal('joe');
should(importedMember1.note).equal(null);
importedMember1.subscribed.should.equal(true);
importedMember1.newsletters.length.should.equal(filteredNewsletters.length);
importedMember1.labels.length.should.equal(1);
testUtils.API.isISO8601(importedMember1.created_at).should.be.true();
importedMember1.comped.should.equal(false);
@ -75,6 +89,7 @@ describe('Members Importer API', function () {
importedMember2.name.should.equal('test');
should(importedMember2.note).equal('test note');
importedMember2.subscribed.should.equal(false);
importedMember2.newsletters.length.should.equal(0);
importedMember2.labels.length.should.equal(2);
testUtils.API.isISO8601(importedMember2.created_at).should.be.true();
importedMember2.created_at.should.equal('1991-10-02T20:30:31.000Z');
@ -163,6 +178,9 @@ describe('Members Importer API', function () {
// });
it('Can bulk unsubscribe members with filter', async function () {
const filteredNewsletters = newsletters.filter(n => n.get('subscribe_on_signup'));
filteredNewsletters.length.should.be.greaterThan(0, 'For this test to work, we need at least one newsletter fixture with subscribe_on_signup = true');
// import our dummy data for deletion
await request
.post(localUtils.API.getApiQuery(`members/upload/`))
@ -180,7 +198,7 @@ describe('Members Importer API', function () {
browseResponse.body.members.should.have.length(8);
const allMembersSubscribed = browseResponse.body.members.every((member) => {
return member.subscribed;
return member.subscribed && member.newsletters.length > 0;
});
should.ok(allMembersSubscribed);
@ -201,7 +219,7 @@ describe('Members Importer API', function () {
should.exist(bulkUnsubscribeResponse.body.bulk.meta);
should.exist(bulkUnsubscribeResponse.body.bulk.meta.stats);
should.exist(bulkUnsubscribeResponse.body.bulk.meta.stats.successful);
should.equal(bulkUnsubscribeResponse.body.bulk.meta.stats.successful, 8);
should.equal(bulkUnsubscribeResponse.body.bulk.meta.stats.successful, 8 * filteredNewsletters.length);
const postUnsubscribeBrowseResponse = await request
.get(localUtils.API.getApiQuery('members/?filter=label:bulk-unsubscribe-test'))
@ -212,7 +230,7 @@ describe('Members Importer API', function () {
postUnsubscribeBrowseResponse.body.members.should.have.length(8);
const allMembersUnsubscribed = postUnsubscribeBrowseResponse.body.members.every((member) => {
return !member.subscribed;
return member.newsletters.length === 0;
});
should.ok(allMembersUnsubscribed);

146
yarn.lock
View file

@ -1892,7 +1892,12 @@
"@tryghost/root-utils" "^0.3.14"
debug "^4.3.1"
"@tryghost/domain-events@0.1.9", "@tryghost/domain-events@^0.1.9":
"@tryghost/domain-events@0.1.10", "@tryghost/domain-events@^0.1.10":
version "0.1.10"
resolved "https://registry.yarnpkg.com/@tryghost/domain-events/-/domain-events-0.1.10.tgz#1a657c7964b928ac63e7ada87902d6fa5d4f5a7a"
integrity sha512-C6xqg8VC5KgfgZ0X4zMUcoRBEDk7DS8xSepbIfZx61UZHbJRUMfbZ+AklGdnNQmQFxBc3eUCLAOByF6k1HXvTQ==
"@tryghost/domain-events@^0.1.9":
version "0.1.9"
resolved "https://registry.yarnpkg.com/@tryghost/domain-events/-/domain-events-0.1.9.tgz#f2de5189df2238bb72a53a76abcded0d84ae6fa7"
integrity sha512-n2FEA5xBQBCHq02CNBMh8BkWV7zdy9yp/Y4dGWc2QQ58lcNTgSBqZBaPpfPURgTGJbLqO+xsNQgU0LYvUN9JTQ==
@ -1957,10 +1962,10 @@
utils-copy-error "^1.0.1"
uuid "^8.3.2"
"@tryghost/express-dynamic-redirects@0.2.8":
version "0.2.8"
resolved "https://registry.yarnpkg.com/@tryghost/express-dynamic-redirects/-/express-dynamic-redirects-0.2.8.tgz#67b4f19c6210734e0226e6c8f18c0ad7ce2a75a2"
integrity sha512-kg1vDQ0E8hVJTwu3OYebYdI1KD3wwTwTfRv1dXEA7r8JSMdJw/LdlxJ39WBiQObmCuHYu2nsnTf3/Ccah7Ew+w==
"@tryghost/express-dynamic-redirects@0.2.9":
version "0.2.9"
resolved "https://registry.yarnpkg.com/@tryghost/express-dynamic-redirects/-/express-dynamic-redirects-0.2.9.tgz#62094c67bce0c5e6918a292e8a7e0d8d4c72aa16"
integrity sha512-BguBNvq+E/wEiJ+FJv5C5g0Q9Xj4PZEwy5Zs7GWkqUFGiDB3Evfpsj02MucNyuC1ysxz1FwpEyr2NRxk65ZkQg==
"@tryghost/express-test@0.10.0":
version "0.10.0"
@ -2117,55 +2122,60 @@
json-stringify-safe "^5.0.1"
lodash "^4.17.21"
"@tryghost/magic-link@1.0.21", "@tryghost/magic-link@^1.0.21":
version "1.0.21"
resolved "https://registry.yarnpkg.com/@tryghost/magic-link/-/magic-link-1.0.21.tgz#6b1fbcd007d5ba64bd2b0d47a1ace6c1d50658f2"
integrity sha512-d57N8356H0w4in3Zb+IpzgnOO1IkYW+D/agZPjilyQcB11Hj+pJq8xhw2S0Ra4wSrzO2pbTBxEC/yceufiryEg==
"@tryghost/magic-link@1.0.22", "@tryghost/magic-link@^1.0.22":
version "1.0.22"
resolved "https://registry.yarnpkg.com/@tryghost/magic-link/-/magic-link-1.0.22.tgz#1bfa1fa93efb0d493f242ed400e8b663fc0c7e60"
integrity sha512-d+uut21jb3iW6x7yqMvNcd7nBT5Y2VMTu4pFVA+eXkFQSQxFY+AD3qSeJxkSXp5KxFpKOZRRJycrG2Ppsh8TaA==
dependencies:
bluebird "^3.5.5"
jsonwebtoken "^8.5.1"
lodash "^4.17.15"
"@tryghost/member-analytics-service@^0.1.11":
version "0.1.11"
resolved "https://registry.yarnpkg.com/@tryghost/member-analytics-service/-/member-analytics-service-0.1.11.tgz#99da7eb47678aa0770ac2aef7c99fdd3e5a648b9"
integrity sha512-IdddDTbpHp7hLJWIELCBrgkGDr6duZsYK7HPkjW51L/5cqj3I0XjLCbjQavF4hXFhVZ3hekiHq61v7agWPRCXA==
"@tryghost/member-analytics-service@^0.1.12":
version "0.1.12"
resolved "https://registry.yarnpkg.com/@tryghost/member-analytics-service/-/member-analytics-service-0.1.12.tgz#dbf3ef1ee7e7732ca9e162a0b10608207ebf9032"
integrity sha512-Mat32Q1rIo1N5WvjzJsyterBzVhEkSkAbsCa8nXOPsmCk/rRmOY9aO/KD6B6wQB84whMgK0Y2+QpYQk2lLmVtQ==
dependencies:
"@tryghost/domain-events" "^0.1.9"
"@tryghost/domain-events" "^0.1.10"
"@tryghost/errors" "^1.0.0"
"@tryghost/member-events" "^0.4.1"
"@tryghost/member-events" "^0.4.2"
"@tryghost/tpl" "^0.1.4"
bson-objectid "^2.0.1"
"@tryghost/member-events@0.4.1", "@tryghost/member-events@^0.4.1":
"@tryghost/member-events@0.4.2", "@tryghost/member-events@^0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@tryghost/member-events/-/member-events-0.4.2.tgz#d1266abad38aa0b8f9b9f3395b083971e85221bf"
integrity sha512-4VlGlB9uq3xxgeW3OXx9//Yb1kiRm7HbVVGiBJ9LMMjeoKZnpUQA9zHyY5FUK4Aolvz/wyxjb4Ci7JNjvoMrJg==
"@tryghost/member-events@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@tryghost/member-events/-/member-events-0.4.1.tgz#d75c183e29c1d186bc8a584a1f05b6e7c05c23b0"
integrity sha512-eYsDWrsLbdX5alZI9hsxsswN0XnlkOH6AvOmzeaX17pxDqzAhG7DN9rs0lDH3Ov3aniRfHc2UukcQ65oJbvA1A==
"@tryghost/members-analytics-ingress@^0.1.12":
version "0.1.12"
resolved "https://registry.yarnpkg.com/@tryghost/members-analytics-ingress/-/members-analytics-ingress-0.1.12.tgz#517bb68636cd093ec1fa725fc0ac9605efce4a1e"
integrity sha512-ljf5/7QwnHZDpF2aAQLneML7ytk8TELYOwLvX+ye6FLhqRHNzEeas1baFgpfJl+Iyf3u4ZQU9NWnoj09yFgC3A==
"@tryghost/members-analytics-ingress@^0.1.13":
version "0.1.13"
resolved "https://registry.yarnpkg.com/@tryghost/members-analytics-ingress/-/members-analytics-ingress-0.1.13.tgz#6ab72155aea2defe44b88c79520d645f6dd54c78"
integrity sha512-XaEll3ea0yXJofdiP1a2I6GWnc+kPcoMe8lWZ3r8qOVtfD7Yynkd3sL7JBC9I3RRihCQYxmKka7+AOTh69V+XA==
dependencies:
"@tryghost/domain-events" "^0.1.9"
"@tryghost/member-events" "^0.4.1"
"@tryghost/domain-events" "^0.1.10"
"@tryghost/member-events" "^0.4.2"
"@tryghost/members-api@6.1.0":
version "6.1.0"
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-6.1.0.tgz#60ee22f62ea0def89815a3563243d9e1840e8a09"
integrity sha512-XnfO59JrtN4PySwlfdygnXg6o7+4nVwwSHvhCXBlUEgrszQhT3mQXMSyGvRM4gHg3zOhKOVvzYWPh5Nboos/nA==
"@tryghost/members-api@6.2.2":
version "6.2.2"
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-6.2.2.tgz#ea888882edde3ccce811a481aca11b5bb8e86ea6"
integrity sha512-fDlrNkTm1DMH7fDlxEZQxjQSx+HJzw7g188hN948l52QdxB8sSdBcKl5+0e9WnOvV6EvbVTAq4pvm+94CsIE8w==
dependencies:
"@nexes/nql" "^0.6.0"
"@tryghost/debug" "^0.1.2"
"@tryghost/domain-events" "^0.1.9"
"@tryghost/domain-events" "^0.1.10"
"@tryghost/errors" "^1.1.1"
"@tryghost/logging" "^2.0.0"
"@tryghost/magic-link" "^1.0.21"
"@tryghost/member-analytics-service" "^0.1.11"
"@tryghost/member-events" "^0.4.1"
"@tryghost/members-analytics-ingress" "^0.1.12"
"@tryghost/members-payments" "^0.3.1"
"@tryghost/members-stripe-service" "^0.10.0"
"@tryghost/magic-link" "^1.0.22"
"@tryghost/member-analytics-service" "^0.1.12"
"@tryghost/member-events" "^0.4.2"
"@tryghost/members-analytics-ingress" "^0.1.13"
"@tryghost/members-payments" "^0.3.2"
"@tryghost/members-stripe-service" "^0.10.1"
"@tryghost/tpl" "^0.1.2"
"@types/jsonwebtoken" "^8.5.1"
bluebird "^3.5.4"
@ -2178,10 +2188,10 @@
lodash "^4.17.11"
node-jose "^2.0.0"
"@tryghost/members-csv@^1.2.10":
version "1.2.10"
resolved "https://registry.yarnpkg.com/@tryghost/members-csv/-/members-csv-1.2.10.tgz#b69b41bc6eaacb0f6b53f959cdf8dc8ec5d156b3"
integrity sha512-D7OVxkcOQ7/bD0f/0XGd4yXEc8+R8HVfxzRckov7DERXNlfa0EBdXMCijWKwfIbGYC9BJLZxykpaCtE7krN5Ww==
"@tryghost/members-csv@^1.2.11":
version "1.2.11"
resolved "https://registry.yarnpkg.com/@tryghost/members-csv/-/members-csv-1.2.11.tgz#dd2eaec417a2671786e8b827fa1dd64f4edd2a4c"
integrity sha512-qxbcqnJSja3L/5UOlBIsGtySyHvpYNyM+qmv8EWiLSBcrE2cPorhlwZZI9BUM4ZW2FQ90cEihgADoueNdniX0Q==
dependencies:
bluebird "^3.7.2"
fs-extra "^10.0.0"
@ -2198,36 +2208,36 @@
"@tryghost/member-events" "^0.4.1"
moment-timezone "^0.5.34"
"@tryghost/members-importer@0.5.8":
version "0.5.8"
resolved "https://registry.yarnpkg.com/@tryghost/members-importer/-/members-importer-0.5.8.tgz#afb9aa0ff8d1aa113591116d1a82dbe13ab13b58"
integrity sha512-AKT4G8IOpQZJxol6wozkpDtf9HVb1kvncMEZuk4nq7V92VHzOP6M88UJ98GUwtmDkAvLZ5+uilkkiVIFuRZVoA==
"@tryghost/members-importer@0.5.9":
version "0.5.9"
resolved "https://registry.yarnpkg.com/@tryghost/members-importer/-/members-importer-0.5.9.tgz#c2cca9b414a252f088d76ad01d2b9643f16176ec"
integrity sha512-wNmBnizOUIqmfn6LCuBQJW+A99UyDv63jW9Juu2rEKkmRQo6VOZemcCGgKGZP2yFYB3PthckMoAfteBkUNyfLw==
dependencies:
"@tryghost/errors" "^1.0.0"
"@tryghost/members-csv" "^1.2.10"
"@tryghost/members-csv" "^1.2.11"
"@tryghost/tpl" "^0.1.3"
moment-timezone "^0.5.23"
"@tryghost/members-offers@0.11.1", "@tryghost/members-offers@^0.11.1":
version "0.11.1"
resolved "https://registry.yarnpkg.com/@tryghost/members-offers/-/members-offers-0.11.1.tgz#f9f4704b187aabaa54b641cc954415c50cf0165d"
integrity sha512-cA4/AVvL6SH4ATvud75u/S1UiBtVZa0/0V4gX5V/Ncq11UMXb/kZV20cRSIF/5iuaBtrfrCfaUvpXQfXZylRXQ==
"@tryghost/members-offers@0.11.2", "@tryghost/members-offers@^0.11.2":
version "0.11.2"
resolved "https://registry.yarnpkg.com/@tryghost/members-offers/-/members-offers-0.11.2.tgz#8c53344055fbd8dbec203f166ce74a12e0085aec"
integrity sha512-sAhrHm3ShFtf+5Tz8MfALriDPgdgKISRFXZ9j4bldu5uZrjwSJMWJwH3z/wtwdWix9yL1mNxH9EE+hjPMZzAzg==
dependencies:
"@nexes/mongo-utils" "^0.3.1"
"@tryghost/string" "^0.1.20"
"@tryghost/members-payments@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@tryghost/members-payments/-/members-payments-0.3.1.tgz#cd52c87fcb02fbe86dbda9c843a5946f9dad40e0"
integrity sha512-12aTBMHb9JcxC6rsGSeIQnwsEDNVpCtcp+ail9W7ghcBjbLTqf6menWq++DavsohFfzStwI3D5reKstA2M68+A==
"@tryghost/members-payments@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@tryghost/members-payments/-/members-payments-0.3.2.tgz#f48e5a61a726d3a0f233925ecc1eb0643172e539"
integrity sha512-qbxw9oix49+iFzHHtO5M2dUcde9AApc3FWR0SFCIZdi08cQnQnqyEzg2T6QtQHH+k8/5WMZBewHzHd2tXlGfBA==
dependencies:
"@tryghost/domain-events" "^0.1.9"
"@tryghost/members-offers" "^0.11.1"
"@tryghost/domain-events" "^0.1.10"
"@tryghost/members-offers" "^0.11.2"
"@tryghost/members-ssr@1.0.23":
version "1.0.23"
resolved "https://registry.yarnpkg.com/@tryghost/members-ssr/-/members-ssr-1.0.23.tgz#7e42d0db36a564437a20f7d0419083204eafac9a"
integrity sha512-yowIa1l/LnEAG7C7szV987hRekJwlZMt77JjOvCnHH94aQnCjr2KGKuSy68oBG2eKlUmXO4Iaest0iuEpWEcSQ==
"@tryghost/members-ssr@1.0.24":
version "1.0.24"
resolved "https://registry.yarnpkg.com/@tryghost/members-ssr/-/members-ssr-1.0.24.tgz#12c2d8af366754a1eb3e71f7ffb17e3859526d7c"
integrity sha512-8LBkE1uj/kRkoY9C+hrVsa2gk4bfkuUI+bOfsF9XQvWjTOQlbY/KFoEN5twxhiw90zFRMqR8bWzfL4EEaaILVg==
dependencies:
"@tryghost/debug" "^0.1.2"
"@tryghost/errors" "^1.1.0"
@ -2237,16 +2247,16 @@
jsonwebtoken "^8.5.1"
lodash "^4.17.11"
"@tryghost/members-stripe-service@0.10.0", "@tryghost/members-stripe-service@^0.10.0":
version "0.10.0"
resolved "https://registry.yarnpkg.com/@tryghost/members-stripe-service/-/members-stripe-service-0.10.0.tgz#e63b672fbb9f30a34ee2be7feb1452a16ae44734"
integrity sha512-ViXwAORs5RF15Q+17zUJw16khppnTF3UtcRgiNX8NXNozFYuIHGLd3A9nKbBgZyPGxEOcBF8u/qBZ1apjEVT2g==
"@tryghost/members-stripe-service@0.10.1", "@tryghost/members-stripe-service@^0.10.1":
version "0.10.1"
resolved "https://registry.yarnpkg.com/@tryghost/members-stripe-service/-/members-stripe-service-0.10.1.tgz#2877ccb7f2a318f0fcc24a4e6a4d30d5116de5ce"
integrity sha512-bbtyLAMDivwFA10KkuxsTwBzCSboyu1f/vqbmolSpaHxSDg8qxIr8fAeJFZWByVrJKfUD0e3IgMRYJl1jW0Ogw==
dependencies:
"@tryghost/debug" "^0.1.4"
"@tryghost/domain-events" "^0.1.9"
"@tryghost/domain-events" "^0.1.10"
"@tryghost/errors" "^1.2.5"
"@tryghost/logging" "^2.0.5"
"@tryghost/member-events" "^0.4.1"
"@tryghost/member-events" "^0.4.2"
leaky-bucket "^2.2.0"
lodash "^4.17.21"
stripe "^8.174.0"
@ -2504,13 +2514,13 @@
moment-timezone "^0.5.23"
validator "7.2.0"
"@tryghost/verification-trigger@0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@tryghost/verification-trigger/-/verification-trigger-0.2.0.tgz#cf9d22ea59be6fca8f8a54464663d8177e063e37"
integrity sha512-qsz7IWJ+LbKmSD6JeEkoa/ZCRMFjwUHwIfRWp2mTNf7WW+dA13T+4VsE14lxRtOsMnD9A12PqWIUQB6H6ef2UQ==
"@tryghost/verification-trigger@0.2.1":
version "0.2.1"
resolved "https://registry.yarnpkg.com/@tryghost/verification-trigger/-/verification-trigger-0.2.1.tgz#8b371fee47dec57bfeb8ab61a4698aaae4d17974"
integrity sha512-ox47qz+aiUC70dy2oOqpuZ1AGHLawFB1DDE76US4vWVAEV0K8Ny+k/vR8Qtt5r8lVN+G8tFpsf8NtELFE2npdw==
dependencies:
"@tryghost/domain-events" "^0.1.9"
"@tryghost/member-events" "^0.4.1"
"@tryghost/domain-events" "^0.1.10"
"@tryghost/member-events" "^0.4.2"
"@tryghost/version-notifications-data-service@0.1.0":
version "0.1.0"