mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-15 03:01:37 -05:00
Added post_id filter and total to activity feed API (#15650)
fixes https://github.com/TryGhost/Team/issues/2091 fixes https://github.com/TryGhost/Team/issues/2089 - Added new fixtures to make testing easier for the activity feed - Improved E2E test coverage of activity feed with separate test file - Added data.post_id filter to enable filtering by events related to a given post - Fixed return types in JSDoc of test agents (TypeScript interprets these as `typeof Agent` if we don't add `InstanceType<Agent>`) - Added total pagination metadata to activity feed API (to allow a basic type of pagination using filters)
This commit is contained in:
parent
2f43e71f7f
commit
a01fb5f1aa
16 changed files with 1051 additions and 89 deletions
|
@ -435,10 +435,7 @@ module.exports = {
|
|||
method: 'browse'
|
||||
},
|
||||
async query(frame) {
|
||||
const events = await membersService.api.events.getEventTimeline(frame.options);
|
||||
return {
|
||||
events
|
||||
};
|
||||
return await membersService.api.events.getEventTimeline(frame.options);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -76,11 +76,12 @@ function bulkAction(bulkActionResult, _apiConfig, frame) {
|
|||
|
||||
/**
|
||||
*
|
||||
* @returns {{events: any[]}}
|
||||
* @returns {{events: any[], meta: any}}
|
||||
*/
|
||||
function activityFeed(data, _apiConfig, frame) {
|
||||
return {
|
||||
events: data.events.map(e => mappers.activityFeedEvents(e, frame))
|
||||
events: data.events.map(e => mappers.activityFeedEvents(e, frame)),
|
||||
meta: data.meta
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,31 @@ const MemberClickEvent = ghostBookshelf.Model.extend({
|
|||
|
||||
member() {
|
||||
return this.belongsTo('Member', 'member_id', 'id');
|
||||
},
|
||||
|
||||
filterExpansions: function filterExpansions() {
|
||||
const expansions = [{
|
||||
key: 'post_id',
|
||||
replacement: 'link.post_id'
|
||||
}];
|
||||
|
||||
return expansions;
|
||||
},
|
||||
|
||||
filterRelations() {
|
||||
return {
|
||||
link: {
|
||||
// Mongo-knex doesn't support belongsTo relations
|
||||
tableName: 'redirects',
|
||||
tableNameAs: 'link',
|
||||
type: 'manyToMany',
|
||||
joinTable: 'members_click_events',
|
||||
joinFrom: 'id',
|
||||
joinTo: 'redirect_id'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}, {
|
||||
async edit() {
|
||||
throw new errors.IncorrectUsageError({message: 'Cannot edit MemberClickEvent'});
|
||||
|
|
|
@ -25,6 +25,21 @@ const MemberPaidSubscriptionEvent = ghostBookshelf.Model.extend({
|
|||
.groupByRaw('currency, DATE(created_at)')
|
||||
.orderByRaw('DATE(created_at)');
|
||||
}
|
||||
},
|
||||
|
||||
filterRelations() {
|
||||
return {
|
||||
subscriptionCreatedEvent: {
|
||||
// Mongo-knex doesn't support belongsTo relations
|
||||
tableName: 'members_subscription_created_events',
|
||||
tableNameAs: 'subscriptionCreatedEvent',
|
||||
type: 'manyToMany',
|
||||
joinTable: 'members_paid_subscription_events',
|
||||
joinFrom: 'id',
|
||||
joinToForeign: 'subscription_id',
|
||||
joinTo: 'subscription_id'
|
||||
}
|
||||
};
|
||||
}
|
||||
}, {
|
||||
permittedOptions(methodName) {
|
||||
|
|
|
@ -0,0 +1,607 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Activity Feed API Can filter events by post id 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>,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"limit": 10,
|
||||
"next": null,
|
||||
"page": null,
|
||||
"pages": 1,
|
||||
"prev": null,
|
||||
"total": 8,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Activity Feed API Can filter events by post id 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": "15074",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Activity Feed API Can limit events 1: [body] 1`] = `
|
||||
Object {
|
||||
"events": Array [
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": Any<String>,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"limit": "2",
|
||||
"next": null,
|
||||
"page": null,
|
||||
"pages": 4,
|
||||
"prev": null,
|
||||
"total": 8,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Activity Feed API Can limit events 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": "1239",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Activity Feed API Returns click events in activity feed 1: [body] 1`] = `
|
||||
Object {
|
||||
"events": Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"link": Object {
|
||||
"from": "/r/0",
|
||||
"to": "https:://ghost.org",
|
||||
},
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "member1@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Mr Egg",
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "HTML Ipsum",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"link": Object {
|
||||
"from": "/r/1",
|
||||
"to": "https:://ghost.org",
|
||||
},
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "member2@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": null,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "Ghostly Kitchen Sink",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"link": Object {
|
||||
"from": "/r/2",
|
||||
"to": "https:://ghost.org",
|
||||
},
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "paid@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Egon Spengler",
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "Short and Sweet",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"link": Object {
|
||||
"from": "/r/3",
|
||||
"to": "https:://ghost.org",
|
||||
},
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "trialing@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Ray Stantz",
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "Not finished yet",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"link": Object {
|
||||
"from": "/r/4",
|
||||
"to": "https:://ghost.org",
|
||||
},
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "comped@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Vinz Clortho",
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "Not so short, bit complex",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"link": Object {
|
||||
"from": "/r/5",
|
||||
"to": "https:://ghost.org",
|
||||
},
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "vip@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Winston Zeddemore",
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "This is a static page",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"link": Object {
|
||||
"from": "/r/6",
|
||||
"to": "https:://ghost.org",
|
||||
},
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "vip-paid@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Peter Venkman",
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "This is a draft static page",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"link": Object {
|
||||
"from": "/r/7",
|
||||
"to": "https:://ghost.org",
|
||||
},
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "with-product@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Dana Barrett",
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "This is a scheduled post!!",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"limit": 10,
|
||||
"next": null,
|
||||
"page": null,
|
||||
"pages": 1,
|
||||
"prev": null,
|
||||
"total": 8,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Activity Feed API Returns click events 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": "3722",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Activity Feed API Returns comments in activity feed 1: [body] 1`] = `
|
||||
Object {
|
||||
"events": Array [
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": Any<String>,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"limit": 10,
|
||||
"next": null,
|
||||
"page": null,
|
||||
"pages": 1,
|
||||
"prev": null,
|
||||
"total": 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Activity Feed API Returns comments 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": "1238",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Activity Feed API Returns feedback events in activity feed 1: [body] 1`] = `
|
||||
Object {
|
||||
"events": Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "member1@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Mr Egg",
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "HTML Ipsum",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"score": Any<Number>,
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "member2@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": null,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "Ghostly Kitchen Sink",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"score": Any<Number>,
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "paid@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Egon Spengler",
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "Short and Sweet",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"score": Any<Number>,
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "trialing@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Ray Stantz",
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "Not finished yet",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"score": Any<Number>,
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "comped@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Vinz Clortho",
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "Not so short, bit complex",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"score": Any<Number>,
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "vip@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Winston Zeddemore",
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "This is a static page",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"score": Any<Number>,
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "vip-paid@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Peter Venkman",
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "This is a draft static page",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"score": Any<Number>,
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"member": Object {
|
||||
"avatar_image": null,
|
||||
"email": "with-product@test.com",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"name": "Dana Barrett",
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"post": Object {
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"title": "This is a scheduled post!!",
|
||||
"url": Any<String>,
|
||||
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
|
||||
},
|
||||
"score": Any<Number>,
|
||||
},
|
||||
"type": Any<String>,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"limit": 10,
|
||||
"next": null,
|
||||
"page": null,
|
||||
"pages": 1,
|
||||
"prev": null,
|
||||
"total": 8,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Activity Feed API Returns feedback events 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": "3690",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Activity Feed API Returns signup events 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>,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"limit": 10,
|
||||
"next": null,
|
||||
"page": null,
|
||||
"pages": 1,
|
||||
"prev": null,
|
||||
"total": 8,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Activity Feed API Returns signup events 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": "23027",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
|
@ -274,6 +274,12 @@ Object {
|
|||
"type": Any<String>,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"limit": 10,
|
||||
"total": 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -281,7 +287,56 @@ exports[`Members API - member attribution Returns sign up attributions in activi
|
|||
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": "9204",
|
||||
"content-length": "9249",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API - member attribution Returns sign up attributions of all types 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>,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"limit": 10,
|
||||
"next": null,
|
||||
"page": null,
|
||||
"pages": 1,
|
||||
"prev": null,
|
||||
"total": 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API - member attribution Returns sign up attributions of all types 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": "9295",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
|
@ -4002,7 +4057,7 @@ exports[`Members API Can subscribe to a newsletter 5: [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": "5053",
|
||||
"content-length": "5144",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
|
|
|
@ -96,8 +96,8 @@ Object {
|
|||
"count": 1,
|
||||
"date": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
|
||||
"negative_delta": 0,
|
||||
"positive_delta": 1,
|
||||
"signups": 1,
|
||||
"positive_delta": 4,
|
||||
"signups": 4,
|
||||
"tier": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
},
|
||||
Object {
|
||||
|
|
178
ghost/core/test/e2e-api/admin/activity-feed.test.js
Normal file
178
ghost/core/test/e2e-api/admin/activity-feed.test.js
Normal file
|
@ -0,0 +1,178 @@
|
|||
const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework');
|
||||
const {anyEtag, anyObjectId, anyUuid, anyISODate, anyString, anyObject, anyNumber} = matchers;
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
let agent;
|
||||
describe('Activity Feed API', function () {
|
||||
before(async function () {
|
||||
agent = await agentProvider.getAdminAPIAgent();
|
||||
await fixtureManager.init('posts', 'newsletters', 'members:newsletters', 'comments', 'redirects', 'clicks', 'feedback');
|
||||
await agent.loginAsOwner();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
mockManager.mockStripe();
|
||||
mockManager.mockMail();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
mockManager.restore();
|
||||
});
|
||||
|
||||
// Activity feed
|
||||
it('Returns comments in activity feed', async function () {
|
||||
// Check activity feed
|
||||
await agent
|
||||
.get(`/members/events?filter=type:comment_event`)
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
events: new Array(2).fill({
|
||||
type: anyString,
|
||||
data: anyObject
|
||||
})
|
||||
})
|
||||
.expect(({body}) => {
|
||||
assert(body.events.find(e => e.type === 'comment_event'), 'Expected a comment event');
|
||||
assert(!body.events.find(e => e.type !== 'comment_event'), 'Expected only comment events');
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns click events in activity feed', async function () {
|
||||
// Check activity feed
|
||||
await agent
|
||||
.get(`/members/events?filter=type:click_event`)
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
events: new Array(8).fill({
|
||||
type: anyString,
|
||||
data: {
|
||||
created_at: anyISODate,
|
||||
member: {
|
||||
id: anyObjectId,
|
||||
uuid: anyUuid
|
||||
},
|
||||
post: {
|
||||
id: anyObjectId,
|
||||
uuid: anyUuid,
|
||||
url: anyString
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.expect(({body}) => {
|
||||
assert(body.events.find(e => e.type === 'click_event'), 'Expected a click event');
|
||||
assert(!body.events.find(e => e.type !== 'click_event'), 'Expected only click events');
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns feedback events in activity feed', async function () {
|
||||
// Check activity feed
|
||||
await agent
|
||||
.get(`/members/events?filter=type:feedback_event`)
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
events: new Array(8).fill({
|
||||
type: anyString,
|
||||
data: {
|
||||
created_at: anyISODate,
|
||||
id: anyObjectId,
|
||||
member: {
|
||||
id: anyObjectId,
|
||||
uuid: anyUuid
|
||||
},
|
||||
post: {
|
||||
id: anyObjectId,
|
||||
uuid: anyUuid,
|
||||
url: anyString
|
||||
},
|
||||
score: anyNumber
|
||||
}
|
||||
})
|
||||
})
|
||||
.expect(({body}) => {
|
||||
assert(body.events.find(e => e.type === 'feedback_event'), 'Expected a feedback event');
|
||||
assert(!body.events.find(e => e.type !== 'feedback_event'), 'Expected only feedback events');
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns signup events 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(8).fill({
|
||||
type: anyString,
|
||||
data: anyObject
|
||||
})
|
||||
})
|
||||
.expect(({body}) => {
|
||||
assert(body.events.find(e => e.type === 'signup_event'), 'Expected a signup event');
|
||||
assert(!body.events.find(e => e.type !== 'signup_event'), 'Expected only signup events');
|
||||
});
|
||||
});
|
||||
|
||||
it('Can filter events by post id', async function () {
|
||||
const postId = fixtureManager.get('posts', 0).id;
|
||||
await agent
|
||||
.get(`/members/events?filter=data.post_id:${postId}`)
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
events: new Array(8).fill({
|
||||
type: anyString,
|
||||
data: anyObject
|
||||
})
|
||||
})
|
||||
.expect(({body}) => {
|
||||
assert(!body.events.find(e => (e.data?.post?.id ?? e.data?.attribution?.id) !== postId), 'Should only return events for the post');
|
||||
|
||||
// Check all post_id event types are covered by this test
|
||||
assert(body.events.find(e => e.type === 'click_event'), 'Expected a click event');
|
||||
assert(body.events.find(e => e.type === 'comment_event'), 'Expected a comment event');
|
||||
assert(body.events.find(e => e.type === 'feedback_event'), 'Expected a feedback event');
|
||||
assert(body.events.find(e => e.type === 'signup_event'), 'Expected a signup event');
|
||||
assert(body.events.find(e => e.type === 'subscription_event'), 'Expected a subscription event');
|
||||
|
||||
// Assert total is correct
|
||||
assert.equal(body.meta.pagination.total, 8);
|
||||
});
|
||||
});
|
||||
|
||||
it('Can limit events', async function () {
|
||||
const postId = fixtureManager.get('posts', 0).id;
|
||||
await agent
|
||||
.get(`/members/events?filter=data.post_id:${postId}&limit=2`)
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
events: new Array(2).fill({
|
||||
type: anyString,
|
||||
data: anyObject
|
||||
})
|
||||
})
|
||||
.expect(({body}) => {
|
||||
assert(!body.events.find(e => (e.data?.post?.id ?? e.data?.attribution?.id) !== postId), 'Should only return events for the post');
|
||||
|
||||
// Assert total is correct
|
||||
assert.equal(body.meta.pagination.total, 8);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -390,7 +390,7 @@ describe('Members API - member attribution', function () {
|
|||
});
|
||||
|
||||
// Activity feed
|
||||
it('Returns sign up attributions in activity feed', async function () {
|
||||
it('Returns sign up attributions of all types in activity feed', async function () {
|
||||
// Check activity feed
|
||||
await agent
|
||||
.get(`/members/events/?filter=type:signup_event`)
|
||||
|
@ -431,56 +431,6 @@ describe('Members API', function () {
|
|||
mockManager.restore();
|
||||
});
|
||||
|
||||
// Activity feed
|
||||
it('Returns comments in activity feed', async function () {
|
||||
// Check activity feed
|
||||
await agent
|
||||
.get(`/members/events?filter=type:comment_event`)
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
events: new Array(2).fill({
|
||||
type: anyString,
|
||||
data: anyObject
|
||||
})
|
||||
})
|
||||
.expect(({body}) => {
|
||||
should(body.events.find(e => e.type === 'comment_event')).not.be.undefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns click events in activity feed', async function () {
|
||||
// Check activity feed
|
||||
await agent
|
||||
.get(`/members/events?filter=type:click_event`)
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot({
|
||||
events: new Array(8).fill({
|
||||
type: anyString,
|
||||
data: {
|
||||
created_at: anyISODate,
|
||||
member: {
|
||||
id: anyObjectId,
|
||||
uuid: anyUuid
|
||||
},
|
||||
post: {
|
||||
id: anyObjectId,
|
||||
uuid: anyUuid,
|
||||
url: anyString
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.expect(({body}) => {
|
||||
should(body.events.find(e => e.type === 'click_event')).not.be.undefined();
|
||||
});
|
||||
});
|
||||
|
||||
// List Members
|
||||
|
||||
it('Can browse', async function () {
|
||||
|
@ -1801,6 +1751,9 @@ describe('Members API', function () {
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'signup_event'
|
||||
},
|
||||
{
|
||||
type: 'newsletter_event',
|
||||
data: {
|
||||
|
@ -1811,9 +1764,6 @@ describe('Members API', function () {
|
|||
id: newsletters[0].id
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'signup_event'
|
||||
}
|
||||
]);
|
||||
|
||||
|
|
|
@ -372,6 +372,16 @@ Object {
|
|||
"type": Any<String>,
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"limit": 10,
|
||||
"next": null,
|
||||
"page": null,
|
||||
"pages": 1,
|
||||
"prev": null,
|
||||
"total": 8,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -391,7 +401,7 @@ exports[`Members API Member attribution Returns subscription created attribution
|
|||
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": "14722",
|
||||
"content-length": "14813",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
|
@ -402,6 +412,16 @@ Object {
|
|||
exports[`Members API Member attribution empty initial activity feed 1: [body] 1`] = `
|
||||
Object {
|
||||
"events": Array [],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"limit": 10,
|
||||
"next": null,
|
||||
"page": null,
|
||||
"pages": 0,
|
||||
"prev": null,
|
||||
"total": 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -409,7 +429,7 @@ exports[`Members API Member attribution empty initial activity feed 2: [headers]
|
|||
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-length": "104",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Version, Origin, Accept-Encoding",
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
const sinon = require('sinon');
|
||||
const models = require('../../../../core/server/models');
|
||||
|
||||
describe('Unit: models/MemberClickEvent', function () {
|
||||
before(function () {
|
||||
models.init();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('Has link and member relations', function () {
|
||||
const model = models.MemberClickEvent.forge({id: 'any'});
|
||||
model.link();
|
||||
model.member();
|
||||
});
|
||||
|
||||
it('Has filter relations', function () {
|
||||
const model = models.MemberClickEvent.forge({id: 'any'});
|
||||
model.filterRelations();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
const sinon = require('sinon');
|
||||
const models = require('../../../../core/server/models');
|
||||
|
||||
describe('Unit: models/MemberPaidSubscriptionEvent', function () {
|
||||
before(function () {
|
||||
models.init();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('Has member and subscriptionCreatedEvent relations', function () {
|
||||
const model = models.MemberPaidSubscriptionEvent.forge({id: 'any'});
|
||||
model.member();
|
||||
model.subscriptionCreatedEvent();
|
||||
});
|
||||
|
||||
it('Has filter relations', function () {
|
||||
const model = models.MemberPaidSubscriptionEvent.forge({id: 'any'});
|
||||
model.filterRelations();
|
||||
});
|
||||
});
|
|
@ -158,7 +158,7 @@ const resetData = async () => {
|
|||
* Creates a ContentAPITestAgent which is a drop-in substitution for supertest.
|
||||
* It is automatically hooked up to the Content API so you can make requests to e.g.
|
||||
* agent.get('/posts/') without having to worry about URL paths
|
||||
* @returns {Promise<ContentAPITestAgent>} agent
|
||||
* @returns {Promise<InstanceType<ContentAPITestAgent>>} agent
|
||||
*/
|
||||
const getContentAPIAgent = async () => {
|
||||
try {
|
||||
|
@ -182,7 +182,7 @@ const getContentAPIAgent = async () => {
|
|||
*
|
||||
* @param {Object} [options={}]
|
||||
* @param {Boolean} [options.members] Include members in the boot process
|
||||
* @returns {Promise<AdminAPITestAgent>} agent
|
||||
* @returns {Promise<InstanceType<AdminAPITestAgent>>} agent
|
||||
*/
|
||||
const getAdminAPIAgent = async (options = {}) => {
|
||||
const bootOptions = {};
|
||||
|
@ -210,7 +210,7 @@ const getAdminAPIAgent = async (options = {}) => {
|
|||
* It is automatically hooked up to the Members API so you can make requests to e.g.
|
||||
* agent.get('/webhooks/stripe/') without having to worry about URL paths
|
||||
*
|
||||
* @returns {Promise<MembersAPITestAgent>} agent
|
||||
* @returns {Promise<InstanceType<MembersAPITestAgent>>} agent
|
||||
*/
|
||||
const getMembersAPIAgent = async () => {
|
||||
const bootOptions = {
|
||||
|
@ -235,7 +235,7 @@ const getMembersAPIAgent = async () => {
|
|||
* It is automatically hooked up to the Ghost API so you can make requests to e.g.
|
||||
* agent.get('/well-known/jwks.json') without having to worry about URL paths
|
||||
*
|
||||
* @returns {Promise<GhostAPITestAgent>} agent
|
||||
* @returns {Promise<InstanceType<GhostAPITestAgent>>} agent
|
||||
*/
|
||||
const getGhostAPIAgent = async () => {
|
||||
const bootOptions = {
|
||||
|
@ -258,7 +258,7 @@ const getGhostAPIAgent = async () => {
|
|||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<{adminAgent: AdminAPITestAgent, membersAgent: MembersAPITestAgent}>} agents
|
||||
* @returns {Promise<{adminAgent: InstanceType<AdminAPITestAgent>, membersAgent: InstanceType<MembersAPITestAgent>}>} agents
|
||||
*/
|
||||
const getAgentsForMembers = async () => {
|
||||
let membersAgent;
|
||||
|
|
|
@ -655,6 +655,12 @@ const fixtures = {
|
|||
}));
|
||||
},
|
||||
|
||||
insertFeedback: async function insertFeedback() {
|
||||
await Promise.all(DataGenerator.forKnex.members_feedback.map((feedback) => {
|
||||
return models.MemberFeedback.add(feedback, context.internal);
|
||||
}));
|
||||
},
|
||||
|
||||
insertSnippets: function insertSnippets() {
|
||||
return Promise.map(DataGenerator.forKnex.snippets, function (snippet) {
|
||||
return models.Snippet.add(snippet, context.internal);
|
||||
|
@ -794,6 +800,9 @@ const toDoList = {
|
|||
},
|
||||
clicks: function insertClicks() {
|
||||
return fixtures.insertClicks();
|
||||
},
|
||||
feedback: function insertFeedback() {
|
||||
return fixtures.insertFeedback();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1659,7 +1659,20 @@ DataGenerator.forKnex = (function () {
|
|||
const members_paid_subscription_events = [
|
||||
createBasic(DataGenerator.Content.members_paid_subscription_events[0]),
|
||||
createBasic(DataGenerator.Content.members_paid_subscription_events[1]),
|
||||
createBasic(DataGenerator.Content.members_paid_subscription_events[2])
|
||||
createBasic(DataGenerator.Content.members_paid_subscription_events[2]),
|
||||
...members_subscription_created_events.map((e) => {
|
||||
return {
|
||||
id: ObjectId().toHexString(),
|
||||
type: 'created',
|
||||
mrr_delta: 1000,
|
||||
currency: 'usd',
|
||||
source: 'stripe',
|
||||
subscription_id: e.subscription_id,
|
||||
member_id: e.member_id,
|
||||
from_plan: null,
|
||||
to_plan: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730bb8'
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
const redirects = posts.map((post, index) => {
|
||||
|
@ -1682,6 +1695,16 @@ DataGenerator.forKnex = (function () {
|
|||
};
|
||||
});
|
||||
|
||||
const members_feedback = posts.map((redirect, index) => {
|
||||
return {
|
||||
id: ObjectId().toHexString(),
|
||||
member_id: members[index].id,
|
||||
post_id: redirect.id,
|
||||
score: index % 2,
|
||||
created_at: new Date()
|
||||
};
|
||||
});
|
||||
|
||||
const snippets = [
|
||||
createBasic(DataGenerator.Content.snippets[0])
|
||||
];
|
||||
|
@ -1761,7 +1784,8 @@ DataGenerator.forKnex = (function () {
|
|||
members_paid_subscription_events,
|
||||
members_created_events,
|
||||
members_subscription_created_events,
|
||||
members_click_events
|
||||
members_click_events,
|
||||
members_feedback
|
||||
};
|
||||
}());
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ module.exports = class EventRepository {
|
|||
if (!options.limit) {
|
||||
options.limit = 10;
|
||||
}
|
||||
let filters = this.getNQLSubset(options.filter);
|
||||
|
||||
// Changing this order might need a change in the query functions
|
||||
// because of the different underlying models.
|
||||
|
@ -43,15 +44,21 @@ module.exports = class EventRepository {
|
|||
|
||||
// Create a list of all events that can be queried
|
||||
const pageActions = [
|
||||
{type: 'newsletter_event', action: 'getNewsletterSubscriptionEvents'},
|
||||
{type: 'subscription_event', action: 'getSubscriptionEvents'},
|
||||
{type: 'login_event', action: 'getLoginEvents'},
|
||||
{type: 'payment_event', action: 'getPaymentEvents'},
|
||||
{type: 'signup_event', action: 'getSignupEvents'},
|
||||
{type: 'comment_event', action: 'getCommentEvents'},
|
||||
{type: 'click_event', action: 'getClickEvents'}
|
||||
{type: 'click_event', action: 'getClickEvents'},
|
||||
{type: 'signup_event', action: 'getSignupEvents'},
|
||||
{type: 'subscription_event', action: 'getSubscriptionEvents'}
|
||||
];
|
||||
|
||||
// Some events are not filterable by post_id
|
||||
if (!filters['data.post_id']) {
|
||||
pageActions.push(
|
||||
{type: 'newsletter_event', action: 'getNewsletterSubscriptionEvents'},
|
||||
{type: 'login_event', action: 'getLoginEvents'},
|
||||
{type: 'payment_event', action: 'getPaymentEvents'}
|
||||
);
|
||||
}
|
||||
|
||||
if (this._EmailRecipient) {
|
||||
pageActions.push({type: 'email_delivered_event', action: 'getEmailDeliveredEvents'});
|
||||
pageActions.push({type: 'email_opened_event', action: 'getEmailOpenedEvents'});
|
||||
|
@ -62,8 +69,6 @@ module.exports = class EventRepository {
|
|||
pageActions.push({type: 'feedback_event', action: 'getFeedbackEvents'});
|
||||
}
|
||||
|
||||
let filters = this.getNQLSubset(options.filter);
|
||||
|
||||
//Filter events to query
|
||||
const filteredPages = filters.type ? pageActions.filter(page => nql(filters.type).queryJSON(page)) : pageActions;
|
||||
|
||||
|
@ -72,14 +77,28 @@ module.exports = class EventRepository {
|
|||
|
||||
const allEventPages = await Promise.all(pages);
|
||||
|
||||
const allEvents = allEventPages.reduce((accumulator, page) => accumulator.concat(page.data), []);
|
||||
const allEvents = allEventPages.flatMap(page => page.data);
|
||||
const totalEvents = allEventPages.reduce((accumulator, page) => accumulator + page.meta.pagination.total, 0);
|
||||
|
||||
return allEvents.sort((a, b) => {
|
||||
return new Date(b.data.created_at) - new Date(a.data.created_at);
|
||||
}).reduce((memo, event) => {
|
||||
//disable the event filtering
|
||||
return memo.concat(event);
|
||||
}, []).slice(0, options.limit);
|
||||
return {
|
||||
events: allEvents.sort(
|
||||
(a, b) => {
|
||||
return new Date(b.data.created_at).getTime() - new Date(a.data.created_at).getTime();
|
||||
}
|
||||
).slice(0, options.limit),
|
||||
meta: {
|
||||
pagination: {
|
||||
limit: options.limit,
|
||||
total: totalEvents,
|
||||
pages: options.limit > 0 ? Math.ceil(totalEvents / options.limit) : null,
|
||||
|
||||
// Other values are unavailable (not possible to calculate easily)
|
||||
page: null,
|
||||
next: null,
|
||||
prev: null
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async registerPayment(data) {
|
||||
|
@ -162,6 +181,10 @@ module.exports = class EventRepository {
|
|||
if (filters['data.member_id']) {
|
||||
options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:'));
|
||||
}
|
||||
if (filters['data.post_id']) {
|
||||
options.filter.push(filters['data.post_id'].replace(/data.post_id:/g, 'subscriptionCreatedEvent.attribution_id:'));
|
||||
options.filter.push('subscriptionCreatedEvent.attribution_type:post');
|
||||
}
|
||||
options.filter = options.filter.join('+');
|
||||
|
||||
const {data: models, meta} = await this._MemberPaidSubscriptionEvent.findPage(options);
|
||||
|
@ -288,6 +311,10 @@ module.exports = class EventRepository {
|
|||
if (filters['data.source']) {
|
||||
options.filter.push(filters['data.source'].replace(/data.source:/g, 'source:'));
|
||||
}
|
||||
if (filters['data.post_id']) {
|
||||
options.filter.push(filters['data.post_id'].replace(/data.post_id:/g, 'attribution_id:'));
|
||||
options.filter.push('attribution_type:post');
|
||||
}
|
||||
options.filter = options.filter.join('+');
|
||||
|
||||
const {data: models, meta} = await this._MemberCreatedEvent.findPage(options);
|
||||
|
@ -320,6 +347,9 @@ module.exports = class EventRepository {
|
|||
if (filters['data.member_id']) {
|
||||
options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:'));
|
||||
}
|
||||
if (filters['data.post_id']) {
|
||||
options.filter.push(filters['data.post_id'].replace(/data.post_id:/g, 'post_id:'));
|
||||
}
|
||||
options.filter = options.filter.join('+');
|
||||
|
||||
const {data: models, meta} = await this._Comment.findPage(options);
|
||||
|
@ -349,6 +379,9 @@ module.exports = class EventRepository {
|
|||
if (filters['data.member_id']) {
|
||||
options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:'));
|
||||
}
|
||||
if (filters['data.post_id']) {
|
||||
options.filter.push(filters['data.post_id'].replace(/data.post_id:/g, 'post_id:'));
|
||||
}
|
||||
options.filter = options.filter.join('+');
|
||||
|
||||
const {data: models, meta} = await this._MemberLinkClickEvent.findPage(options);
|
||||
|
@ -378,6 +411,9 @@ module.exports = class EventRepository {
|
|||
if (filters['data.member_id']) {
|
||||
options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:'));
|
||||
}
|
||||
if (filters['data.post_id']) {
|
||||
options.filter.push(filters['data.post_id'].replace(/data.post_id:/g, 'post_id:'));
|
||||
}
|
||||
options.filter = options.filter.join('+');
|
||||
|
||||
const {data: models, meta} = await this._MemberFeedback.findPage(options);
|
||||
|
@ -519,7 +555,7 @@ module.exports = class EventRepository {
|
|||
|
||||
const lex = nql(filter).lex();
|
||||
|
||||
const allowedFilters = ['type','data.created_at','data.member_id'];
|
||||
const allowedFilters = ['type','data.created_at','data.member_id', 'data.post_id'];
|
||||
const properties = lex
|
||||
.filter(x => x.token === 'PROP')
|
||||
.map(x => x.matched.slice(0, -1));
|
||||
|
|
Loading…
Add table
Reference in a new issue