2022-10-18 16:52:04 +03:00
const { agentProvider , mockManager , fixtureManager , matchers } = require ( '../../utils/e2e-framework' ) ;
2023-01-17 14:56:29 +03:00
const { anyEtag , anyErrorId , anyObjectId , anyContentLength , anyContentVersion , anyUuid , anyISODate , anyString , anyObject , anyNumber } = matchers ;
2022-10-18 16:52:04 +03:00
2023-06-21 11:56:59 +03:00
const assert = require ( 'assert/strict' ) ;
2022-10-27 13:13:24 +03:00
const moment = require ( 'moment' ) ;
2023-03-03 20:58:19 +03:00
const sinon = require ( 'sinon' ) ;
const logging = require ( '@tryghost/logging' ) ;
2022-10-18 16:52:04 +03:00
let agent ;
2022-10-27 18:23:45 +03:00
2022-11-07 17:08:56 +03:00
async function testPagination ( skippedTypes , postId , totalExpected , limit ) {
2022-10-27 18:23:45 +03:00
const postFilter = postId ? ` +data.post_id: ${ postId } ` : '' ;
// To make the test cover more edge cases, we test different limit configurations
2022-11-07 17:08:56 +03:00
const { body : firstPage } = await agent
. get ( ` /members/events?filter= ${ encodeURIComponent ( ` type:-[ ${ skippedTypes . join ( ',' ) } ] ${ postFilter } ` ) } &limit= ${ limit } ` )
. expectStatus ( 200 )
. matchHeaderSnapshot ( {
etag : anyEtag ,
2023-01-17 14:56:29 +03:00
'content-version' : anyContentVersion ,
2022-11-07 17:08:56 +03:00
'content-length' : anyContentLength // Depending on random conditions (ID generation) the order of events can change
} )
. matchBodySnapshot ( {
events : new Array ( limit ) . fill ( {
type : anyString ,
data : anyObject
} )
} )
. expect ( ( { body } ) => {
if ( postId ) {
assert ( ! body . events . find ( e => ( e . data ? . post ? . id ? ? e . data ? . attribution ? . id ? ? e . data ? . email ? . post _id ) !== postId && e . type !== 'aggregated_click_event' ) , 'Should only return events for the post' ) ;
}
// Assert total is correct
assert . equal ( body . meta . pagination . total , totalExpected , 'Expected total of ' + totalExpected + ' at limit ' + limit ) ;
} ) ;
let previousPage = firstPage ;
let page = 1 ;
const allEvents = previousPage . events ;
while ( allEvents . length < totalExpected && page < 50 ) {
page += 1 ;
// Calculate next page
let lastId = previousPage . events [ previousPage . events . length - 1 ] . data . id ;
let lastCreatedAt = moment ( previousPage . events [ previousPage . events . length - 1 ] . data . created _at ) . format ( 'YYYY-MM-DD HH:mm:ss' ) ;
const remaining = totalExpected - ( page - 1 ) * limit ;
const { body : secondPage } = await agent
. get ( ` /members/events?filter= ${ encodeURIComponent ( ` type:-[ ${ skippedTypes . join ( ',' ) } ] ${ postFilter } +(data.created_at:<' ${ lastCreatedAt } ',(data.created_at:' ${ lastCreatedAt } '+id:< ${ lastId } )) ` ) } &limit= ${ limit } ` )
2022-10-27 18:23:45 +03:00
. expectStatus ( 200 )
. matchHeaderSnapshot ( {
2022-10-27 19:11:33 +03:00
etag : anyEtag ,
2023-01-17 14:56:29 +03:00
'content-version' : anyContentVersion ,
2022-10-27 19:11:33 +03:00
'content-length' : anyContentLength // Depending on random conditions (ID generation) the order of events can change
2022-10-27 18:23:45 +03:00
} )
. matchBodySnapshot ( {
2022-11-07 17:08:56 +03:00
events : new Array ( Math . min ( remaining , limit ) ) . fill ( {
2022-10-27 18:23:45 +03:00
type : anyString ,
data : anyObject
} )
} )
. expect ( ( { body } ) => {
if ( postId ) {
assert ( ! body . events . find ( e => ( e . data ? . post ? . id ? ? e . data ? . attribution ? . id ? ? e . data ? . email ? . post _id ) !== postId && e . type !== 'aggregated_click_event' ) , 'Should only return events for the post' ) ;
}
// Assert total is correct
2022-11-07 17:08:56 +03:00
assert . equal ( body . meta . pagination . total , remaining , 'Expected total to be correct for page ' + page + ' with limit ' + limit ) ;
2022-10-27 18:23:45 +03:00
} ) ;
2022-11-07 17:08:56 +03:00
allEvents . push ( ... secondPage . events ) ;
}
2022-10-27 18:23:45 +03:00
2022-11-07 17:08:56 +03:00
// Check if the ordering is correct and we didn't receive duplicate events
assert . equal ( allEvents . length , totalExpected , 'Total actually received should match the total' ) ;
for ( const event of allEvents ) {
// Check no other events have the same id
assert . equal ( allEvents . filter ( e => e . data . id === event . data . id ) . length , 1 ) ;
2022-10-27 18:23:45 +03:00
}
}
2022-10-18 16:52:04 +03:00
describe ( 'Activity Feed API' , function ( ) {
before ( async function ( ) {
agent = await agentProvider . getAdminAPIAgent ( ) ;
2022-10-20 14:29:00 +03:00
await fixtureManager . init ( 'posts' , 'newsletters' , 'members:newsletters' , 'comments' , 'redirects' , 'clicks' , 'feedback' , 'members:emails' ) ;
2022-10-18 16:52:04 +03:00
await agent . loginAsOwner ( ) ;
} ) ;
beforeEach ( function ( ) {
mockManager . mockMail ( ) ;
} ) ;
afterEach ( function ( ) {
mockManager . restore ( ) ;
2023-03-03 20:58:19 +03:00
sinon . restore ( ) ;
2022-10-18 16:52:04 +03:00
} ) ;
2022-10-30 17:52:30 +03:00
describe ( 'Filter splitting' , function ( ) {
2022-10-27 13:13:24 +03:00
it ( 'Can use NQL OR for type only' , async function ( ) {
// Check activity feed
await agent
. get ( ` /members/events?filter=type:comment_event,type:click_event ` )
. expectStatus ( 200 )
. matchHeaderSnapshot ( {
2023-01-17 14:56:29 +03:00
etag : anyEtag ,
'content-version' : anyContentVersion
2022-10-27 13:13:24 +03:00
} )
. matchBodySnapshot ( {
events : new Array ( 10 ) . fill ( {
type : anyString ,
data : anyObject
} )
} )
. expect ( ( { body } ) => {
assert ( ! body . events . find ( e => e . type !== 'click_event' && e . type !== 'comment_event' ) , 'Expected only click and comment events' ) ;
} ) ;
} ) ;
it ( 'Cannot combine type filter with OR filter' , async function ( ) {
// This query is not allowed because we need to split the filter in two AND filters
2023-03-03 20:58:19 +03:00
const loggingStub = sinon . stub ( logging , 'error' ) ;
2022-10-27 13:13:24 +03:00
await agent
. get ( ` /members/events?filter=type:comment_event,data.post_id:123 ` )
. expectStatus ( 400 )
. matchHeaderSnapshot ( {
2023-01-17 14:56:29 +03:00
etag : anyEtag ,
'content-version' : anyContentVersion
2022-10-27 13:13:24 +03:00
} )
. matchBodySnapshot ( {
errors : [
{
id : anyErrorId
}
]
} ) ;
2023-03-03 20:58:19 +03:00
sinon . assert . calledOnce ( loggingStub ) ;
2022-10-27 13:13:24 +03:00
} ) ;
it ( 'Can only combine type and other filters at the root level' , async function ( ) {
2023-03-03 20:58:19 +03:00
const loggingStub = sinon . stub ( logging , 'error' ) ;
2022-10-27 13:13:24 +03:00
await agent
. get ( ` /members/events?filter= ${ encodeURIComponent ( '(type:comment_event+data.post_id:123)+data.post_id:123' ) } ` )
. expectStatus ( 400 )
. matchHeaderSnapshot ( {
2023-01-17 14:56:29 +03:00
etag : anyEtag ,
'content-version' : anyContentVersion
2022-10-27 13:13:24 +03:00
} )
. matchBodySnapshot ( {
errors : [
{
id : anyErrorId
}
]
} ) ;
2023-03-03 20:58:19 +03:00
sinon . assert . calledOnce ( loggingStub ) ;
2022-10-27 13:13:24 +03:00
} ) ;
it ( 'Can use OR as long as it is not combined with type' , async function ( ) {
const postId = fixtureManager . get ( 'posts' , 0 ) . id ;
const memberId = fixtureManager . get ( 'members' , 0 ) . id ;
await agent
. get ( ` /members/events?filter= ${ encodeURIComponent ( ` data.post_id: ${ postId } ,data.member_id: ${ memberId } ` ) } ` )
. expectStatus ( 200 )
. matchBodySnapshot ( {
events : new Array ( 10 ) . fill ( {
type : anyString ,
data : anyObject
} )
} )
. expect ( ( { body } ) => {
assert ( ! body . events . find ( e => ( e . data ? . post ? . id ? ? e . data ? . attribution ? . id ? ? e . data ? . email ? . post _id ) !== postId && e . data ? . member ? . id !== memberId ) , 'Expected only events either from the given post or member' ) ;
} ) ;
} ) ;
it ( 'Can AND two ORs' , async function ( ) {
const postId = fixtureManager . get ( 'posts' , 0 ) . id ;
const memberId = fixtureManager . get ( 'members' , 0 ) . id ;
await agent
. get ( ` /members/events?filter= ${ encodeURIComponent ( ` (type:comment_event,type:click_event)+(data.post_id: ${ postId } ,data.member_id: ${ memberId } ) ` ) } ` )
. expectStatus ( 200 )
. matchBodySnapshot ( {
events : new Array ( 3 ) . fill ( {
type : anyString ,
data : anyObject
} )
} )
. expect ( ( { body } ) => {
assert ( ! body . events . find ( e => e . type !== 'click_event' && e . type !== 'comment_event' ) , 'Expected only click and comment events' ) ;
assert ( ! body . events . find ( e => ( e . data ? . post ? . id ? ? e . data ? . attribution ? . id ? ? e . data ? . email ? . post _id ) !== postId && e . data ? . member ? . id !== memberId ) , 'Expected only events either from the given post or member' ) ;
} ) ;
} ) ;
} ) ;
2022-11-07 17:08:56 +03:00
describe ( 'Filter-based pagination' , function ( ) {
2022-10-30 17:52:30 +03:00
it ( 'Can do filter based pagination for all posts' , async function ( ) {
// There is an annoying restriction in the pagination. It doesn't work for mutliple email events at the same time because they have the same id (causes issues as we use id to deduplicate the created_at timestamp)
// If that is ever fixed (it is difficult) we can update this test to not use a filter
// Same for click_event and aggregated_click_event (use same id)
const skippedTypes = [ 'email_opened_event' , 'email_failed_event' , 'email_delivered_event' , 'aggregated_click_event' ] ;
2022-11-28 16:43:35 +03:00
await testPagination ( skippedTypes , null , 36 , 36 ) ;
2022-10-30 17:52:30 +03:00
} ) ;
it ( 'Can do filter based pagination for one post' , async function ( ) {
const postId = fixtureManager . get ( 'posts' , 0 ) . id ;
// There is an annoying restriction in the pagination. It doesn't work for mutliple email events at the same time because they have the same id (causes issues as we use id to deduplicate the created_at timestamp)
// If that is ever fixed (it is difficult) we can update this test to not use a filter
// Same for click_event and aggregated_click_event (use same id)
const skippedTypes = [ 'email_opened_event' , 'email_failed_event' , 'email_delivered_event' , 'aggregated_click_event' ] ;
2022-11-28 16:43:35 +03:00
await testPagination ( skippedTypes , postId , 12 , 10 ) ;
2022-10-30 17:52:30 +03:00
} ) ;
it ( 'Can do filter based pagination for aggregated clicks for one post' , async function ( ) {
// Same as previous but with aggregated clicks instead of normal click events + email_delivered_events instead of sent events
const postId = fixtureManager . get ( 'posts' , 0 ) . id ;
const skippedTypes = [ 'email_opened_event' , 'email_failed_event' , 'email_sent_event' , 'click_event' ] ;
2022-11-07 17:08:56 +03:00
await testPagination ( skippedTypes , postId , 9 , 8 ) ;
2022-10-30 17:52:30 +03:00
} ) ;
it ( 'Can do filter based pagination for aggregated clicks for all posts' , async function ( ) {
// Same as previous but with aggregated clicks instead of normal click events + email_delivered_events instead of sent events
const skippedTypes = [ 'email_opened_event' , 'email_failed_event' , 'email_sent_event' , 'click_event' ] ;
2022-11-07 17:08:56 +03:00
await testPagination ( skippedTypes , null , 33 , 32 ) ;
2022-10-30 17:52:30 +03:00
} ) ;
} ) ;
2022-10-18 16:52:04 +03:00
// 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 ( {
2023-01-17 14:56:29 +03:00
etag : anyEtag ,
'content-version' : anyContentVersion
2022-10-18 16:52:04 +03:00
} )
. 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 ( {
2023-01-17 14:56:29 +03:00
etag : anyEtag ,
'content-version' : anyContentVersion
2022-10-18 16:52:04 +03:00
} )
. matchBodySnapshot ( {
events : new Array ( 8 ) . fill ( {
type : anyString ,
data : {
2022-10-27 13:13:24 +03:00
id : anyObjectId ,
2022-10-18 16:52:04 +03:00
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 ( {
2023-01-17 14:56:29 +03:00
etag : anyEtag ,
'content-version' : anyContentVersion
2022-10-18 16:52:04 +03:00
} )
. 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 ( {
2023-01-17 14:56:29 +03:00
etag : anyEtag ,
'content-version' : anyContentVersion
2022-10-18 16:52:04 +03:00
} )
. 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' ) ;
} ) ;
} ) ;
2022-10-24 12:11:44 +03:00
it ( 'Returns email sent events in activity feed' , async function ( ) {
// Check activity feed
await agent
. get ( ` /members/events?filter=type:email_sent_event ` )
. expectStatus ( 200 )
. matchHeaderSnapshot ( {
2023-01-17 14:56:29 +03:00
etag : anyEtag ,
'content-version' : anyContentVersion
2022-10-24 12:11:44 +03:00
} )
. matchBodySnapshot ( {
2022-11-28 16:43:35 +03:00
events : new Array ( 4 ) . fill ( {
2022-10-24 12:11:44 +03:00
type : anyString ,
data : anyObject
} )
} )
. expect ( ( { body } ) => {
assert ( body . events . find ( e => e . type === 'email_sent_event' ) , 'Expected an email sent event' ) ;
assert ( ! body . events . find ( e => e . type !== 'email_sent_event' ) , 'Expected only email sent events' ) ;
} ) ;
} ) ;
2022-10-20 14:29:00 +03:00
it ( 'Returns email delivered events in activity feed' , async function ( ) {
// Check activity feed
await agent
. get ( ` /members/events?filter=type:email_delivered_event ` )
. expectStatus ( 200 )
. matchHeaderSnapshot ( {
2023-01-17 14:56:29 +03:00
etag : anyEtag ,
'content-version' : anyContentVersion
2022-10-20 14:29:00 +03:00
} )
. matchBodySnapshot ( {
events : new Array ( 1 ) . fill ( {
type : anyString ,
data : anyObject
} )
} )
. expect ( ( { body } ) => {
assert ( body . events . find ( e => e . type === 'email_delivered_event' ) , 'Expected an email delivered event' ) ;
assert ( ! body . events . find ( e => e . type !== 'email_delivered_event' ) , 'Expected only email delivered events' ) ;
} ) ;
} ) ;
it ( 'Returns email opened events in activity feed' , async function ( ) {
// Check activity feed
await agent
. get ( ` /members/events?filter=type:email_opened_event ` )
. expectStatus ( 200 )
. matchHeaderSnapshot ( {
2023-01-17 14:56:29 +03:00
etag : anyEtag ,
'content-version' : anyContentVersion
2022-10-20 14:29:00 +03:00
} )
. matchBodySnapshot ( {
events : new Array ( 1 ) . fill ( {
type : anyString ,
data : anyObject
} )
} )
. expect ( ( { body } ) => {
assert ( body . events . find ( e => e . type === 'email_opened_event' ) , 'Expected an email opened event' ) ;
assert ( ! body . events . find ( e => e . type !== 'email_opened_event' ) , 'Expected only email opened events' ) ;
} ) ;
} ) ;
2022-10-18 16:52:04 +03:00
it ( 'Can filter events by post id' , async function ( ) {
const postId = fixtureManager . get ( 'posts' , 0 ) . id ;
2022-10-20 14:29:00 +03:00
2022-10-18 16:52:04 +03:00
await agent
2022-10-24 12:11:44 +03:00
. get ( ` /members/events?filter=data.post_id: ${ postId } &limit=20 ` )
2022-10-18 16:52:04 +03:00
. expectStatus ( 200 )
. matchHeaderSnapshot ( {
2023-01-17 14:56:29 +03:00
etag : anyEtag ,
'content-version' : anyContentVersion
2022-10-18 16:52:04 +03:00
} )
. matchBodySnapshot ( {
2022-11-28 16:43:35 +03:00
events : new Array ( 15 ) . fill ( {
2022-10-18 16:52:04 +03:00
type : anyString ,
data : anyObject
} )
} )
. expect ( ( { body } ) => {
2022-10-27 18:23:45 +03:00
assert ( ! body . events . find ( e => ( e . data ? . post ? . id ? ? e . data ? . attribution ? . id ? ? e . data ? . email ? . post _id ) !== postId && e . type !== 'aggregated_click_event' ) , 'Should only return events for the post' ) ;
2022-10-18 16:52:04 +03:00
// 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' ) ;
2022-10-27 18:23:45 +03:00
assert ( body . events . find ( e => e . type === 'aggregated_click_event' ) , 'Expected an aggregated click event' ) ;
2022-10-18 16:52:04 +03:00
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' ) ;
2022-10-20 14:29:00 +03:00
assert ( body . events . find ( e => e . type === 'email_delivered_event' ) , 'Expected an email delivered event' ) ;
2022-10-24 12:11:44 +03:00
assert ( body . events . find ( e => e . type === 'email_sent_event' ) , 'Expected an email sent event' ) ;
2022-10-20 14:29:00 +03:00
assert ( body . events . find ( e => e . type === 'email_opened_event' ) , 'Expected an email opened event' ) ;
2022-10-18 16:52:04 +03:00
// Assert total is correct
2022-11-28 16:43:35 +03:00
assert . equal ( body . meta . pagination . total , 15 ) ;
2022-10-18 16:52:04 +03:00
} ) ;
} ) ;
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 ( {
2022-10-27 19:11:33 +03:00
etag : anyEtag ,
2023-01-17 14:56:29 +03:00
'content-version' : anyContentVersion ,
2022-10-27 19:11:33 +03:00
'content-length' : anyContentLength // Depending on random conditions (ID generation) the order of events can change
2022-10-18 16:52:04 +03:00
} )
. matchBodySnapshot ( {
events : new Array ( 2 ) . fill ( {
type : anyString ,
data : anyObject
} )
} )
. expect ( ( { body } ) => {
2022-10-20 14:29:00 +03:00
assert ( ! body . events . find ( e => ( e . data ? . post ? . id ? ? e . data ? . attribution ? . id ? ? e . data ? . email ? . post _id ) !== postId ) , 'Should only return events for the post' ) ;
2022-10-18 16:52:04 +03:00
// Assert total is correct
2022-11-28 16:43:35 +03:00
assert . equal ( body . meta . pagination . total , 15 ) ;
2022-10-18 16:52:04 +03:00
} ) ;
} ) ;
} ) ;