Ghost/ghost/link-tracking/test/LinkClickTrackingService.test.js
2024-07-10 02:04:45 +05:30

350 lines
12 KiB
JavaScript

const LinkClickTrackingService = require('../lib/LinkClickTrackingService');
const sinon = require('sinon');
const should = require('should');
const assert = require('assert/strict');
const ObjectID = require('bson-objectid').default;
const PostLink = require('../lib/PostLink');
const {RedirectEvent} = require('@tryghost/link-redirects');
const errors = require('@tryghost/errors');
describe('LinkClickTrackingService', function () {
it('exists', function () {
require('../');
});
describe('init', function () {
it('initialises only once', function () {
const subscribe = sinon.stub();
const service = new LinkClickTrackingService({
DomainEvents: {
subscribe
}
});
service.init();
assert.ok(subscribe.calledOnce);
service.init();
assert.ok(subscribe.calledOnce);
});
});
describe('getLinks', function () {
it('passes call to postLinkRepository', async function () {
const getAll = sinon.stub().resolves(['test']);
const service = new LinkClickTrackingService({
postLinkRepository: {
getAll
}
});
const links = await service.getLinks({filter: 'post_id:1'});
// Check called with filter
assert.ok(getAll.calledOnceWithExactly({filter: 'post_id:1'}));
// Check returned value
assert.deepEqual(links, ['test']);
});
});
describe('addRedirectToUrl', function () {
it('Creates a redirect', async function () {
const getSlugUrl = sinon.stub().resolves(new URL('https://example.com/r/uniqueslug'));
const save = sinon.stub().resolves();
const linkId = new ObjectID();
const addRedirect = sinon.stub().resolves({link_id: linkId, to: new URL('https://example.com/destination'), from: new URL('https://example.com/r/uniqueslug')});
const service = new LinkClickTrackingService({
linkRedirectService: {
getSlugUrl,
addRedirect
},
postLinkRepository: {
save
}
});
const postId = new ObjectID().toHexString();
const updatedUrl = await service.addRedirectToUrl(new URL('https://example.com/destination'), {id: postId});
assert.equal(updatedUrl.toString(), 'https://example.com/r/uniqueslug');
// Check getSlugUrl called
assert(getSlugUrl.calledOnce);
// Check save called
assert(
save.calledOnceWithExactly(
new PostLink({
post_id: postId,
link_id: linkId
})
)
);
});
});
describe('addTrackingToUrl', function () {
it('Creates a redirect', async function () {
const getSlugUrl = sinon.stub().resolves(new URL('https://example.com/r/uniqueslug'));
const save = sinon.stub().resolves();
const linkId = new ObjectID();
const addRedirect = sinon.stub().resolves({link_id: linkId, to: new URL('https://example.com/destination'), from: new URL('https://example.com/r/uniqueslug')});
const service = new LinkClickTrackingService({
linkRedirectService: {
getSlugUrl,
addRedirect
},
postLinkRepository: {
save
}
});
const postId = new ObjectID().toHexString();
const updatedUrl = await service.addTrackingToUrl(new URL('https://example.com/destination'), {id: postId}, '123');
assert.equal(updatedUrl.toString(), 'https://example.com/r/uniqueslug?m=123');
// Check getSlugUrl called
assert(getSlugUrl.calledOnce);
// Check save called
assert(
save.calledOnceWithExactly(
new PostLink({
post_id: postId,
link_id: linkId
})
)
);
});
});
describe('subscribe', function () {
it('Ignores redirects without a member id', async function () {
const event = RedirectEvent.create({
url: new URL('https://example.com/destination'),
link: {}
});
const save = sinon.stub().resolves();
const service = new LinkClickTrackingService({
DomainEvents: {
subscribe: (eventType, callback) => {
assert.equal(eventType, RedirectEvent);
callback(event);
}
},
linkClickRepository: {
save
}
});
service.subscribe();
assert(!save.called);
});
it('Tracks redirects with a member id', async function () {
const linkId = new ObjectID();
const event = RedirectEvent.create({
url: new URL('https://example.com/destination?m=memberId'),
link: {
link_id: linkId
}
});
const save = sinon.stub().resolves();
const service = new LinkClickTrackingService({
DomainEvents: {
subscribe: (eventType, callback) => {
assert.equal(eventType, RedirectEvent);
callback(event);
}
},
linkClickRepository: {
save
}
});
service.subscribe();
assert(save.calledOnce);
assert.equal(save.firstCall.args[0].member_uuid, 'memberId');
assert.equal(save.firstCall.args[0].link_id, linkId);
});
});
describe('bulkEdit', function () {
it('returns the result of updating links', async function () {
const service = new LinkClickTrackingService({
urlUtils: {
absoluteToTransformReady: (d) => {
return d;
},
isSiteUrl: sinon.stub().returns(true)
},
postLinkRepository: {
updateLinks: sinon.stub().resolves({
successful: 0,
unsuccessful: 0,
errors: [],
unsuccessfulData: []
})
},
linkRedirectService: {
getFilteredIds: sinon.stub().resolves([])
}
});
const options = {
filter: `post_id:1+to:'https://test.com'`
};
const result = await service.bulkEdit({
action: 'updateLink',
meta: {
link: {to: 'https://example.com'}
}
}, options);
should(result).eql({
successful: 0,
unsuccessful: 0,
errors: [],
unsuccessfulData: []
});
});
//test for #parseLinkFilter method
it('correctly decodes and parses the filter', async function () {
const urlUtilsStub = {
absoluteToTransformReady: sinon.stub().returnsArg(0),
isSiteUrl: sinon.stub().returns(true)
};
const postLinkRepositoryStub = {
updateLinks: sinon.stub().resolves({
successful: 0,
unsuccessful: 0,
errors: [],
unsuccessfulData: []
})
};
const linkRedirectServiceStub = {
getFilteredIds: sinon.stub().resolves([])
};
const service = new LinkClickTrackingService({
urlUtils: urlUtilsStub,
postLinkRepository: postLinkRepositoryStub,
linkRedirectService: linkRedirectServiceStub
});
const options = {
filter: 'post_id:1+to:\'https://example.com/path\''
};
const data = {
action: 'updateLink',
meta: {
link: {to: 'https://example.com/new-path'}
}
};
const result = await service.bulkEdit(data, options);
should(postLinkRepositoryStub.updateLinks.calledOnce).be.true();
should(result).eql({
successful: 0,
unsuccessful: 0,
errors: [],
unsuccessfulData: []
});
const [filterOptions] = linkRedirectServiceStub.getFilteredIds.firstCall.args;
should(filterOptions.filter).equal('post_id:\'1\'+to:\'https://example.com/path\'');
});
//test for #parseLinkFilter method
it('correctly decodes and parses the filter for encoded urls', async function () {
const urlUtilsStub = {
absoluteToTransformReady: sinon.stub().returnsArg(0),
isSiteUrl: sinon.stub().returns(true)
};
const postLinkRepositoryStub = {
updateLinks: sinon.stub().resolves({
successful: 0,
unsuccessful: 0,
errors: [],
unsuccessfulData: []
})
};
const linkRedirectServiceStub = {
getFilteredIds: sinon.stub().resolves([])
};
const service = new LinkClickTrackingService({
urlUtils: urlUtilsStub,
postLinkRepository: postLinkRepositoryStub,
linkRedirectService: linkRedirectServiceStub
});
const options = {
filter: 'post_id:1+to:\'https://example.com/path%2Ftestpath\''
};
const data = {
action: 'updateLink',
meta: {
link: {to: 'https://example.com/new-path'}
}
};
const result = await service.bulkEdit(data, options);
should(postLinkRepositoryStub.updateLinks.calledOnce).be.true();
should(result).eql({
successful: 0,
unsuccessful: 0,
errors: [],
unsuccessfulData: []
});
const [filterOptions] = linkRedirectServiceStub.getFilteredIds.firstCall.args;
should(filterOptions.filter).equal('post_id:\'1\'+to:\'https://example.com/path%2Ftestpath\'');
});
//test for #parseLinkFilter method
it('throws BadRequestError for invalid filter', async function () {
const urlUtilsStub = {
absoluteToTransformReady: sinon.stub().returnsArg(0),
isSiteUrl: sinon.stub().returns(true)
};
const postLinkRepositoryStub = {
updateLinks: sinon.stub().resolves({
successful: 0,
unsuccessful: 0,
errors: [],
unsuccessfulData: []
})
};
const linkRedirectServiceStub = {
getFilteredIds: sinon.stub().resolves([])
};
const service = new LinkClickTrackingService({
urlUtils: urlUtilsStub,
postLinkRepository: postLinkRepositoryStub,
linkRedirectService: linkRedirectServiceStub
});
const options = {
filter: 'invalid_filter'
};
const data = {
action: 'updateLink',
meta: {
link: {to: 'https://example.com/new-path'}
}
};
await should(service.bulkEdit(data, options)).be.rejectedWith(errors.BadRequestError);
});
});
});