mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-27 10:42:45 +03:00
Added member attributions to activity feed (#15283)
refs https://github.com/TryGhost/Team/issues/1833 refs https://github.com/TryGhost/Team/issues/1834 We've added the attribution property to subscription and signup events when the flag is enabled. The attributions resource is fetched by creating multiple relations on the model, rather than polymorphic as we ran into issues with that as they can't be nullable/optional. The parse-member-event structure has been updated to make it easier to work with, specifically `getObject` is only used when the event is clickable, and there is now a join property which makes it easier to join the action and the object.
This commit is contained in:
parent
2c60340a7d
commit
f124d142c9
@ -102,9 +102,8 @@
|
|||||||
<span class="gh-dashboard-list-subtext">
|
<span class="gh-dashboard-list-subtext">
|
||||||
{{capitalize-first-letter parsedEvent.action}}
|
{{capitalize-first-letter parsedEvent.action}}
|
||||||
{{#if parsedEvent.url}}
|
{{#if parsedEvent.url}}
|
||||||
|
{{parsedEvent.join}}
|
||||||
<a class="ghost-members-activity-object-link {{if (feature "memberAttribution") 'hidden'}}" href="{{parsedEvent.url}}" target="_blank" rel="noopener noreferrer">{{parsedEvent.object}}</a>
|
<a class="ghost-members-activity-object-link {{if (feature "memberAttribution") 'hidden'}}" href="{{parsedEvent.url}}" target="_blank" rel="noopener noreferrer">{{parsedEvent.object}}</a>
|
||||||
{{else}}
|
|
||||||
{{parsedEvent.object}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if parsedEvent.info}}
|
{{#if parsedEvent.info}}
|
||||||
<span class="highlight">{{parsedEvent.info}}</span>
|
<span class="highlight">{{parsedEvent.info}}</span>
|
||||||
|
@ -27,9 +27,8 @@
|
|||||||
<span class="gh-members-activity-description">
|
<span class="gh-members-activity-description">
|
||||||
{{capitalize-first-letter event.action}}
|
{{capitalize-first-letter event.action}}
|
||||||
{{#if event.url}}
|
{{#if event.url}}
|
||||||
|
{{event.join}}
|
||||||
<a class="ghost-members-activity-object-link" href="{{event.url}}" target="_blank" rel="noopener noreferrer">{{event.object}}</a>
|
<a class="ghost-members-activity-object-link" href="{{event.url}}" target="_blank" rel="noopener noreferrer">{{event.object}}</a>
|
||||||
{{else}}
|
|
||||||
{{event.object}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if event.email}}
|
{{#if event.email}}
|
||||||
<GhEmailPreviewLink @data={{event.email}} />
|
<GhEmailPreviewLink @data={{event.email}} />
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<GhMemberAvatar @member={{event.member}} @containerClass="w9 h9 mr3 flex-shrink-0" />
|
<GhMemberAvatar @member={{event.member}} @containerClass="w9 h9 mr3 flex-shrink-0" />
|
||||||
<div class="w-80">
|
<div class="w-80">
|
||||||
<h3 class="ma0 pa0 gh-members-list-name {{unless event.member.name "gh-members-name-noname"}}">{{or event.member.name event.member.email}}</h3>
|
<h3 class="ma0 pa0 gh-members-list-name {{unless event.member.name "gh-members-name-noname"}}">{{event.subject}}</h3>
|
||||||
{{#if event.member.name}}
|
{{#if event.member.name}}
|
||||||
<p class="ma0 pa0 middarkgrey f8 gh-members-list-email">{{event.member.email}}</p>
|
<p class="ma0 pa0 middarkgrey f8 gh-members-list-email">{{event.member.email}}</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@ -21,10 +21,9 @@
|
|||||||
<div class="gh-members-activity-event">
|
<div class="gh-members-activity-event">
|
||||||
<span class="gh-members-activity-description">
|
<span class="gh-members-activity-description">
|
||||||
{{capitalize-first-letter event.action}}
|
{{capitalize-first-letter event.action}}
|
||||||
{{#if event.url}}
|
{{#if (and event.url (not (feature "memberAttribution")))}}
|
||||||
<a class="ghost-members-activity-object-link {{if (feature "memberAttribution") 'hidden'}}" href="{{event.url}}" target="_blank" rel="noopener noreferrer">{{event.object}}</a>
|
{{event.join}}
|
||||||
{{else}}
|
<a class="ghost-members-activity-object-link" href="{{event.url}}" target="_blank" rel="noopener noreferrer">{{event.object}}</a>
|
||||||
{{event.object}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if event.email}}
|
{{#if event.email}}
|
||||||
<span class="{{if (feature "memberAttribution") 'hidden'}}"><GhEmailPreviewLink @data={{event.email}} /></span>
|
<span class="{{if (feature "memberAttribution") 'hidden'}}"><GhEmailPreviewLink @data={{event.email}} /></span>
|
||||||
|
@ -2,13 +2,15 @@ import moment from 'moment';
|
|||||||
import {getNonDecimal, getSymbol} from 'ghost-admin/utils/currency';
|
import {getNonDecimal, getSymbol} from 'ghost-admin/utils/currency';
|
||||||
|
|
||||||
export default function parseMemberEvent(event, hasMultipleNewsletters) {
|
export default function parseMemberEvent(event, hasMultipleNewsletters) {
|
||||||
let subject = event.data.member.name || event.data.member.email;
|
const subject = event.data.member.name || event.data.member.email;
|
||||||
let icon = getIcon(event);
|
const icon = getIcon(event);
|
||||||
let action = getAction(event);
|
const action = getAction(event, hasMultipleNewsletters);
|
||||||
let object = getObject(event, hasMultipleNewsletters);
|
const info = getInfo(event);
|
||||||
let info = getInfo(event);
|
|
||||||
|
const join = getJoin(event);
|
||||||
|
const object = getObject(event);
|
||||||
const url = getURL(event);
|
const url = getURL(event);
|
||||||
let timestamp = moment(event.data.created_at);
|
const timestamp = moment(event.data.created_at);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
memberId: event.data.member_id ?? event.data.member?.id,
|
memberId: event.data.member_id ?? event.data.member?.id,
|
||||||
@ -18,6 +20,7 @@ export default function parseMemberEvent(event, hasMultipleNewsletters) {
|
|||||||
icon,
|
icon,
|
||||||
subject,
|
subject,
|
||||||
action,
|
action,
|
||||||
|
join,
|
||||||
object,
|
object,
|
||||||
info,
|
info,
|
||||||
url,
|
url,
|
||||||
@ -77,7 +80,7 @@ function getIcon(event) {
|
|||||||
return 'event-' + icon;
|
return 'event-' + icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAction(event) {
|
function getAction(event, hasMultipleNewsletters) {
|
||||||
if (event.type === 'signup_event') {
|
if (event.type === 'signup_event') {
|
||||||
return 'signed up';
|
return 'signed up';
|
||||||
}
|
}
|
||||||
@ -91,80 +94,100 @@ function getAction(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === 'newsletter_event') {
|
if (event.type === 'newsletter_event') {
|
||||||
|
let newsletter = 'newsletter';
|
||||||
|
if (hasMultipleNewsletters && event.data.newsletter && event.data.newsletter.name) {
|
||||||
|
newsletter = 'newsletter – ' + event.data.newsletter.name;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.data.subscribed) {
|
if (event.data.subscribed) {
|
||||||
return 'subscribed to';
|
return 'subscribed to ' + newsletter;
|
||||||
} else {
|
} else {
|
||||||
return 'unsubscribed from';
|
return 'unsubscribed from ' + newsletter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === 'subscription_event') {
|
if (event.type === 'subscription_event') {
|
||||||
if (event.data.type === 'created') {
|
if (event.data.type === 'created') {
|
||||||
return 'started';
|
return 'started their subscription';
|
||||||
}
|
}
|
||||||
if (event.data.type === 'updated') {
|
if (event.data.type === 'updated') {
|
||||||
return 'changed';
|
return 'changed their subscription';
|
||||||
}
|
}
|
||||||
if (event.data.type === 'canceled') {
|
if (event.data.type === 'canceled') {
|
||||||
return 'canceled';
|
return 'canceled their subscription';
|
||||||
}
|
}
|
||||||
if (event.data.type === 'reactivated') {
|
if (event.data.type === 'reactivated') {
|
||||||
return 'reactivated';
|
return 'reactivated their subscription';
|
||||||
}
|
}
|
||||||
if (event.data.type === 'expired') {
|
if (event.data.type === 'expired') {
|
||||||
return 'ended';
|
return 'ended their subscription';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'changed';
|
return 'changed their subscription';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === 'email_opened_event') {
|
if (event.type === 'email_opened_event') {
|
||||||
return 'opened';
|
return 'opened an email';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === 'email_delivered_event') {
|
if (event.type === 'email_delivered_event') {
|
||||||
return 'received';
|
return 'received an email';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === 'email_failed_event') {
|
if (event.type === 'email_failed_event') {
|
||||||
return 'failed to receive';
|
return 'failed to receive an email';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === 'comment_event') {
|
if (event.type === 'comment_event') {
|
||||||
if (event.data.parent) {
|
if (event.data.parent) {
|
||||||
return 'replied to a comment on';
|
return 'replied to a comment';
|
||||||
}
|
}
|
||||||
return 'commented on';
|
return 'commented';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getObject(event, hasMultipleNewsletters) {
|
/**
|
||||||
if (event.type === 'newsletter_event') {
|
* When we need to append the action and object in one sentence, you can add extra words here.
|
||||||
if (hasMultipleNewsletters && event.data.newsletter && event.data.newsletter.name) {
|
* E.g.,
|
||||||
return 'newsletter – ' + event.data.newsletter.name;
|
* action: 'Signed up'.
|
||||||
|
* object: 'My blog post'
|
||||||
|
* When both words need to get appended, we'll add 'on'
|
||||||
|
* -> do this by returning 'on' in getJoin()
|
||||||
|
* This string is not added when action and object are in a separete table column, or when the getObject/getURL is empty
|
||||||
|
*/
|
||||||
|
function getJoin(event) {
|
||||||
|
if (event.type === 'signup_event' || event.type === 'subscription_event') {
|
||||||
|
if (event.data.attribution?.title) {
|
||||||
|
// Add 'Attributed to ' for now, until this is incorporated in the design
|
||||||
|
return 'on';
|
||||||
}
|
}
|
||||||
return 'newsletter';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === 'subscription_event') {
|
|
||||||
return 'their subscription';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type.match?.(/^email_/)) {
|
|
||||||
return 'an email';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === 'subscription_event') {
|
|
||||||
return 'their subscription';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === 'comment_event') {
|
if (event.type === 'comment_event') {
|
||||||
|
if (event.data.post) {
|
||||||
|
return 'on';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clickable object, shown between action and info, or in a separate column in some views
|
||||||
|
*/
|
||||||
|
function getObject(event) {
|
||||||
|
if (event.type === 'signup_event' || event.type === 'subscription_event') {
|
||||||
|
if (event.data.attribution?.title) {
|
||||||
|
// Add 'Attributed to ' for now, until this is incorporated in the design
|
||||||
|
return event.data.attribution.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type === 'comment_event') {
|
if (event.type === 'comment_event') {
|
||||||
if (event.data.post) {
|
if (event.data.post) {
|
||||||
return event.data.post.title;
|
return event.data.post.title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -179,13 +202,6 @@ function getInfo(event) {
|
|||||||
let symbol = getSymbol(event.data.currency);
|
let symbol = getSymbol(event.data.currency);
|
||||||
return `(MRR ${sign}${symbol}${Math.abs(mrrDelta)})`;
|
return `(MRR ${sign}${symbol}${Math.abs(mrrDelta)})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: we can include the post title
|
|
||||||
/*if (event.type === 'comment_event') {
|
|
||||||
if (event.data.post) {
|
|
||||||
return event.data.post.title;
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,5 +214,11 @@ function getURL(event) {
|
|||||||
return event.data.post.url;
|
return event.data.post.url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.type === 'signup_event' || event.type === 'subscription_event') {
|
||||||
|
if (event.data.attribution && event.data.attribution.url) {
|
||||||
|
return event.data.attribution.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,16 @@ const MemberCreatedEvent = ghostBookshelf.Model.extend({
|
|||||||
return this.belongsTo('Member', 'member_id', 'id');
|
return this.belongsTo('Member', 'member_id', 'id');
|
||||||
},
|
},
|
||||||
|
|
||||||
attribution() {
|
postAttribution() {
|
||||||
return this.belongsTo('Post', 'attribution_id', 'id');
|
return this.belongsTo('Post', 'attribution_id', 'id');
|
||||||
|
},
|
||||||
|
|
||||||
|
userAttribution() {
|
||||||
|
return this.belongsTo('User', 'attribution_id', 'id');
|
||||||
|
},
|
||||||
|
|
||||||
|
tagAttribution() {
|
||||||
|
return this.belongsTo('Tag', 'attribution_id', 'id');
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
async edit() {
|
async edit() {
|
||||||
|
@ -8,6 +8,10 @@ const MemberPaidSubscriptionEvent = ghostBookshelf.Model.extend({
|
|||||||
return this.belongsTo('Member', 'member_id', 'id');
|
return this.belongsTo('Member', 'member_id', 'id');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
subscriptionCreatedEvent() {
|
||||||
|
return this.belongsTo('SubscriptionCreatedEvent', 'subscription_id', 'subscription_id');
|
||||||
|
},
|
||||||
|
|
||||||
customQuery(qb, options) {
|
customQuery(qb, options) {
|
||||||
if (options.aggregateMRRDeltas) {
|
if (options.aggregateMRRDeltas) {
|
||||||
if (options.limit || options.filter) {
|
if (options.limit || options.filter) {
|
||||||
|
@ -12,8 +12,16 @@ const SubscriptionCreatedEvent = ghostBookshelf.Model.extend({
|
|||||||
return this.belongsTo('StripeCustomerSubscription', 'subscription_id', 'id');
|
return this.belongsTo('StripeCustomerSubscription', 'subscription_id', 'id');
|
||||||
},
|
},
|
||||||
|
|
||||||
attribution() {
|
postAttribution() {
|
||||||
return this.belongsTo('Post', 'attribution_id', 'id');
|
return this.belongsTo('Post', 'attribution_id', 'id');
|
||||||
|
},
|
||||||
|
|
||||||
|
userAttribution() {
|
||||||
|
return this.belongsTo('User', 'attribution_id', 'id');
|
||||||
|
},
|
||||||
|
|
||||||
|
tagAttribution() {
|
||||||
|
return this.belongsTo('Tag', 'attribution_id', 'id');
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
async edit() {
|
async edit() {
|
||||||
|
@ -24,7 +24,7 @@ class MemberAttributionServiceWrapper {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const attributionBuilder = new AttributionBuilder({urlTranslator});
|
this.attributionBuilder = new AttributionBuilder({urlTranslator});
|
||||||
|
|
||||||
// Expose the service
|
// Expose the service
|
||||||
this.service = new MemberAttributionService({
|
this.service = new MemberAttributionService({
|
||||||
@ -32,7 +32,7 @@ class MemberAttributionServiceWrapper {
|
|||||||
MemberCreatedEvent: models.MemberCreatedEvent,
|
MemberCreatedEvent: models.MemberCreatedEvent,
|
||||||
SubscriptionCreatedEvent: models.SubscriptionCreatedEvent
|
SubscriptionCreatedEvent: models.SubscriptionCreatedEvent
|
||||||
},
|
},
|
||||||
attributionBuilder,
|
attributionBuilder: this.attributionBuilder,
|
||||||
labsService
|
labsService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -185,6 +185,8 @@ function createApiInstance(config) {
|
|||||||
MemberStatusEvent: models.MemberStatusEvent,
|
MemberStatusEvent: models.MemberStatusEvent,
|
||||||
MemberProductEvent: models.MemberProductEvent,
|
MemberProductEvent: models.MemberProductEvent,
|
||||||
MemberAnalyticEvent: models.MemberAnalyticEvent,
|
MemberAnalyticEvent: models.MemberAnalyticEvent,
|
||||||
|
MemberCreatedEvent: models.MemberCreatedEvent,
|
||||||
|
SubscriptionCreatedEvent: models.SubscriptionCreatedEvent,
|
||||||
OfferRedemption: models.OfferRedemption,
|
OfferRedemption: models.OfferRedemption,
|
||||||
Offer: models.Offer,
|
Offer: models.Offer,
|
||||||
StripeProduct: models.StripeProduct,
|
StripeProduct: models.StripeProduct,
|
||||||
|
@ -1,5 +1,279 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Members API - member attribution Can read member attributed to a page 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": "618ba1ffbe2896088840a6e9",
|
||||||
|
"title": "This is a static page",
|
||||||
|
"type": "page",
|
||||||
|
"url": "http://127.0.0.1:2369/static-page-test/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": "member-attributed-to-page@test.com",
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "free",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Array [],
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API - member attribution Can read member attributed to a page 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "1955",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API - member attribution Can read member attributed to a post 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": "618ba1ffbe2896088840a6df",
|
||||||
|
"title": "HTML Ipsum",
|
||||||
|
"type": "post",
|
||||||
|
"url": "http://127.0.0.1:2369/html-ipsum/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": "member-attributed-to-post@test.com",
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "free",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Array [],
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API - member attribution Can read member attributed to a post 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "1938",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API - member attribution Can read member attributed to a tag 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": "618ba1febe2896088840a6db",
|
||||||
|
"title": "kitchen sink",
|
||||||
|
"type": "tag",
|
||||||
|
"url": "http://127.0.0.1:2369/tag/kitchen-sink/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": "member-attributed-to-tag@test.com",
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "free",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Array [],
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API - member attribution Can read member attributed to a tag 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "1944",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API - member attribution Can read member attributed to an author 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": "1",
|
||||||
|
"title": "Joe Bloggs",
|
||||||
|
"type": "author",
|
||||||
|
"url": "http://127.0.0.1:2369/author/joe-bloggs/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": "member-attributed-to-author@test.com",
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "free",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Array [],
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API - member attribution Can read member attributed to an author 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "1926",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API - member attribution Can read member attributed to an url 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": null,
|
||||||
|
"title": "/a-static-page/",
|
||||||
|
"type": "url",
|
||||||
|
"url": "http://127.0.0.1:2369/a-static-page/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": "member-attributed-to-url@test.com",
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "free",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Array [],
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API - member attribution Can read member attributed to an url 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "1922",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API - member attribution Returns sign up attributions in activity feed 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"events": Array [
|
||||||
|
Object {
|
||||||
|
"data": Any<Object>,
|
||||||
|
"type": Any<String>,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"data": Any<Object>,
|
||||||
|
"type": Any<String>,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"data": Any<Object>,
|
||||||
|
"type": Any<String>,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"data": Any<Object>,
|
||||||
|
"type": Any<String>,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"data": Any<Object>,
|
||||||
|
"type": Any<String>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API - member attribution Returns sign up attributions in activity feed 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "8514",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Members API Add should fail when passing incorrect email_type query parameter 1: [body] 1`] = `
|
exports[`Members API Add should fail when passing incorrect email_type query parameter 1: [body] 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"errors": Array [
|
"errors": Array [
|
||||||
@ -3748,3 +4022,280 @@ Object {
|
|||||||
"x-powered-by": "Express",
|
"x-powered-by": "Express",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Members APi - member attribution Can read 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": null,
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": "member1@test.com",
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": "Mr Egg",
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "free",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Array [],
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members APi - member attribution Can read 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "1321",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members APi - member attribution Can read member attributed to a page 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": "618ba1ffbe2896088840a6e9",
|
||||||
|
"title": "This is a static page",
|
||||||
|
"type": "page",
|
||||||
|
"url": "http://127.0.0.1:2369/static-page-test/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": "member-attributed-to-page@test.com",
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "free",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Array [],
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members APi - member attribution Can read member attributed to a page 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "1955",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members APi - member attribution Can read member attributed to a post 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": "618ba1ffbe2896088840a6df",
|
||||||
|
"title": "HTML Ipsum",
|
||||||
|
"type": "post",
|
||||||
|
"url": "http://127.0.0.1:2369/html-ipsum/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": "member-attributed-to-post@test.com",
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "free",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Array [],
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members APi - member attribution Can read member attributed to a post 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "1938",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members APi - member attribution Can read member attributed to a tag 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": "618ba1febe2896088840a6db",
|
||||||
|
"title": "kitchen sink",
|
||||||
|
"type": "tag",
|
||||||
|
"url": "http://127.0.0.1:2369/tag/kitchen-sink/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": "member-attributed-to-tag@test.com",
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "free",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Array [],
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members APi - member attribution Can read member attributed to a tag 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "1944",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members APi - member attribution Can read member attributed to an author 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": "1",
|
||||||
|
"title": "Joe Bloggs",
|
||||||
|
"type": "author",
|
||||||
|
"url": "http://127.0.0.1:2369/author/joe-bloggs/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": "member-attributed-to-author@test.com",
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "free",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Array [],
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members APi - member attribution Can read member attributed to an author 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "1926",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members APi - member attribution Can read member attributed to an url 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": null,
|
||||||
|
"title": "/a-static-page/",
|
||||||
|
"type": "url",
|
||||||
|
"url": "http://127.0.0.1:2369/a-static-page/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": "member-attributed-to-url@test.com",
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "free",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Array [],
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members APi - member attribution Can read member attributed to an url 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "1922",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -10,6 +10,10 @@ const testUtils = require('../../utils');
|
|||||||
const Papa = require('papaparse');
|
const Papa = require('papaparse');
|
||||||
|
|
||||||
const models = require('../../../core/server/models');
|
const models = require('../../../core/server/models');
|
||||||
|
const membersService = require('../../../core/server/services/members');
|
||||||
|
const memberAttributionService = require('../../../core/server/services/member-attribution');
|
||||||
|
const urlService = require('../../../core/server/services/url');
|
||||||
|
const urlUtils = require('../../../core/shared/url-utils');
|
||||||
|
|
||||||
async function assertMemberEvents({eventType, memberId, asserts}) {
|
async function assertMemberEvents({eventType, memberId, asserts}) {
|
||||||
const events = await models[eventType].where('member_id', memberId).fetchAll();
|
const events = await models[eventType].where('member_id', memberId).fetchAll();
|
||||||
@ -153,6 +157,229 @@ describe('Members API without Stripe', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tests specific for member attribution
|
||||||
|
describe('Members API - member attribution', function () {
|
||||||
|
const signupAttributions = [];
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
agent = await agentProvider.getAdminAPIAgent();
|
||||||
|
await fixtureManager.init('posts', 'newsletters', 'members:newsletters', 'comments');
|
||||||
|
await agent.loginAsOwner();
|
||||||
|
// This is required so that the only members in this test are created by this test, and not from fixtures.
|
||||||
|
await models.Member.query().del();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
mockManager.mockStripe();
|
||||||
|
mockManager.mockMail();
|
||||||
|
|
||||||
|
// For some reason it is enabled by default?
|
||||||
|
mockManager.mockLabsEnabled('memberAttribution');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
mockManager.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can read member attributed to a post', async function () {
|
||||||
|
const id = fixtureManager.get('posts', 0).id;
|
||||||
|
const post = await models.Post.where('id', id).fetch({require: true});
|
||||||
|
|
||||||
|
// Set the attribution for this member manually
|
||||||
|
const member = await membersService.api.members.create({
|
||||||
|
email: 'member-attributed-to-post@test.com',
|
||||||
|
attribution: memberAttributionService.attributionBuilder.build({
|
||||||
|
id,
|
||||||
|
url: '/out-of-date/',
|
||||||
|
type: 'post'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true});
|
||||||
|
|
||||||
|
await agent
|
||||||
|
.get(`/members/${member.id}/`)
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
members: new Array(1).fill(memberMatcherShallowIncludes)
|
||||||
|
})
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag
|
||||||
|
})
|
||||||
|
.expect(({body}) => {
|
||||||
|
should(body.members[0].attribution).eql({
|
||||||
|
id: post.id,
|
||||||
|
url: absoluteUrl,
|
||||||
|
type: 'post',
|
||||||
|
title: post.get('title')
|
||||||
|
});
|
||||||
|
signupAttributions.push(body.members[0].attribution);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can read member attributed to a page', async function () {
|
||||||
|
const id = fixtureManager.get('posts', 5).id;
|
||||||
|
const post = await models.Post.where('id', id).fetch({require: true});
|
||||||
|
|
||||||
|
// Set the attribution for this member manually
|
||||||
|
const member = await membersService.api.members.create({
|
||||||
|
email: 'member-attributed-to-page@test.com',
|
||||||
|
attribution: memberAttributionService.attributionBuilder.build({
|
||||||
|
id,
|
||||||
|
url: '/out-of-date/',
|
||||||
|
type: 'page'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true});
|
||||||
|
|
||||||
|
await agent
|
||||||
|
.get(`/members/${member.id}/`)
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
members: new Array(1).fill(memberMatcherShallowIncludes)
|
||||||
|
})
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag
|
||||||
|
})
|
||||||
|
.expect(({body}) => {
|
||||||
|
should(body.members[0].attribution).eql({
|
||||||
|
id: post.id,
|
||||||
|
url: absoluteUrl,
|
||||||
|
type: 'page',
|
||||||
|
title: post.get('title')
|
||||||
|
});
|
||||||
|
signupAttributions.push(body.members[0].attribution);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can read member attributed to a tag', async function () {
|
||||||
|
const id = fixtureManager.get('tags', 0).id;
|
||||||
|
const tag = await models.Tag.where('id', id).fetch({require: true});
|
||||||
|
|
||||||
|
// Set the attribution for this member manually
|
||||||
|
const member = await membersService.api.members.create({
|
||||||
|
email: 'member-attributed-to-tag@test.com',
|
||||||
|
attribution: memberAttributionService.attributionBuilder.build({
|
||||||
|
id,
|
||||||
|
url: '/out-of-date/',
|
||||||
|
type: 'tag'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const absoluteUrl = urlService.getUrlByResourceId(tag.id, {absolute: true});
|
||||||
|
|
||||||
|
await agent
|
||||||
|
.get(`/members/${member.id}/`)
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
members: new Array(1).fill(memberMatcherShallowIncludes)
|
||||||
|
})
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag
|
||||||
|
})
|
||||||
|
.expect(({body}) => {
|
||||||
|
should(body.members[0].attribution).eql({
|
||||||
|
id: tag.id,
|
||||||
|
url: absoluteUrl,
|
||||||
|
type: 'tag',
|
||||||
|
title: tag.get('name')
|
||||||
|
});
|
||||||
|
signupAttributions.push(body.members[0].attribution);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can read member attributed to an author', async function () {
|
||||||
|
const id = fixtureManager.get('users', 0).id;
|
||||||
|
const author = await models.User.where('id', id).fetch({require: true});
|
||||||
|
|
||||||
|
// Set the attribution for this member manually
|
||||||
|
const member = await membersService.api.members.create({
|
||||||
|
email: 'member-attributed-to-author@test.com',
|
||||||
|
attribution: memberAttributionService.attributionBuilder.build({
|
||||||
|
id,
|
||||||
|
url: '/out-of-date/',
|
||||||
|
type: 'author'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const absoluteUrl = urlService.getUrlByResourceId(author.id, {absolute: true});
|
||||||
|
|
||||||
|
await agent
|
||||||
|
.get(`/members/${member.id}/`)
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
members: new Array(1).fill(memberMatcherShallowIncludes)
|
||||||
|
})
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag
|
||||||
|
})
|
||||||
|
.expect(({body}) => {
|
||||||
|
should(body.members[0].attribution).eql({
|
||||||
|
id: author.id,
|
||||||
|
url: absoluteUrl,
|
||||||
|
type: 'author',
|
||||||
|
title: author.get('name')
|
||||||
|
});
|
||||||
|
signupAttributions.push(body.members[0].attribution);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can read member attributed to an url', async function () {
|
||||||
|
// Set the attribution for this member manually
|
||||||
|
const member = await membersService.api.members.create({
|
||||||
|
email: 'member-attributed-to-url@test.com',
|
||||||
|
attribution: memberAttributionService.attributionBuilder.build({
|
||||||
|
id: null,
|
||||||
|
url: '/a-static-page/',
|
||||||
|
type: 'url'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const absoluteUrl = urlUtils.createUrl('/a-static-page/', true);
|
||||||
|
|
||||||
|
await agent
|
||||||
|
.get(`/members/${member.id}/`)
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
members: new Array(1).fill(memberMatcherShallowIncludes)
|
||||||
|
})
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag
|
||||||
|
})
|
||||||
|
.expect(({body}) => {
|
||||||
|
should(body.members[0].attribution).eql({
|
||||||
|
id: null,
|
||||||
|
url: absoluteUrl,
|
||||||
|
type: 'url',
|
||||||
|
title: '/a-static-page/'
|
||||||
|
});
|
||||||
|
signupAttributions.push(body.members[0].attribution);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activity feed
|
||||||
|
it('Returns sign up attributions in activity feed', async function () {
|
||||||
|
// Check activity feed
|
||||||
|
await agent
|
||||||
|
.get(`/members/events/?filter=type:signup_event`)
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag
|
||||||
|
})
|
||||||
|
.matchBodySnapshot({
|
||||||
|
events: new Array(signupAttributions.length).fill({
|
||||||
|
type: anyString,
|
||||||
|
data: anyObject
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.expect(({body}) => {
|
||||||
|
should(body.events.find(e => e.type !== 'signup_event')).be.undefined();
|
||||||
|
should(body.events.map(e => e.data.attribution)).containDeep(signupAttributions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Members API', function () {
|
describe('Members API', function () {
|
||||||
let newsletters;
|
let newsletters;
|
||||||
|
|
||||||
|
@ -0,0 +1,448 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with author attribution 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": "1",
|
||||||
|
"title": "Joe Bloggs",
|
||||||
|
"type": "author",
|
||||||
|
"url": "http://127.0.0.1:2369/author/joe-bloggs/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": Any<String>,
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "paid",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Any<Array>,
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with author attribution 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "2795",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with deleted post attribution 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": null,
|
||||||
|
"title": "/removed-blog-post/",
|
||||||
|
"type": "url",
|
||||||
|
"url": "http://127.0.0.1:2369/removed-blog-post/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": Any<String>,
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "paid",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Any<Array>,
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with deleted post attribution 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "2809",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with empty attribution object 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": null,
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": Any<String>,
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "paid",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Any<Array>,
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with empty attribution object 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "2611",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with page attribution 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": "618ba1ffbe2896088840a6e9",
|
||||||
|
"title": "This is a static page",
|
||||||
|
"type": "page",
|
||||||
|
"url": "http://127.0.0.1:2369/static-page-test/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": Any<String>,
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "paid",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Any<Array>,
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with page attribution 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "2857",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with post attribution 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": "618ba1ffbe2896088840a6df",
|
||||||
|
"title": "HTML Ipsum",
|
||||||
|
"type": "post",
|
||||||
|
"url": "http://127.0.0.1:2369/html-ipsum/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": Any<String>,
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "paid",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Any<Array>,
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with post attribution 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "2823",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with tag attribution 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": "618ba1febe2896088840a6db",
|
||||||
|
"title": "kitchen sink",
|
||||||
|
"type": "tag",
|
||||||
|
"url": "http://127.0.0.1:2369/tag/kitchen-sink/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": Any<String>,
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "paid",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Any<Array>,
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with tag attribution 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "2837",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with url attribution 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": Object {
|
||||||
|
"id": null,
|
||||||
|
"title": "/",
|
||||||
|
"type": "url",
|
||||||
|
"url": "http://127.0.0.1:2369/",
|
||||||
|
},
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": Any<String>,
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "paid",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Any<Array>,
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with url attribution 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "2737",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent without attribution 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"members": Array [
|
||||||
|
Object {
|
||||||
|
"attribution": null,
|
||||||
|
"avatar_image": null,
|
||||||
|
"comped": false,
|
||||||
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"email": Any<String>,
|
||||||
|
"email_count": 0,
|
||||||
|
"email_open_rate": null,
|
||||||
|
"email_opened_count": 0,
|
||||||
|
"geolocation": null,
|
||||||
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
|
"labels": Any<Array>,
|
||||||
|
"last_seen_at": null,
|
||||||
|
"name": null,
|
||||||
|
"newsletters": Any<Array>,
|
||||||
|
"note": null,
|
||||||
|
"status": "paid",
|
||||||
|
"subscribed": true,
|
||||||
|
"subscriptions": Any<Array>,
|
||||||
|
"tiers": Any<Array>,
|
||||||
|
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
|
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent without attribution 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "2611",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Returns subscription created attributions in activity feed 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"events": Array [
|
||||||
|
Object {
|
||||||
|
"data": Any<Object>,
|
||||||
|
"type": Any<String>,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"data": Any<Object>,
|
||||||
|
"type": Any<String>,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"data": Any<Object>,
|
||||||
|
"type": Any<String>,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"data": Any<Object>,
|
||||||
|
"type": Any<String>,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"data": Any<Object>,
|
||||||
|
"type": Any<String>,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"data": Any<Object>,
|
||||||
|
"type": Any<String>,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"data": Any<Object>,
|
||||||
|
"type": Any<String>,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"data": Any<Object>,
|
||||||
|
"type": Any<String>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Returns subscription created attributions in activity feed 1: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "7784",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution Returns subscription created attributions in activity feed 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "13523",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution empty initial activity feed 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"events": Array [],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Members API Member attribution empty initial activity feed 2: [headers] 1`] = `
|
||||||
|
Object {
|
||||||
|
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||||
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
|
"content-length": "13",
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
|
"vary": "Origin, Accept-Encoding",
|
||||||
|
"x-powered-by": "Express",
|
||||||
|
}
|
||||||
|
`;
|
@ -4,9 +4,11 @@ const nock = require('nock');
|
|||||||
const should = require('should');
|
const should = require('should');
|
||||||
const stripe = require('stripe');
|
const stripe = require('stripe');
|
||||||
const {Product} = require('../../../core/server/models/product');
|
const {Product} = require('../../../core/server/models/product');
|
||||||
const {agentProvider, mockManager, fixtureManager} = require('../../utils/e2e-framework');
|
const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework');
|
||||||
const models = require('../../../core/server/models');
|
const models = require('../../../core/server/models');
|
||||||
const offers = require('../../../core/server/services/offers');
|
const urlService = require('../../../core/server/services/url');
|
||||||
|
const urlUtils = require('../../../core/shared/url-utils');
|
||||||
|
const {anyEtag, anyObjectId, anyUuid, anyISODateTime, anyISODate, anyString, anyArray, anyLocationFor, anyErrorId, anyObject} = matchers;
|
||||||
|
|
||||||
let membersAgent;
|
let membersAgent;
|
||||||
let adminAgent;
|
let adminAgent;
|
||||||
@ -42,28 +44,9 @@ async function assertSubscription(subscriptionId, asserts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('Members API', function () {
|
describe('Members API', function () {
|
||||||
before(async function () {
|
|
||||||
const agents = await agentProvider.getAgentsForMembers();
|
|
||||||
membersAgent = agents.membersAgent;
|
|
||||||
adminAgent = agents.adminAgent;
|
|
||||||
|
|
||||||
await fixtureManager.init('members');
|
|
||||||
await adminAgent.loginAsOwner();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
mockManager.mockMail();
|
|
||||||
mockManager.mockStripe();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function () {
|
|
||||||
mockManager.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
// @todo: Test what happens when a complementary subscription ends (should create comped -> free event)
|
// @todo: Test what happens when a complementary subscription ends (should create comped -> free event)
|
||||||
// @todo: Test what happens when a complementary subscription starts a paid subscription
|
// @todo: Test what happens when a complementary subscription starts a paid subscription
|
||||||
|
|
||||||
describe('/webhooks/stripe/', function () {
|
|
||||||
// We create some shared stripe resources, so we don't have to have nocks in every test case
|
// We create some shared stripe resources, so we don't have to have nocks in every test case
|
||||||
const subscription = {};
|
const subscription = {};
|
||||||
const customer = {};
|
const customer = {};
|
||||||
@ -150,25 +133,24 @@ describe('Members API', function () {
|
|||||||
Object.assign(object, newValues);
|
Object.assign(object, newValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
describe('/webhooks/stripe/', function () {
|
||||||
* Helper method to create an existing member based on a customer in stripe (= current customer)
|
before(async function () {
|
||||||
*/
|
const agents = await agentProvider.getAgentsForMembers();
|
||||||
async function createMemberFromStripe() {
|
membersAgent = agents.membersAgent;
|
||||||
const initialMember = {
|
adminAgent = agents.adminAgent;
|
||||||
name: customer.name,
|
|
||||||
email: customer.email,
|
|
||||||
subscribed: true,
|
|
||||||
stripe_customer_id: customer.id
|
|
||||||
};
|
|
||||||
|
|
||||||
const {body} = await adminAgent
|
await fixtureManager.init('members');
|
||||||
.post(`/members/`)
|
await adminAgent.loginAsOwner();
|
||||||
.body({members: [initialMember]})
|
});
|
||||||
.expectStatus(201);
|
|
||||||
assert.equal(body.members.length, 1, 'The member was not created');
|
beforeEach(function () {
|
||||||
const member = body.members[0];
|
mockManager.mockMail();
|
||||||
return member;
|
mockManager.mockStripe();
|
||||||
}
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
mockManager.restore();
|
||||||
|
});
|
||||||
|
|
||||||
it('Responds with a 401 when the signature is invalid', async function () {
|
it('Responds with a 401 when the signature is invalid', async function () {
|
||||||
await membersAgent.post('/webhooks/stripe/')
|
await membersAgent.post('/webhooks/stripe/')
|
||||||
@ -196,129 +178,47 @@ describe('Members API', function () {
|
|||||||
.header('stripe-signature', webhookSignature)
|
.header('stripe-signature', webhookSignature)
|
||||||
.expectStatus(200);
|
.expectStatus(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Handles cancellation of paid subscriptions correctly', async function () {
|
|
||||||
const customer_id = createStripeID('cust');
|
|
||||||
const subscription_id = createStripeID('sub');
|
|
||||||
|
|
||||||
// Create a new subscription in Stripe
|
|
||||||
set(subscription, {
|
|
||||||
id: subscription_id,
|
|
||||||
customer: customer_id,
|
|
||||||
status: 'active',
|
|
||||||
items: {
|
|
||||||
type: 'list',
|
|
||||||
data: [{
|
|
||||||
id: 'item_123',
|
|
||||||
price: {
|
|
||||||
id: 'price_123',
|
|
||||||
product: 'product_123',
|
|
||||||
active: true,
|
|
||||||
nickname: 'Monthly',
|
|
||||||
currency: 'USD',
|
|
||||||
recurring: {
|
|
||||||
interval: 'month'
|
|
||||||
},
|
|
||||||
unit_amount: 500,
|
|
||||||
type: 'recurring'
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
start_date: Date.now() / 1000,
|
|
||||||
current_period_end: Date.now() / 1000 + (60 * 60 * 24 * 31),
|
|
||||||
cancel_at_period_end: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a new customer in Stripe
|
|
||||||
set(customer, {
|
|
||||||
id: customer_id,
|
|
||||||
name: 'Test Member',
|
|
||||||
email: 'expired-paid-test@email.com',
|
|
||||||
subscriptions: {
|
|
||||||
type: 'list',
|
|
||||||
data: [subscription]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Make sure this customer has a corresponding member in the database
|
|
||||||
// And all the subscriptions are setup correctly
|
|
||||||
const initialMember = await createMemberFromStripe();
|
|
||||||
assert.equal(initialMember.status, 'paid', 'The member initial status should be paid');
|
|
||||||
assert.equal(initialMember.tiers.length, 1, 'The member should have one tier');
|
|
||||||
should(initialMember.subscriptions).match([
|
|
||||||
{
|
|
||||||
status: 'active'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Cancel the previously created subscription in Stripe
|
|
||||||
set(subscription, {
|
|
||||||
...subscription,
|
|
||||||
cancel_at_period_end: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send the webhook call to anounce the cancelation
|
|
||||||
const webhookPayload = JSON.stringify({
|
|
||||||
type: 'customer.subscription.updated',
|
|
||||||
data: {
|
|
||||||
object: subscription
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const webhookSignature = stripe.webhooks.generateTestHeaderString({
|
|
||||||
payload: webhookPayload,
|
|
||||||
secret: process.env.WEBHOOK_SECRET
|
|
||||||
});
|
|
||||||
|
|
||||||
await membersAgent.post('/webhooks/stripe/')
|
|
||||||
.body(webhookPayload)
|
|
||||||
.header('stripe-signature', webhookSignature)
|
|
||||||
.expectStatus(200);
|
|
||||||
|
|
||||||
// Check status has been updated to 'free' after cancelling
|
|
||||||
const {body: body2} = await adminAgent.get('/members/' + initialMember.id + '/');
|
|
||||||
assert.equal(body2.members.length, 1, 'The member does not exist');
|
|
||||||
const updatedMember = body2.members[0];
|
|
||||||
assert.equal(updatedMember.status, 'paid');
|
|
||||||
assert.equal(updatedMember.tiers.length, 1, 'The member should have tiers');
|
|
||||||
should(updatedMember.subscriptions).match([
|
|
||||||
{
|
|
||||||
cancel_at_period_end: true
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Check the status events for this newly created member (should be NULL -> paid only)
|
|
||||||
await assertMemberEvents({
|
|
||||||
eventType: 'MemberStatusEvent',
|
|
||||||
memberId: updatedMember.id,
|
|
||||||
asserts: [
|
|
||||||
{
|
|
||||||
from_status: null,
|
|
||||||
to_status: 'free'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from_status: 'free',
|
|
||||||
to_status: 'paid'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
await assertMemberEvents({
|
|
||||||
eventType: 'MemberPaidSubscriptionEvent',
|
|
||||||
memberId: updatedMember.id,
|
|
||||||
asserts: [
|
|
||||||
{
|
|
||||||
type: 'created',
|
|
||||||
mrr_delta: 500
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'canceled',
|
|
||||||
mrr_delta: -500
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Handling the end of subscriptions', function () {
|
describe('Handling the end of subscriptions', function () {
|
||||||
|
before(async function () {
|
||||||
|
const agents = await agentProvider.getAgentsForMembers();
|
||||||
|
membersAgent = agents.membersAgent;
|
||||||
|
adminAgent = agents.adminAgent;
|
||||||
|
|
||||||
|
await fixtureManager.init('members');
|
||||||
|
await adminAgent.loginAsOwner();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
mockManager.mockMail();
|
||||||
|
mockManager.mockStripe();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
mockManager.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to create an existing member based on a customer in stripe (= current customer)
|
||||||
|
*/
|
||||||
|
async function createMemberFromStripe() {
|
||||||
|
const initialMember = {
|
||||||
|
name: customer.name,
|
||||||
|
email: customer.email,
|
||||||
|
subscribed: true,
|
||||||
|
stripe_customer_id: customer.id
|
||||||
|
};
|
||||||
|
|
||||||
|
const {body} = await adminAgent
|
||||||
|
.post(`/members/`)
|
||||||
|
.body({members: [initialMember]})
|
||||||
|
.expectStatus(201);
|
||||||
|
assert.equal(body.members.length, 1, 'The member was not created');
|
||||||
|
const member = body.members[0];
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
|
||||||
let canceledPaidMember;
|
let canceledPaidMember;
|
||||||
|
|
||||||
it('Handles cancellation of paid subscriptions correctly', async function () {
|
it('Handles cancellation of paid subscriptions correctly', async function () {
|
||||||
@ -667,7 +567,15 @@ describe('Members API', function () {
|
|||||||
describe('checkout.session.completed', function () {
|
describe('checkout.session.completed', function () {
|
||||||
// The subscription that we got from Stripe was created 2 seconds earlier (used for testing events)
|
// The subscription that we got from Stripe was created 2 seconds earlier (used for testing events)
|
||||||
const beforeNow = Math.floor((Date.now() - 2000) / 1000) * 1000;
|
const beforeNow = Math.floor((Date.now() - 2000) / 1000) * 1000;
|
||||||
before(function () {
|
|
||||||
|
before(async function () {
|
||||||
|
const agents = await agentProvider.getAgentsForMembers();
|
||||||
|
membersAgent = agents.membersAgent;
|
||||||
|
adminAgent = agents.adminAgent;
|
||||||
|
|
||||||
|
await fixtureManager.init('members');
|
||||||
|
await adminAgent.loginAsOwner();
|
||||||
|
|
||||||
set(subscription, {
|
set(subscription, {
|
||||||
id: 'sub_123',
|
id: 'sub_123',
|
||||||
customer: 'cus_123',
|
customer: 'cus_123',
|
||||||
@ -696,6 +604,15 @@ describe('Members API', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
mockManager.mockMail();
|
||||||
|
mockManager.mockStripe();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
mockManager.restore();
|
||||||
|
});
|
||||||
|
|
||||||
it('Will create a member if one does not exist', async function () {
|
it('Will create a member if one does not exist', async function () {
|
||||||
set(customer, {
|
set(customer, {
|
||||||
id: 'cus_123',
|
id: 'cus_123',
|
||||||
@ -950,6 +867,13 @@ describe('Members API', function () {
|
|||||||
let couponId = 'testCoupon123';
|
let couponId = 'testCoupon123';
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
|
const agents = await agentProvider.getAgentsForMembers();
|
||||||
|
membersAgent = agents.membersAgent;
|
||||||
|
adminAgent = agents.adminAgent;
|
||||||
|
|
||||||
|
await fixtureManager.init('members');
|
||||||
|
await adminAgent.loginAsOwner();
|
||||||
|
|
||||||
// Create a random offer_id that we'll use
|
// Create a random offer_id that we'll use
|
||||||
// The actual amounts don't matter as we'll only take the ones from Stripe ATM
|
// The actual amounts don't matter as we'll only take the ones from Stripe ATM
|
||||||
const newOffer = {
|
const newOffer = {
|
||||||
@ -984,6 +908,15 @@ describe('Members API', function () {
|
|||||||
offer = body.offers[0];
|
offer = body.offers[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
mockManager.mockMail();
|
||||||
|
mockManager.mockStripe();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
mockManager.restore();
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for repetitive tests. It tests the MRR and MRR delta given a discount + a price
|
* Helper for repetitive tests. It tests the MRR and MRR delta given a discount + a price
|
||||||
*/
|
*/
|
||||||
@ -1685,14 +1618,59 @@ describe('Members API', function () {
|
|||||||
|
|
||||||
// Test if the session metadata is processed correctly
|
// Test if the session metadata is processed correctly
|
||||||
describe('Member attribution', function () {
|
describe('Member attribution', function () {
|
||||||
|
before(async function () {
|
||||||
|
const agents = await agentProvider.getAgentsForMembers();
|
||||||
|
membersAgent = agents.membersAgent;
|
||||||
|
adminAgent = agents.adminAgent;
|
||||||
|
|
||||||
|
await fixtureManager.init('posts', 'products');
|
||||||
|
await adminAgent.loginAsOwner();
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
mockManager.mockLabsEnabled('memberAttribution');
|
mockManager.mockLabsEnabled('memberAttribution');
|
||||||
|
mockManager.mockMail();
|
||||||
|
mockManager.mockStripe();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
mockManager.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
// The subscription that we got from Stripe was created 2 seconds earlier (used for testing events)
|
// The subscription that we got from Stripe was created 2 seconds earlier (used for testing events)
|
||||||
const beforeNow = Math.floor((Date.now() - 2000) / 1000) * 1000;
|
const beforeNow = Math.floor((Date.now() - 2000) / 1000) * 1000;
|
||||||
|
|
||||||
async function testWithAttribution(attribution) {
|
const memberMatcherShallowIncludes = {
|
||||||
|
id: anyObjectId,
|
||||||
|
uuid: anyUuid,
|
||||||
|
email: anyString,
|
||||||
|
created_at: anyISODateTime,
|
||||||
|
updated_at: anyISODateTime,
|
||||||
|
subscriptions: anyArray,
|
||||||
|
labels: anyArray,
|
||||||
|
tiers: anyArray,
|
||||||
|
newsletters: anyArray
|
||||||
|
};
|
||||||
|
|
||||||
|
// Activity feed
|
||||||
|
// This test is here because creating subscriptions is a PITA now, and we would need to essentially duplicate all above tests elsewhere
|
||||||
|
it('empty initial activity feed', async function () {
|
||||||
|
// Check activity feed
|
||||||
|
await adminAgent
|
||||||
|
.get(`/members/events/?filter=type:subscription_event`)
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag
|
||||||
|
})
|
||||||
|
.matchBodySnapshot({
|
||||||
|
events: new Array(0).fill({
|
||||||
|
type: anyString,
|
||||||
|
data: anyObject
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function testWithAttribution(attribution, attributionResource) {
|
||||||
const customer_id = createStripeID('cust');
|
const customer_id = createStripeID('cust');
|
||||||
const subscription_id = createStripeID('sub');
|
const subscription_id = createStripeID('sub');
|
||||||
|
|
||||||
@ -1809,8 +1787,27 @@ describe('Members API', function () {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await adminAgent
|
||||||
|
.get(`/members/${member.id}/`)
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
members: new Array(1).fill(memberMatcherShallowIncludes)
|
||||||
|
})
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag
|
||||||
|
})
|
||||||
|
.expect(({body: body3}) => {
|
||||||
|
should(body3.members[0].attribution).eql(attributionResource);
|
||||||
|
should(body3.members[0].subscriptions[0].attribution).eql(attributionResource);
|
||||||
|
subscriptionAttributions.push(body3.members[0].subscriptions[0].attribution);
|
||||||
|
});
|
||||||
|
|
||||||
|
return memberModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subscriptionAttributions = [];
|
||||||
|
|
||||||
it('Creates a SubscriptionCreatedEvent with url attribution', async function () {
|
it('Creates a SubscriptionCreatedEvent with url attribution', async function () {
|
||||||
// This mainly tests for nullable fields being set to null and handled correctly
|
// This mainly tests for nullable fields being set to null and handled correctly
|
||||||
const attribution = {
|
const attribution = {
|
||||||
@ -1819,28 +1816,143 @@ describe('Members API', function () {
|
|||||||
type: 'url'
|
type: 'url'
|
||||||
};
|
};
|
||||||
|
|
||||||
await testWithAttribution(attribution);
|
const absoluteUrl = urlUtils.createUrl('/', true);
|
||||||
|
|
||||||
|
await testWithAttribution(attribution, {
|
||||||
|
id: null,
|
||||||
|
url: absoluteUrl,
|
||||||
|
type: 'url',
|
||||||
|
title: '/'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Creates a SubscriptionCreatedEvent with post attribution', async function () {
|
it('Creates a SubscriptionCreatedEvent with post attribution', async function () {
|
||||||
|
const id = fixtureManager.get('posts', 0).id;
|
||||||
|
const post = await models.Post.where('id', id).fetch({require: true});
|
||||||
|
|
||||||
const attribution = {
|
const attribution = {
|
||||||
id: 'my-post-id',
|
id: post.id,
|
||||||
url: '/my-post-slug',
|
url: '/out-of-date-url/',
|
||||||
type: 'post'
|
type: 'post'
|
||||||
};
|
};
|
||||||
|
|
||||||
await testWithAttribution(attribution);
|
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true});
|
||||||
|
|
||||||
|
await testWithAttribution(attribution, {
|
||||||
|
id: post.id,
|
||||||
|
url: absoluteUrl,
|
||||||
|
type: 'post',
|
||||||
|
title: post.get('title')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Creates a SubscriptionCreatedEvent with deleted post attribution', async function () {
|
||||||
|
const attribution = {
|
||||||
|
id: 'doesnt-exist-anylonger',
|
||||||
|
url: '/removed-blog-post/',
|
||||||
|
type: 'post'
|
||||||
|
};
|
||||||
|
|
||||||
|
const absoluteUrl = urlUtils.createUrl('/removed-blog-post/', true);
|
||||||
|
|
||||||
|
await testWithAttribution(attribution, {
|
||||||
|
id: null,
|
||||||
|
url: absoluteUrl,
|
||||||
|
type: 'url',
|
||||||
|
title: '/removed-blog-post/'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Creates a SubscriptionCreatedEvent with page attribution', async function () {
|
||||||
|
const id = fixtureManager.get('posts', 5).id;
|
||||||
|
const post = await models.Post.where('id', id).fetch({require: true});
|
||||||
|
|
||||||
|
const attribution = {
|
||||||
|
id: post.id,
|
||||||
|
url: '/out-of-date-url/',
|
||||||
|
type: 'page'
|
||||||
|
};
|
||||||
|
|
||||||
|
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true});
|
||||||
|
|
||||||
|
await testWithAttribution(attribution, {
|
||||||
|
id: post.id,
|
||||||
|
url: absoluteUrl,
|
||||||
|
type: 'page',
|
||||||
|
title: post.get('title')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Creates a SubscriptionCreatedEvent with tag attribution', async function () {
|
||||||
|
const id = fixtureManager.get('tags', 0).id;
|
||||||
|
const tag = await models.Tag.where('id', id).fetch({require: true});
|
||||||
|
|
||||||
|
const attribution = {
|
||||||
|
id: tag.id,
|
||||||
|
url: '/out-of-date-url/',
|
||||||
|
type: 'tag'
|
||||||
|
};
|
||||||
|
|
||||||
|
const absoluteUrl = urlService.getUrlByResourceId(tag.id, {absolute: true});
|
||||||
|
|
||||||
|
await testWithAttribution(attribution, {
|
||||||
|
id: tag.id,
|
||||||
|
url: absoluteUrl,
|
||||||
|
type: 'tag',
|
||||||
|
title: tag.get('name')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Creates a SubscriptionCreatedEvent with author attribution', async function () {
|
||||||
|
const id = fixtureManager.get('users', 0).id;
|
||||||
|
const author = await models.User.where('id', id).fetch({require: true});
|
||||||
|
|
||||||
|
const attribution = {
|
||||||
|
id: author.id,
|
||||||
|
url: '/out-of-date-url/',
|
||||||
|
type: 'author'
|
||||||
|
};
|
||||||
|
|
||||||
|
const absoluteUrl = urlService.getUrlByResourceId(author.id, {absolute: true});
|
||||||
|
|
||||||
|
await testWithAttribution(attribution, {
|
||||||
|
id: author.id,
|
||||||
|
url: absoluteUrl,
|
||||||
|
type: 'author',
|
||||||
|
title: author.get('name')
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Creates a SubscriptionCreatedEvent without attribution', async function () {
|
it('Creates a SubscriptionCreatedEvent without attribution', async function () {
|
||||||
const attribution = undefined;
|
const attribution = undefined;
|
||||||
await testWithAttribution(attribution);
|
await testWithAttribution(attribution, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Creates a SubscriptionCreatedEvent with empty attribution object', async function () {
|
it('Creates a SubscriptionCreatedEvent with empty attribution object', async function () {
|
||||||
// Shouldn't happen, but to make sure we handle it
|
// Shouldn't happen, but to make sure we handle it
|
||||||
const attribution = {};
|
const attribution = {};
|
||||||
await testWithAttribution(attribution);
|
await testWithAttribution(attribution, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activity feed
|
||||||
|
// This test is here because creating subscriptions is a PITA now, and we would need to essentially duplicate all above tests elsewhere
|
||||||
|
it('Returns subscription created attributions in activity feed', async function () {
|
||||||
|
// Check activity feed
|
||||||
|
await adminAgent
|
||||||
|
.get(`/members/events/?filter=type:subscription_event`)
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag
|
||||||
|
})
|
||||||
|
.matchBodySnapshot({
|
||||||
|
events: new Array(subscriptionAttributions.length).fill({
|
||||||
|
type: anyString,
|
||||||
|
data: anyObject
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.expect(({body}) => {
|
||||||
|
should(body.events.find(e => e.type !== 'subscription_event')).be.undefined();
|
||||||
|
should(body.events.map(e => e.data.attribution)).containDeep(subscriptionAttributions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -33,7 +33,7 @@ describe('Member Attribution Service', function () {
|
|||||||
type: 'url'
|
type: 'url'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
(await attribution.getResource()).should.match(({
|
(await attribution.fetchResource()).should.match(({
|
||||||
id: null,
|
id: null,
|
||||||
url: absoluteUrl,
|
url: absoluteUrl,
|
||||||
type: 'url',
|
type: 'url',
|
||||||
@ -60,7 +60,7 @@ describe('Member Attribution Service', function () {
|
|||||||
|
|
||||||
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true, withSubdirectory: true});
|
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true, withSubdirectory: true});
|
||||||
|
|
||||||
(await attribution.getResource()).should.match(({
|
(await attribution.fetchResource()).should.match(({
|
||||||
id: post.id,
|
id: post.id,
|
||||||
url: absoluteUrl,
|
url: absoluteUrl,
|
||||||
type: 'post',
|
type: 'post',
|
||||||
@ -92,7 +92,7 @@ describe('Member Attribution Service', function () {
|
|||||||
// Unpublish this post
|
// Unpublish this post
|
||||||
await models.Post.edit({status: 'draft'}, {id});
|
await models.Post.edit({status: 'draft'}, {id});
|
||||||
|
|
||||||
(await attribution.getResource()).should.match(({
|
(await attribution.fetchResource()).should.match(({
|
||||||
id: null,
|
id: null,
|
||||||
url: absoluteUrl,
|
url: absoluteUrl,
|
||||||
type: 'url',
|
type: 'url',
|
||||||
@ -123,7 +123,7 @@ describe('Member Attribution Service', function () {
|
|||||||
|
|
||||||
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true, withSubdirectory: true});
|
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true, withSubdirectory: true});
|
||||||
|
|
||||||
(await attribution.getResource()).should.match(({
|
(await attribution.fetchResource()).should.match(({
|
||||||
id: post.id,
|
id: post.id,
|
||||||
url: absoluteUrl,
|
url: absoluteUrl,
|
||||||
type: 'page',
|
type: 'page',
|
||||||
@ -150,7 +150,7 @@ describe('Member Attribution Service', function () {
|
|||||||
|
|
||||||
const absoluteUrl = urlService.getUrlByResourceId(tag.id, {absolute: true, withSubdirectory: true});
|
const absoluteUrl = urlService.getUrlByResourceId(tag.id, {absolute: true, withSubdirectory: true});
|
||||||
|
|
||||||
(await attribution.getResource()).should.match(({
|
(await attribution.fetchResource()).should.match(({
|
||||||
id: tag.id,
|
id: tag.id,
|
||||||
url: absoluteUrl,
|
url: absoluteUrl,
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
@ -177,7 +177,7 @@ describe('Member Attribution Service', function () {
|
|||||||
|
|
||||||
const absoluteUrl = urlService.getUrlByResourceId(author.id, {absolute: true, withSubdirectory: true});
|
const absoluteUrl = urlService.getUrlByResourceId(author.id, {absolute: true, withSubdirectory: true});
|
||||||
|
|
||||||
(await attribution.getResource()).should.match(({
|
(await attribution.fetchResource()).should.match(({
|
||||||
id: author.id,
|
id: author.id,
|
||||||
url: absoluteUrl,
|
url: absoluteUrl,
|
||||||
type: 'author',
|
type: 'author',
|
||||||
@ -212,7 +212,7 @@ describe('Member Attribution Service', function () {
|
|||||||
type: 'url'
|
type: 'url'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
(await attribution.getResource()).should.match(({
|
(await attribution.fetchResource()).should.match(({
|
||||||
id: null,
|
id: null,
|
||||||
url: absoluteUrl,
|
url: absoluteUrl,
|
||||||
type: 'url',
|
type: 'url',
|
||||||
@ -245,7 +245,7 @@ describe('Member Attribution Service', function () {
|
|||||||
|
|
||||||
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true, withSubdirectory: true});
|
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true, withSubdirectory: true});
|
||||||
|
|
||||||
(await attribution.getResource()).should.match(({
|
(await attribution.fetchResource()).should.match(({
|
||||||
id: post.id,
|
id: post.id,
|
||||||
url: absoluteUrl,
|
url: absoluteUrl,
|
||||||
type: 'post',
|
type: 'post',
|
||||||
@ -280,7 +280,7 @@ describe('Member Attribution Service', function () {
|
|||||||
// Unpublish this post
|
// Unpublish this post
|
||||||
await models.Post.edit({status: 'draft'}, {id});
|
await models.Post.edit({status: 'draft'}, {id});
|
||||||
|
|
||||||
(await attribution.getResource()).should.match(({
|
(await attribution.fetchResource()).should.match(({
|
||||||
id: null,
|
id: null,
|
||||||
url: absoluteUrl,
|
url: absoluteUrl,
|
||||||
type: 'url',
|
type: 'url',
|
||||||
@ -310,7 +310,7 @@ describe('Member Attribution Service', function () {
|
|||||||
|
|
||||||
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true, withSubdirectory: true});
|
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true, withSubdirectory: true});
|
||||||
|
|
||||||
(await attribution.getResource()).should.match(({
|
(await attribution.fetchResource()).should.match(({
|
||||||
id: post.id,
|
id: post.id,
|
||||||
url: absoluteUrl,
|
url: absoluteUrl,
|
||||||
type: 'page',
|
type: 'page',
|
||||||
@ -338,7 +338,7 @@ describe('Member Attribution Service', function () {
|
|||||||
|
|
||||||
const absoluteUrl = urlService.getUrlByResourceId(tag.id, {absolute: true, withSubdirectory: true});
|
const absoluteUrl = urlService.getUrlByResourceId(tag.id, {absolute: true, withSubdirectory: true});
|
||||||
|
|
||||||
(await attribution.getResource()).should.match(({
|
(await attribution.fetchResource()).should.match(({
|
||||||
id: tag.id,
|
id: tag.id,
|
||||||
url: absoluteUrl,
|
url: absoluteUrl,
|
||||||
type: 'tag',
|
type: 'tag',
|
||||||
@ -366,7 +366,7 @@ describe('Member Attribution Service', function () {
|
|||||||
|
|
||||||
const absoluteUrl = urlService.getUrlByResourceId(author.id, {absolute: true, withSubdirectory: true});
|
const absoluteUrl = urlService.getUrlByResourceId(author.id, {absolute: true, withSubdirectory: true});
|
||||||
|
|
||||||
(await attribution.getResource()).should.match(({
|
(await attribution.fetchResource()).should.match(({
|
||||||
id: author.id,
|
id: author.id,
|
||||||
url: absoluteUrl,
|
url: absoluteUrl,
|
||||||
type: 'author',
|
type: 'author',
|
||||||
|
@ -478,6 +478,27 @@ const fixtures = {
|
|||||||
return models.Product.add(archivedProduct, context.internal);
|
return models.Product.add(archivedProduct, context.internal);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
insertProducts: async function insertProducts() {
|
||||||
|
let coreProductFixtures = fixtureManager.findModelFixtures('Product').entries;
|
||||||
|
await Promise.map(coreProductFixtures, async (product) => {
|
||||||
|
const found = await models.Product.findOne(product, context.internal);
|
||||||
|
if (!found) {
|
||||||
|
await models.Product.add(product, context.internal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const product = await models.Product.findOne({type: 'paid'}, context.internal);
|
||||||
|
|
||||||
|
await Promise.each(_.cloneDeep(DataGenerator.forKnex.stripe_products), function (stripeProduct) {
|
||||||
|
stripeProduct.product_id = product.id;
|
||||||
|
return models.StripeProduct.add(stripeProduct, context.internal);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.each(_.cloneDeep(DataGenerator.forKnex.stripe_prices), function (stripePrice) {
|
||||||
|
return models.StripePrice.add(stripePrice, context.internal);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
insertMembersAndLabelsAndProducts: function insertMembersAndLabelsAndProducts(newsletters = false) {
|
insertMembersAndLabelsAndProducts: function insertMembersAndLabelsAndProducts(newsletters = false) {
|
||||||
return Promise.map(DataGenerator.forKnex.labels, function (label) {
|
return Promise.map(DataGenerator.forKnex.labels, function (label) {
|
||||||
return models.Label.add(label, context.internal);
|
return models.Label.add(label, context.internal);
|
||||||
@ -684,6 +705,9 @@ const toDoList = {
|
|||||||
members: function insertMembersAndLabelsAndProducts() {
|
members: function insertMembersAndLabelsAndProducts() {
|
||||||
return fixtures.insertMembersAndLabelsAndProducts(false);
|
return fixtures.insertMembersAndLabelsAndProducts(false);
|
||||||
},
|
},
|
||||||
|
products: function insertProducts() {
|
||||||
|
return fixtures.insertProducts();
|
||||||
|
},
|
||||||
newsletters: function insertNewsletters() {
|
newsletters: function insertNewsletters() {
|
||||||
return fixtures.insertNewsletters();
|
return fixtures.insertNewsletters();
|
||||||
},
|
},
|
||||||
|
@ -27,35 +27,46 @@ class Attribution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the instance to a parsed instance with more information about the resource included.
|
* Converts the instance to a parsed instance with more information about the resource included.
|
||||||
* It does:
|
* It does:
|
||||||
* - Fetch the resource and add some information about it to the attribution
|
* - Uses the passed model and adds a title to the attribution
|
||||||
* - If the resource exists and have a new url, it updates the url if possible
|
* - If the resource exists and has a new url, it updates the url if possible
|
||||||
* - Returns an absolute URL instead of a relative one
|
* - Returns an absolute URL instead of a relative one
|
||||||
* @returns {Promise<AttributionResource>}
|
* @param {Object|null} [model] The Post/User/Tag model of the resource associated with this attribution
|
||||||
|
* @returns {AttributionResource}
|
||||||
*/
|
*/
|
||||||
async getResource() {
|
getResource(model) {
|
||||||
|
if (!this.id || this.type === 'url' || !this.type || !model) {
|
||||||
|
return {
|
||||||
|
id: null,
|
||||||
|
type: 'url',
|
||||||
|
url: this.#urlTranslator.relativeToAbsolute(this.url),
|
||||||
|
title: this.url
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUrl = this.#urlTranslator.getUrlByResourceId(this.id, {absolute: true});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: model.id,
|
||||||
|
type: this.type,
|
||||||
|
url: updatedUrl,
|
||||||
|
title: model.get('title') ?? model.get('name') ?? this.url
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as getResource, but fetches the model by ID instead of passing it as a parameter
|
||||||
|
*/
|
||||||
|
async fetchResource() {
|
||||||
if (!this.id || this.type === 'url' || !this.type) {
|
if (!this.id || this.type === 'url' || !this.type) {
|
||||||
return {
|
// No fetch required
|
||||||
id: null,
|
return this.getResource();
|
||||||
type: 'url',
|
|
||||||
url: this.#urlTranslator.relativeToAbsolute(this.url),
|
|
||||||
title: this.url
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resource = await this.#urlTranslator.getResourceById(this.id, this.type, {absolute: true});
|
// Fetch model
|
||||||
|
const model = await this.#urlTranslator.getResourceById(this.id, this.type, {absolute: true});
|
||||||
if (resource) {
|
return this.getResource(model);
|
||||||
return resource;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: null,
|
|
||||||
type: 'url',
|
|
||||||
url: this.#urlTranslator.relativeToAbsolute(this.url),
|
|
||||||
title: this.url
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,13 +24,47 @@ class MemberAttributionService {
|
|||||||
return this.attributionBuilder.getAttribution(history);
|
return this.attributionBuilder.getAttribution(history);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the attribution resource for a given event model (MemberCreatedEvent / SubscriptionCreatedEvent), where the model has the required relations already loaded
|
||||||
|
* You need to already load the 'postAttribution', 'userAttribution', and 'tagAttribution' relations
|
||||||
|
* @param {Object} eventModel MemberCreatedEvent or SubscriptionCreatedEvent
|
||||||
|
* @returns {import('./attribution').AttributionResource|null}
|
||||||
|
*/
|
||||||
|
getEventAttribution(eventModel) {
|
||||||
|
if (eventModel.get('attribution_type') === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _attribution = this.attributionBuilder.build({
|
||||||
|
id: eventModel.get('attribution_id'),
|
||||||
|
url: eventModel.get('attribution_url'),
|
||||||
|
type: eventModel.get('attribution_type')
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_attribution.type !== 'url') {
|
||||||
|
// Find the right relation to use to fetch the resource
|
||||||
|
const tryRelations = [
|
||||||
|
eventModel.related('postAttribution'),
|
||||||
|
eventModel.related('userAttribution'),
|
||||||
|
eventModel.related('tagAttribution')
|
||||||
|
];
|
||||||
|
for (const relation of tryRelations) {
|
||||||
|
if (relation && relation.id) {
|
||||||
|
// We need to check the ID, because .related() always returs a model when eager loaded, even when the relation didn't exist
|
||||||
|
return _attribution.getResource(relation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _attribution.getResource(null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the parsed attribution for a member creation event
|
* Returns the parsed attribution for a member creation event
|
||||||
* @param {string} memberId
|
* @param {string} memberId
|
||||||
* @returns {Promise<import('./attribution').AttributionResource|null>}
|
* @returns {Promise<import('./attribution').AttributionResource|null>}
|
||||||
*/
|
*/
|
||||||
async getMemberCreatedAttribution(memberId) {
|
async getMemberCreatedAttribution(memberId) {
|
||||||
const memberCreatedEvent = await this.models.MemberCreatedEvent.findOne({member_id: memberId}, {require: false});
|
const memberCreatedEvent = await this.models.MemberCreatedEvent.findOne({member_id: memberId}, {require: false, withRelated: []});
|
||||||
if (!memberCreatedEvent || !memberCreatedEvent.get('attribution_type')) {
|
if (!memberCreatedEvent || !memberCreatedEvent.get('attribution_type')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -39,7 +73,7 @@ class MemberAttributionService {
|
|||||||
url: memberCreatedEvent.get('attribution_url'),
|
url: memberCreatedEvent.get('attribution_url'),
|
||||||
type: memberCreatedEvent.get('attribution_type')
|
type: memberCreatedEvent.get('attribution_type')
|
||||||
});
|
});
|
||||||
return await attribution.getResource();
|
return await attribution.fetchResource();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,7 +82,7 @@ class MemberAttributionService {
|
|||||||
* @returns {Promise<import('./attribution').AttributionResource|null>}
|
* @returns {Promise<import('./attribution').AttributionResource|null>}
|
||||||
*/
|
*/
|
||||||
async getSubscriptionCreatedAttribution(subscriptionId) {
|
async getSubscriptionCreatedAttribution(subscriptionId) {
|
||||||
const subscriptionCreatedEvent = await this.models.SubscriptionCreatedEvent.findOne({subscription_id: subscriptionId}, {require: false});
|
const subscriptionCreatedEvent = await this.models.SubscriptionCreatedEvent.findOne({subscription_id: subscriptionId}, {require: false, withRelated: []});
|
||||||
if (!subscriptionCreatedEvent || !subscriptionCreatedEvent.get('attribution_type')) {
|
if (!subscriptionCreatedEvent || !subscriptionCreatedEvent.get('attribution_type')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -57,7 +91,7 @@ class MemberAttributionService {
|
|||||||
url: subscriptionCreatedEvent.get('attribution_url'),
|
url: subscriptionCreatedEvent.get('attribution_url'),
|
||||||
type: subscriptionCreatedEvent.get('attribution_type')
|
type: subscriptionCreatedEvent.get('attribution_type')
|
||||||
});
|
});
|
||||||
return await attribution.getResource();
|
return await attribution.fetchResource();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,8 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translate a url into a type and id
|
* Translate a url into, (id+type), or a resource, and vice versa
|
||||||
* And also in reverse
|
|
||||||
*/
|
*/
|
||||||
class UrlTranslator {
|
class UrlTranslator {
|
||||||
/**
|
/**
|
||||||
@ -81,9 +80,11 @@ class UrlTranslator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getResourceById(id, type, options = {absolute: true}) {
|
getUrlByResourceId(id, options = {absolute: true}) {
|
||||||
const url = this.urlService.getUrlByResourceId(id, options);
|
return this.urlService.getUrlByResourceId(id, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getResourceById(id, type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'post':
|
case 'post':
|
||||||
case 'page': {
|
case 'page': {
|
||||||
@ -92,12 +93,7 @@ class UrlTranslator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return post;
|
||||||
id: post.id,
|
|
||||||
type,
|
|
||||||
url,
|
|
||||||
title: post.get('title')
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
case 'author': {
|
case 'author': {
|
||||||
const user = await this.models.User.findOne({id}, {require: false});
|
const user = await this.models.User.findOne({id}, {require: false});
|
||||||
@ -105,12 +101,7 @@ class UrlTranslator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return user;
|
||||||
id: user.id,
|
|
||||||
type,
|
|
||||||
url,
|
|
||||||
title: user.get('name')
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
case 'tag': {
|
case 'tag': {
|
||||||
const tag = await this.models.Tag.findOne({id}, {require: false});
|
const tag = await this.models.Tag.findOne({id}, {require: false});
|
||||||
@ -118,12 +109,7 @@ class UrlTranslator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return tag;
|
||||||
id: tag.id,
|
|
||||||
type,
|
|
||||||
url,
|
|
||||||
title: tag.get('name')
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -25,17 +25,20 @@ describe('AttributionBuilder', function () {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
getResourceById(id, type) {
|
getResourceById(id) {
|
||||||
if (id === 'invalid') {
|
if (id === 'invalid') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
type,
|
get() {
|
||||||
url: 'https://absolute/dir/path',
|
return 'Title';
|
||||||
title: 'Title'
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getUrlByResourceId() {
|
||||||
|
return 'https://absolute/dir/path';
|
||||||
|
},
|
||||||
relativeToAbsolute(path) {
|
relativeToAbsolute(path) {
|
||||||
return 'https://absolute/dir' + path;
|
return 'https://absolute/dir' + path;
|
||||||
},
|
},
|
||||||
@ -105,7 +108,7 @@ describe('AttributionBuilder', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Returns post resource', async function () {
|
it('Returns post resource', async function () {
|
||||||
should(await attributionBuilder.build({type: 'post', id: '123', url: '/post'}).getResource()).match({
|
should(await attributionBuilder.build({type: 'post', id: '123', url: '/post'}).fetchResource()).match({
|
||||||
type: 'post',
|
type: 'post',
|
||||||
id: '123',
|
id: '123',
|
||||||
url: 'https://absolute/dir/path',
|
url: 'https://absolute/dir/path',
|
||||||
@ -114,7 +117,7 @@ describe('AttributionBuilder', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Returns url resource', async function () {
|
it('Returns url resource', async function () {
|
||||||
should(await attributionBuilder.build({type: 'url', id: null, url: '/url'}).getResource()).match({
|
should(await attributionBuilder.build({type: 'url', id: null, url: '/url'}).fetchResource()).match({
|
||||||
type: 'url',
|
type: 'url',
|
||||||
id: null,
|
id: null,
|
||||||
url: 'https://absolute/dir/url',
|
url: 'https://absolute/dir/url',
|
||||||
@ -123,7 +126,7 @@ describe('AttributionBuilder', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Returns url resource if not found', async function () {
|
it('Returns url resource if not found', async function () {
|
||||||
should(await attributionBuilder.build({type: 'post', id: 'invalid', url: '/post'}).getResource()).match({
|
should(await attributionBuilder.build({type: 'post', id: 'invalid', url: '/post'}).fetchResource()).match({
|
||||||
type: 'url',
|
type: 'url',
|
||||||
id: null,
|
id: null,
|
||||||
url: 'https://absolute/dir/post',
|
url: 'https://absolute/dir/post',
|
||||||
|
@ -9,4 +9,97 @@ describe('MemberAttributionService', function () {
|
|||||||
new MemberAttributionService({});
|
new MemberAttributionService({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getEventAttribution', function () {
|
||||||
|
it('returns null if attribution_type is null', function () {
|
||||||
|
const service = new MemberAttributionService({});
|
||||||
|
const model = {
|
||||||
|
id: 'event_id',
|
||||||
|
get() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
should(service.getEventAttribution(model)).eql(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns url attribution types', function () {
|
||||||
|
const service = new MemberAttributionService({
|
||||||
|
attributionBuilder: {
|
||||||
|
build(attribution) {
|
||||||
|
return {
|
||||||
|
...attribution,
|
||||||
|
getResource() {
|
||||||
|
return {
|
||||||
|
...attribution,
|
||||||
|
title: 'added'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const model = {
|
||||||
|
id: 'event_id',
|
||||||
|
get(name) {
|
||||||
|
if (name === 'attribution_type') {
|
||||||
|
return 'url';
|
||||||
|
}
|
||||||
|
if (name === 'attribution_url') {
|
||||||
|
return '/my/url/';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
should(service.getEventAttribution(model)).eql({
|
||||||
|
id: null,
|
||||||
|
type: 'url',
|
||||||
|
url: '/my/url/',
|
||||||
|
title: 'added'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns first loaded relation', function () {
|
||||||
|
const service = new MemberAttributionService({
|
||||||
|
attributionBuilder: {
|
||||||
|
build(attribution) {
|
||||||
|
return {
|
||||||
|
...attribution,
|
||||||
|
getResource() {
|
||||||
|
return {
|
||||||
|
...attribution,
|
||||||
|
title: 'added'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const model = {
|
||||||
|
id: 'event_id',
|
||||||
|
get(name) {
|
||||||
|
if (name === 'attribution_type') {
|
||||||
|
return 'user';
|
||||||
|
}
|
||||||
|
if (name === 'attribution_url') {
|
||||||
|
return '/my/url/';
|
||||||
|
}
|
||||||
|
return 'test_user_id';
|
||||||
|
},
|
||||||
|
related(name) {
|
||||||
|
if (name === 'userAttribution') {
|
||||||
|
return {
|
||||||
|
id: 'test_user_id'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
should(service.getEventAttribution(model)).eql({
|
||||||
|
id: 'test_user_id',
|
||||||
|
type: 'user',
|
||||||
|
url: '/my/url/',
|
||||||
|
title: 'added'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -111,38 +111,26 @@ describe('UrlTranslator', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns for post', async function () {
|
it('returns for post', async function () {
|
||||||
should(await translator.getResourceById('id', 'post')).eql({
|
should(await translator.getResourceById('id', 'post')).match({
|
||||||
type: 'post',
|
id: 'post_id'
|
||||||
id: 'post_id',
|
|
||||||
title: 'Title',
|
|
||||||
url: '/path'
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns for page', async function () {
|
it('returns for page', async function () {
|
||||||
should(await translator.getResourceById('id', 'page')).eql({
|
should(await translator.getResourceById('id', 'page')).match({
|
||||||
type: 'page',
|
id: 'post_id'
|
||||||
id: 'post_id',
|
|
||||||
title: 'Title',
|
|
||||||
url: '/path'
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns for tag', async function () {
|
it('returns for tag', async function () {
|
||||||
should(await translator.getResourceById('id', 'tag')).eql({
|
should(await translator.getResourceById('id', 'tag')).match({
|
||||||
type: 'tag',
|
id: 'tag_id'
|
||||||
id: 'tag_id',
|
|
||||||
title: 'Title',
|
|
||||||
url: '/path'
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns for user', async function () {
|
it('returns for user', async function () {
|
||||||
should(await translator.getResourceById('id', 'author')).eql({
|
should(await translator.getResourceById('id', 'author')).match({
|
||||||
type: 'author',
|
id: 'user_id'
|
||||||
id: 'user_id',
|
|
||||||
title: 'Title',
|
|
||||||
url: '/path'
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -48,6 +48,8 @@ module.exports = function MembersAPI({
|
|||||||
MemberProductEvent,
|
MemberProductEvent,
|
||||||
MemberEmailChangeEvent,
|
MemberEmailChangeEvent,
|
||||||
MemberAnalyticEvent,
|
MemberAnalyticEvent,
|
||||||
|
MemberCreatedEvent,
|
||||||
|
SubscriptionCreatedEvent,
|
||||||
Offer,
|
Offer,
|
||||||
OfferRedemption,
|
OfferRedemption,
|
||||||
StripeProduct,
|
StripeProduct,
|
||||||
@ -105,8 +107,11 @@ module.exports = function MembersAPI({
|
|||||||
MemberPaymentEvent,
|
MemberPaymentEvent,
|
||||||
MemberStatusEvent,
|
MemberStatusEvent,
|
||||||
MemberLoginEvent,
|
MemberLoginEvent,
|
||||||
|
MemberCreatedEvent,
|
||||||
|
SubscriptionCreatedEvent,
|
||||||
Comment,
|
Comment,
|
||||||
labsService
|
labsService,
|
||||||
|
memberAttributionService
|
||||||
});
|
});
|
||||||
|
|
||||||
const memberBREADService = new MemberBREADService({
|
const memberBREADService = new MemberBREADService({
|
||||||
|
@ -8,9 +8,12 @@ module.exports = class EventRepository {
|
|||||||
MemberPaymentEvent,
|
MemberPaymentEvent,
|
||||||
MemberStatusEvent,
|
MemberStatusEvent,
|
||||||
MemberLoginEvent,
|
MemberLoginEvent,
|
||||||
|
MemberCreatedEvent,
|
||||||
|
SubscriptionCreatedEvent,
|
||||||
MemberPaidSubscriptionEvent,
|
MemberPaidSubscriptionEvent,
|
||||||
Comment,
|
Comment,
|
||||||
labsService
|
labsService,
|
||||||
|
memberAttributionService
|
||||||
}) {
|
}) {
|
||||||
this._MemberSubscribeEvent = MemberSubscribeEvent;
|
this._MemberSubscribeEvent = MemberSubscribeEvent;
|
||||||
this._MemberPaidSubscriptionEvent = MemberPaidSubscriptionEvent;
|
this._MemberPaidSubscriptionEvent = MemberPaidSubscriptionEvent;
|
||||||
@ -20,6 +23,9 @@ module.exports = class EventRepository {
|
|||||||
this._EmailRecipient = EmailRecipient;
|
this._EmailRecipient = EmailRecipient;
|
||||||
this._Comment = Comment;
|
this._Comment = Comment;
|
||||||
this._labsService = labsService;
|
this._labsService = labsService;
|
||||||
|
this._MemberCreatedEvent = MemberCreatedEvent;
|
||||||
|
this._SubscriptionCreatedEvent = SubscriptionCreatedEvent;
|
||||||
|
this._memberAttributionService = memberAttributionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerPayment(data) {
|
async registerPayment(data) {
|
||||||
@ -62,6 +68,7 @@ module.exports = class EventRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSubscriptionEvents(options = {}, filters = {}) {
|
async getSubscriptionEvents(options = {}, filters = {}) {
|
||||||
|
if (!this._labsService.isSet('memberAttribution')){
|
||||||
options = {
|
options = {
|
||||||
...options,
|
...options,
|
||||||
withRelated: ['member'],
|
withRelated: ['member'],
|
||||||
@ -90,6 +97,37 @@ module.exports = class EventRepository {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options = {
|
||||||
|
...options,
|
||||||
|
withRelated: ['member', 'subscriptionCreatedEvent.postAttribution', 'subscriptionCreatedEvent.userAttribution', 'subscriptionCreatedEvent.tagAttribution'],
|
||||||
|
filter: []
|
||||||
|
};
|
||||||
|
if (filters['data.created_at']) {
|
||||||
|
options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'created_at:'));
|
||||||
|
}
|
||||||
|
if (filters['data.member_id']) {
|
||||||
|
options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:'));
|
||||||
|
}
|
||||||
|
options.filter = options.filter.join('+');
|
||||||
|
|
||||||
|
const {data: models, meta} = await this._MemberPaidSubscriptionEvent.findPage(options);
|
||||||
|
|
||||||
|
const data = models.map((model) => {
|
||||||
|
return {
|
||||||
|
type: 'subscription_event',
|
||||||
|
data: {
|
||||||
|
...model.toJSON(options),
|
||||||
|
attribution: model.get('type') === 'created' && model.related('subscriptionCreatedEvent') && model.related('subscriptionCreatedEvent').id ? this._memberAttributionService.getEventAttribution(model.related('subscriptionCreatedEvent')) : null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
meta
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async getPaymentEvents(options = {}, filters = {}) {
|
async getPaymentEvents(options = {}, filters = {}) {
|
||||||
options = {
|
options = {
|
||||||
...options,
|
...options,
|
||||||
@ -149,6 +187,7 @@ module.exports = class EventRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSignupEvents(options = {}, filters = {}) {
|
async getSignupEvents(options = {}, filters = {}) {
|
||||||
|
if (!this._labsService.isSet('memberAttribution')){
|
||||||
options = {
|
options = {
|
||||||
...options,
|
...options,
|
||||||
withRelated: ['member'],
|
withRelated: ['member'],
|
||||||
@ -177,6 +216,37 @@ module.exports = class EventRepository {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options = {
|
||||||
|
...options,
|
||||||
|
withRelated: ['member', 'postAttribution', 'userAttribution', 'tagAttribution'],
|
||||||
|
filter: []
|
||||||
|
};
|
||||||
|
if (filters['data.created_at']) {
|
||||||
|
options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'created_at:'));
|
||||||
|
}
|
||||||
|
if (filters['data.member_id']) {
|
||||||
|
options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:'));
|
||||||
|
}
|
||||||
|
options.filter = options.filter.join('+');
|
||||||
|
|
||||||
|
const {data: models, meta} = await this._MemberCreatedEvent.findPage(options);
|
||||||
|
|
||||||
|
const data = models.map((model) => {
|
||||||
|
return {
|
||||||
|
type: 'signup_event',
|
||||||
|
data: {
|
||||||
|
...model.toJSON(options),
|
||||||
|
attribution: this._memberAttributionService.getEventAttribution(model)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
meta
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async getCommentEvents(options = {}, filters = {}) {
|
async getCommentEvents(options = {}, filters = {}) {
|
||||||
options = {
|
options = {
|
||||||
...options,
|
...options,
|
||||||
|
Loading…
Reference in New Issue
Block a user