mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-22 18:31:57 +03:00
6161f94910
refs: https://github.com/TryGhost/Toolbox/issues/595 We're rolling out new rules around the node assert library, the first of which is enforcing the use of assert/strict. This means we don't need to use the strict version of methods, as the standard version will work that way by default. This caught some gotchas in our existing usage of assert where the lack of strict mode had unexpected results: - Url matching needs to be done on `url.href` seeaa58b354a4
- Null and undefined are not the same thing, there were a few cases of this being confused - Particularly questionable changes in [PostExporter tests](c1a468744b
) tracked [here](https://github.com/TryGhost/Team/issues/3505). - A typo seeeaac9c293a
Moving forward, using assert strict should help us to catch unexpected behaviour, particularly around nulls and undefineds during implementation.
465 lines
20 KiB
JavaScript
465 lines
20 KiB
JavaScript
const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework');
|
|
const {anyEtag, anyErrorId, anyObjectId, anyContentLength, anyContentVersion, anyUuid, anyISODate, anyString, anyObject, anyNumber} = matchers;
|
|
|
|
const assert = require('assert/strict');
|
|
const moment = require('moment');
|
|
const sinon = require('sinon');
|
|
const logging = require('@tryghost/logging');
|
|
|
|
let agent;
|
|
|
|
async function testPagination(skippedTypes, postId, totalExpected, limit) {
|
|
const postFilter = postId ? `+data.post_id:${postId}` : '';
|
|
|
|
// To make the test cover more edge cases, we test different limit configurations
|
|
const {body: firstPage} = await agent
|
|
.get(`/members/events?filter=${encodeURIComponent(`type:-[${skippedTypes.join(',')}]${postFilter}`)}&limit=${limit}`)
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
'content-version': anyContentVersion,
|
|
'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}`)
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
'content-version': anyContentVersion,
|
|
'content-length': anyContentLength // Depending on random conditions (ID generation) the order of events can change
|
|
})
|
|
.matchBodySnapshot({
|
|
events: new Array(Math.min(remaining, 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, remaining, 'Expected total to be correct for page ' + page + ' with limit ' + limit);
|
|
});
|
|
allEvents.push(...secondPage.events);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
describe('Activity Feed API', function () {
|
|
before(async function () {
|
|
agent = await agentProvider.getAdminAPIAgent();
|
|
await fixtureManager.init('posts', 'newsletters', 'members:newsletters', 'comments', 'redirects', 'clicks', 'feedback', 'members:emails');
|
|
await agent.loginAsOwner();
|
|
});
|
|
|
|
beforeEach(function () {
|
|
mockManager.mockMail();
|
|
});
|
|
|
|
afterEach(function () {
|
|
mockManager.restore();
|
|
sinon.restore();
|
|
});
|
|
|
|
describe('Filter splitting', function () {
|
|
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({
|
|
etag: anyEtag,
|
|
'content-version': anyContentVersion
|
|
})
|
|
.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
|
|
const loggingStub = sinon.stub(logging, 'error');
|
|
await agent
|
|
.get(`/members/events?filter=type:comment_event,data.post_id:123`)
|
|
.expectStatus(400)
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
'content-version': anyContentVersion
|
|
})
|
|
.matchBodySnapshot({
|
|
errors: [
|
|
{
|
|
id: anyErrorId
|
|
}
|
|
]
|
|
});
|
|
sinon.assert.calledOnce(loggingStub);
|
|
});
|
|
|
|
it('Can only combine type and other filters at the root level', async function () {
|
|
const loggingStub = sinon.stub(logging, 'error');
|
|
await agent
|
|
.get(`/members/events?filter=${encodeURIComponent('(type:comment_event+data.post_id:123)+data.post_id:123')}`)
|
|
.expectStatus(400)
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
'content-version': anyContentVersion
|
|
})
|
|
.matchBodySnapshot({
|
|
errors: [
|
|
{
|
|
id: anyErrorId
|
|
}
|
|
]
|
|
});
|
|
sinon.assert.calledOnce(loggingStub);
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Filter-based pagination', function () {
|
|
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'];
|
|
await testPagination(skippedTypes, null, 36, 36);
|
|
});
|
|
|
|
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'];
|
|
|
|
await testPagination(skippedTypes, postId, 12, 10);
|
|
});
|
|
|
|
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'];
|
|
|
|
await testPagination(skippedTypes, postId, 9, 8);
|
|
});
|
|
|
|
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'];
|
|
await testPagination(skippedTypes, null, 33, 32);
|
|
});
|
|
});
|
|
|
|
// 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,
|
|
'content-version': anyContentVersion
|
|
})
|
|
.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,
|
|
'content-version': anyContentVersion
|
|
})
|
|
.matchBodySnapshot({
|
|
events: new Array(8).fill({
|
|
type: anyString,
|
|
data: {
|
|
id: anyObjectId,
|
|
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,
|
|
'content-version': anyContentVersion
|
|
})
|
|
.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,
|
|
'content-version': anyContentVersion
|
|
})
|
|
.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('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({
|
|
etag: anyEtag,
|
|
'content-version': anyContentVersion
|
|
})
|
|
.matchBodySnapshot({
|
|
events: new Array(4).fill({
|
|
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');
|
|
});
|
|
});
|
|
|
|
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({
|
|
etag: anyEtag,
|
|
'content-version': anyContentVersion
|
|
})
|
|
.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({
|
|
etag: anyEtag,
|
|
'content-version': anyContentVersion
|
|
})
|
|
.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');
|
|
});
|
|
});
|
|
|
|
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}&limit=20`)
|
|
.expectStatus(200)
|
|
.matchHeaderSnapshot({
|
|
etag: anyEtag,
|
|
'content-version': anyContentVersion
|
|
})
|
|
.matchBodySnapshot({
|
|
events: new Array(15).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.type !== 'aggregated_click_event'), '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 === 'aggregated_click_event'), 'Expected an aggregated click 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(body.events.find(e => e.type === 'email_delivered_event'), 'Expected an email delivered event');
|
|
assert(body.events.find(e => e.type === 'email_sent_event'), 'Expected an email sent event');
|
|
assert(body.events.find(e => e.type === 'email_opened_event'), 'Expected an email opened event');
|
|
|
|
// Assert total is correct
|
|
assert.equal(body.meta.pagination.total, 15);
|
|
});
|
|
});
|
|
|
|
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,
|
|
'content-version': anyContentVersion,
|
|
'content-length': anyContentLength // Depending on random conditions (ID generation) the order of events can change
|
|
})
|
|
.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 ?? e.data?.email?.post_id) !== postId), 'Should only return events for the post');
|
|
|
|
// Assert total is correct
|
|
assert.equal(body.meta.pagination.total, 15);
|
|
});
|
|
});
|
|
});
|