2023-01-20 13:45:48 +03:00
const { MentionSendingService } = require ( '../' ) ;
2023-06-21 11:56:59 +03:00
const assert = require ( 'assert/strict' ) ;
2023-01-19 19:35:10 +03:00
const nock = require ( 'nock' ) ;
// non-standard to use externalRequest here, but this is required for the overrides in the libary, which we want to test for security reasons in combination with the package
const externalRequest = require ( '../../core/core/server/lib/request-external.js' ) ;
const sinon = require ( 'sinon' ) ;
const logging = require ( '@tryghost/logging' ) ;
const { createModel } = require ( './utils/index.js' ) ;
2023-02-09 01:29:12 +03:00
// mock up job service
let jobService = {
async addJob ( name , fn ) {
return fn ( ) ;
}
} ;
2023-01-19 19:35:10 +03:00
describe ( 'MentionSendingService' , function ( ) {
let errorLogStub ;
beforeEach ( function ( ) {
nock . disableNetConnect ( ) ;
sinon . stub ( logging , 'info' ) ;
errorLogStub = sinon . stub ( logging , 'error' ) ;
} ) ;
afterEach ( function ( ) {
nock . cleanAll ( ) ;
sinon . restore ( ) ;
} ) ;
2023-01-20 16:32:50 +03:00
after ( function ( ) {
nock . cleanAll ( ) ;
nock . enableNetConnect ( ) ;
} ) ;
2023-01-19 19:35:10 +03:00
describe ( 'listen' , function ( ) {
2023-03-10 02:31:29 +03:00
it ( 'Called on all events we listen to' , async function ( ) {
2023-01-19 19:35:10 +03:00
const service = new MentionSendingService ( { } ) ;
2023-02-02 01:49:58 +03:00
const stub = sinon . stub ( service , 'sendForPost' ) . resolves ( ) ;
2023-01-19 19:35:10 +03:00
let callback ;
const events = {
on : sinon . stub ( ) . callsFake ( ( event , c ) => {
callback = c ;
} )
} ;
service . listen ( events ) ;
2023-03-10 02:31:29 +03:00
sinon . assert . callCount ( events . on , 6 ) ;
2023-01-19 19:35:10 +03:00
await callback ( { } ) ;
sinon . assert . calledOnce ( stub ) ;
} ) ;
} ) ;
2023-02-02 01:49:58 +03:00
describe ( 'sendForPost' , function ( ) {
2023-01-19 19:35:10 +03:00
it ( 'Ignores if disabled' , async function ( ) {
const service = new MentionSendingService ( {
isEnabled : ( ) => false
} ) ;
const stub = sinon . stub ( service , 'sendAll' ) ;
2023-02-02 01:49:58 +03:00
await service . sendForPost ( { } ) ;
2023-01-19 19:35:10 +03:00
sinon . assert . notCalled ( stub ) ;
} ) ;
2023-02-02 02:40:29 +03:00
it ( 'Ignores if importing data' , async function ( ) {
const service = new MentionSendingService ( {
isEnabled : ( ) => true
} ) ;
const stub = sinon . stub ( service , 'sendAll' ) ;
let options = { importing : true } ;
await service . sendForPost ( { } , options ) ;
sinon . assert . notCalled ( stub ) ;
} ) ;
it ( 'Ignores if internal context' , async function ( ) {
const service = new MentionSendingService ( {
isEnabled : ( ) => true
} ) ;
const stub = sinon . stub ( service , 'sendAll' ) ;
let options = { context : { internal : true } } ;
await service . sendForPost ( { } , options ) ;
sinon . assert . notCalled ( stub ) ;
} ) ;
2023-01-19 19:35:10 +03:00
it ( 'Ignores draft posts' , async function ( ) {
const service = new MentionSendingService ( {
isEnabled : ( ) => true
} ) ;
const stub = sinon . stub ( service , 'sendAll' ) ;
2023-02-02 01:49:58 +03:00
await service . sendForPost ( createModel ( {
2023-01-19 19:35:10 +03:00
status : 'draft' ,
html : 'changed' ,
previous : {
status : 'draft' ,
html : ''
}
} ) ) ;
sinon . assert . notCalled ( stub ) ;
} ) ;
it ( 'Ignores if html was not changed' , async function ( ) {
const service = new MentionSendingService ( {
isEnabled : ( ) => true
} ) ;
const stub = sinon . stub ( service , 'sendAll' ) ;
2023-02-02 01:49:58 +03:00
await service . sendForPost ( createModel ( {
2023-01-19 19:35:10 +03:00
status : 'published' ,
html : 'same' ,
previous : {
status : 'published' ,
html : 'same'
}
} ) ) ;
sinon . assert . notCalled ( stub ) ;
} ) ;
it ( 'Ignores email only posts' , async function ( ) {
const service = new MentionSendingService ( {
isEnabled : ( ) => true
} ) ;
const stub = sinon . stub ( service , 'sendAll' ) ;
2023-02-02 01:49:58 +03:00
await service . sendForPost ( createModel ( {
2023-01-19 19:35:10 +03:00
status : 'send' ,
html : 'changed' ,
previous : {
status : 'draft' ,
html : 'same'
}
} ) ) ;
sinon . assert . notCalled ( stub ) ;
} ) ;
it ( 'Sends on publish' , async function ( ) {
const service = new MentionSendingService ( {
isEnabled : ( ) => true ,
2023-02-09 01:29:12 +03:00
getPostUrl : ( ) => 'https://site.com/post/' ,
jobService : jobService
2023-01-19 19:35:10 +03:00
} ) ;
const stub = sinon . stub ( service , 'sendAll' ) ;
2023-02-02 01:49:58 +03:00
await service . sendForPost ( createModel ( {
2023-01-19 19:35:10 +03:00
status : 'published' ,
html : 'same' ,
previous : {
status : 'draft' ,
html : 'same'
}
} ) ) ;
sinon . assert . calledOnce ( stub ) ;
const firstCall = stub . getCall ( 0 ) . args [ 0 ] ;
2023-06-21 11:56:59 +03:00
assert . equal ( firstCall . url . toString ( ) , 'https://site.com/post/' ) ;
assert . equal ( firstCall . html , 'same' ) ;
assert . equal ( firstCall . previousHtml , null ) ;
2023-01-19 19:35:10 +03:00
} ) ;
it ( 'Sends on html change' , async function ( ) {
const service = new MentionSendingService ( {
isEnabled : ( ) => true ,
2023-02-09 01:29:12 +03:00
getPostUrl : ( ) => 'https://site.com/post/' ,
jobService : jobService
2023-01-19 19:35:10 +03:00
} ) ;
const stub = sinon . stub ( service , 'sendAll' ) ;
2023-02-02 01:49:58 +03:00
await service . sendForPost ( createModel ( {
2023-01-19 19:35:10 +03:00
status : 'published' ,
html : 'updated' ,
previous : {
status : 'published' ,
html : 'same'
}
} ) ) ;
sinon . assert . calledOnce ( stub ) ;
const firstCall = stub . getCall ( 0 ) . args [ 0 ] ;
2023-06-21 11:56:59 +03:00
assert . equal ( firstCall . url . toString ( ) , 'https://site.com/post/' ) ;
assert . equal ( firstCall . html , 'updated' ) ;
assert . equal ( firstCall . previousHtml , 'same' ) ;
2023-01-19 19:35:10 +03:00
} ) ;
it ( 'Catches and logs errors' , async function ( ) {
const service = new MentionSendingService ( {
isEnabled : ( ) => true ,
getPostUrl : ( ) => 'https://site.com/post/'
} ) ;
sinon . stub ( service , 'sendAll' ) . rejects ( new Error ( 'Internal error test' ) ) ;
2023-02-02 01:49:58 +03:00
await service . sendForPost ( createModel ( {
2023-01-19 19:35:10 +03:00
status : 'published' ,
html : 'same' ,
previous : {
status : 'draft' ,
html : 'same'
}
} ) ) ;
assert ( errorLogStub . calledTwice ) ;
} ) ;
2023-02-16 20:07:04 +03:00
it ( 'Sends no mentions for posts without html and previous html' , async function ( ) {
const service = new MentionSendingService ( {
isEnabled : ( ) => true ,
getPostUrl : ( ) => 'https://site.com/post/' ,
jobService : jobService
} ) ;
const stub = sinon . stub ( service , 'sendAll' ) ;
await service . sendForPost ( createModel ( {
status : 'published' ,
html : '' ,
previous : {
status : 'draft' ,
html : ''
}
} ) ) ;
assert ( stub . notCalled ) ;
} ) ;
2023-01-19 19:35:10 +03:00
} ) ;
describe ( 'sendAll' , function ( ) {
it ( 'Sends to all links' , async function ( ) {
2023-04-07 10:37:01 +03:00
this . retries ( 1 ) ;
2023-01-19 19:35:10 +03:00
let counter = 0 ;
const scope = nock ( 'https://example.org' )
. persist ( )
. post ( '/webmentions-test' )
. reply ( ( ) => {
counter += 1 ;
return [ 202 ] ;
} ) ;
const service = new MentionSendingService ( {
externalRequest ,
getSiteUrl : ( ) => new URL ( 'https://site.com' ) ,
discoveryService : {
getEndpoint : async ( ) => new URL ( 'https://example.org/webmentions-test' )
}
} ) ;
await service . sendAll ( { url : new URL ( 'https://site.com' ) ,
html : `
< html >
< body >
< a href = "https://example.com" > Example < / a >
< a href = "https://example.com" > Example repeated < / a >
< a href = "https://example.org#fragment" > Example < / a >
< a href = "http://example2.org" > Example 2 < / a >
< / b o d y >
< / h t m l >
` });
2023-06-21 11:56:59 +03:00
assert . equal ( scope . isDone ( ) , true ) ;
2023-01-19 19:35:10 +03:00
assert . equal ( counter , 3 ) ;
} ) ;
it ( 'Catches and logs errors' , async function ( ) {
2023-04-07 10:37:01 +03:00
this . retries ( 1 ) ;
2023-01-19 19:35:10 +03:00
let counter = 0 ;
const scope = nock ( 'https://example.org' )
. persist ( )
. post ( '/webmentions-test' )
. reply ( ( ) => {
counter += 1 ;
if ( counter === 2 ) {
return [ 500 ] ;
}
return [ 202 ] ;
} ) ;
const service = new MentionSendingService ( {
externalRequest ,
getSiteUrl : ( ) => new URL ( 'https://site.com' ) ,
discoveryService : {
getEndpoint : async ( ) => new URL ( 'https://example.org/webmentions-test' )
}
} ) ;
await service . sendAll ( { url : new URL ( 'https://site.com' ) ,
html : `
< html >
< body >
< a href = "https://example.com" > Example < / a >
< a href = "https://example.com" > Example repeated < / a >
< a href = "https://example.org#fragment" > Example < / a >
< a href = "http://example2.org" > Example 2 < / a >
< / b o d y >
< / h t m l >
` });
2023-06-21 11:56:59 +03:00
assert . equal ( scope . isDone ( ) , true ) ;
2023-01-19 19:35:10 +03:00
assert . equal ( counter , 3 ) ;
assert ( errorLogStub . calledOnce ) ;
} ) ;
it ( 'Sends to deleted links' , async function ( ) {
2023-04-07 10:37:01 +03:00
this . retries ( 1 ) ;
2023-01-19 19:35:10 +03:00
let counter = 0 ;
const scope = nock ( 'https://example.org' )
. persist ( )
. post ( '/webmentions-test' )
. reply ( ( ) => {
counter += 1 ;
return [ 202 ] ;
} ) ;
const service = new MentionSendingService ( {
externalRequest ,
getSiteUrl : ( ) => new URL ( 'https://site.com' ) ,
discoveryService : {
getEndpoint : async ( ) => new URL ( 'https://example.org/webmentions-test' )
2023-02-09 01:29:12 +03:00
} ,
jobService : jobService
2023-01-19 19:35:10 +03:00
} ) ;
await service . sendAll ( { url : new URL ( 'https://site.com' ) ,
html : ` <a href="https://example.com">Example</a> ` ,
previousHtml : ` <a href="https://typo.com">Example</a> ` } ) ;
2023-06-21 11:56:59 +03:00
assert . equal ( scope . isDone ( ) , true ) ;
2023-01-19 19:35:10 +03:00
assert . equal ( counter , 2 ) ;
} ) ;
2023-02-16 20:07:04 +03:00
// cheerio must be served a string
it ( 'Does not evaluate links for an empty post' , async function ( ) {
const service = new MentionSendingService ( {
isEnabled : ( ) => true
} ) ;
const linksStub = sinon . stub ( service , 'getLinks' ) ;
await service . sendAll ( { html : ` ` , previousHtml : ` ` } ) ;
sinon . assert . notCalled ( linksStub ) ;
} ) ;
2023-01-19 19:35:10 +03:00
} ) ;
describe ( 'getLinks' , function ( ) {
it ( 'Returns all unique links in a HTML-document' , async function ( ) {
const service = new MentionSendingService ( {
getSiteUrl : ( ) => new URL ( 'https://site.com' )
} ) ;
const links = service . getLinks ( `
< html >
< body >
< a href = "https://example.com" > Example < / a >
< a href = "https://example.com" > Example repeated < / a >
< a href = "https://example.org#fragment" > Example < / a >
< a href = "http://example2.org" > Example 2 < / a >
< / b o d y >
< / h t m l >
` );
2023-06-21 11:56:59 +03:00
assert . deepEqual ( links , [
2023-01-19 19:35:10 +03:00
new URL ( 'https://example.com' ) ,
new URL ( 'https://example.org#fragment' ) ,
new URL ( 'http://example2.org' )
] ) ;
} ) ;
it ( 'Does not include invalid or local URLs' , async function ( ) {
const service = new MentionSendingService ( {
getSiteUrl : ( ) => new URL ( 'https://site.com' )
} ) ;
const links = service . getLinks ( ` <a href="/">Example</a> ` ) ;
2023-06-21 11:56:59 +03:00
assert . deepEqual ( links , [ ] ) ;
2023-01-19 19:35:10 +03:00
} ) ;
it ( 'Does not include non-http protocols' , async function ( ) {
const service = new MentionSendingService ( {
getSiteUrl : ( ) => new URL ( 'https://site.com' )
} ) ;
const links = service . getLinks ( ` <a href="ftp://invalid.com">Example</a> ` ) ;
2023-06-21 11:56:59 +03:00
assert . deepEqual ( links , [ ] ) ;
2023-01-19 19:35:10 +03:00
} ) ;
it ( 'Does not include invalid urls' , async function ( ) {
const service = new MentionSendingService ( {
getSiteUrl : ( ) => new URL ( 'https://site.com' )
} ) ;
const links = service . getLinks ( ` <a href="()">Example</a> ` ) ;
2023-06-21 11:56:59 +03:00
assert . deepEqual ( links , [ ] ) ;
2023-01-19 19:35:10 +03:00
} ) ;
it ( 'Does not include urls from site domain' , async function ( ) {
const service = new MentionSendingService ( {
getSiteUrl : ( ) => new URL ( 'https://site.com' )
} ) ;
const links = service . getLinks ( ` <a href="http://site.com/test?123">Example</a> ` ) ;
2023-06-21 11:56:59 +03:00
assert . deepEqual ( links , [ ] ) ;
2023-01-19 19:35:10 +03:00
} ) ;
it ( 'Ignores invalid site urls' , async function ( ) {
const service = new MentionSendingService ( {
getSiteUrl : ( ) => new URL ( 'invalid()' )
} ) ;
const links = service . getLinks ( ` <a href="http://site.com/test?123">Example</a> ` ) ;
2023-06-21 11:56:59 +03:00
assert . deepEqual ( links , [
2023-01-19 19:35:10 +03:00
new URL ( 'http://site.com/test?123' )
] ) ;
} ) ;
} ) ;
describe ( 'send' , function ( ) {
it ( 'Can handle 202 accepted responses' , async function ( ) {
2023-04-07 10:37:01 +03:00
this . retries ( 1 ) ;
2023-02-20 18:33:11 +03:00
const source = new URL ( 'https://example.com/source' ) ;
const target = new URL ( 'https://target.com/target' ) ;
const endpoint = new URL ( 'https://example.org/webmentions-test' ) ;
2023-01-19 19:35:10 +03:00
const scope = nock ( 'https://example.org' )
. persist ( )
2023-02-01 09:44:55 +03:00
. post ( '/webmentions-test' , ` source= ${ encodeURIComponent ( 'https://example.com/source' ) } &target= ${ encodeURIComponent ( 'https://target.com/target' ) } &source_is_ghost=true ` )
2023-01-19 19:35:10 +03:00
. reply ( 202 ) ;
const service = new MentionSendingService ( { externalRequest } ) ;
await service . send ( {
2023-02-20 18:33:11 +03:00
source : source ,
target : target ,
endpoint : endpoint
2023-01-19 19:35:10 +03:00
} ) ;
assert ( scope . isDone ( ) ) ;
} ) ;
it ( 'Can handle 201 created responses' , async function ( ) {
2023-04-07 10:37:01 +03:00
this . retries ( 1 ) ;
2023-02-20 18:33:11 +03:00
const source = new URL ( 'https://example.com/source' ) ;
const target = new URL ( 'https://target.com/target' ) ;
const endpoint = new URL ( 'https://example.org/webmentions-test' ) ;
2023-01-19 19:35:10 +03:00
const scope = nock ( 'https://example.org' )
. persist ( )
2023-02-01 09:44:55 +03:00
. post ( '/webmentions-test' , ` source= ${ encodeURIComponent ( 'https://example.com/source' ) } &target= ${ encodeURIComponent ( 'https://target.com/target' ) } &source_is_ghost=true ` )
2023-01-19 19:35:10 +03:00
. reply ( 201 ) ;
const service = new MentionSendingService ( { externalRequest } ) ;
await service . send ( {
2023-02-20 18:33:11 +03:00
source : source ,
target : target ,
endpoint : endpoint
2023-01-19 19:35:10 +03:00
} ) ;
assert ( scope . isDone ( ) ) ;
} ) ;
it ( 'Can handle 400 responses' , async function ( ) {
2023-04-07 10:37:01 +03:00
this . retries ( 1 ) ;
2023-01-19 19:35:10 +03:00
const scope = nock ( 'https://example.org' )
. persist ( )
. post ( '/webmentions-test' )
. reply ( 400 ) ;
const service = new MentionSendingService ( { externalRequest } ) ;
await assert . rejects ( service . send ( {
source : new URL ( 'https://example.com/source' ) ,
target : new URL ( 'https://target.com/target' ) ,
endpoint : new URL ( 'https://example.org/webmentions-test' )
} ) , /sending failed/ ) ;
assert ( scope . isDone ( ) ) ;
} ) ;
it ( 'Can handle 500 responses' , async function ( ) {
2023-04-07 10:37:01 +03:00
this . retries ( 1 ) ;
2023-01-19 19:35:10 +03:00
const scope = nock ( 'https://example.org' )
. persist ( )
. post ( '/webmentions-test' )
. reply ( 500 ) ;
const service = new MentionSendingService ( { externalRequest } ) ;
await assert . rejects ( service . send ( {
source : new URL ( 'https://example.com/source' ) ,
target : new URL ( 'https://target.com/target' ) ,
endpoint : new URL ( 'https://example.org/webmentions-test' )
} ) , /sending failed/ ) ;
assert ( scope . isDone ( ) ) ;
} ) ;
2023-02-20 18:33:11 +03:00
it ( 'Can handle redirect responses' , async function ( ) {
2023-04-07 10:37:01 +03:00
this . retries ( 1 ) ;
2023-02-20 18:33:11 +03:00
const scope = nock ( 'https://example.org' )
. persist ( )
. post ( '/webmentions-test' )
. reply ( 302 , '' , {
Location : 'https://example.org/webmentions-test-2'
} ) ;
const scope2 = nock ( 'https://example.org' )
. persist ( )
. post ( '/webmentions-test-2' )
. reply ( 201 ) ;
2023-02-22 18:19:09 +03:00
2023-02-20 18:33:11 +03:00
const service = new MentionSendingService ( { externalRequest } ) ;
await service . send ( {
source : new URL ( 'https://example.com' ) ,
target : new URL ( 'https://example.com' ) ,
endpoint : new URL ( 'https://example.org/webmentions-test' )
} ) ;
assert ( scope . isDone ( ) ) ;
assert ( scope2 . isDone ( ) ) ;
} ) ;
2023-01-19 19:35:10 +03:00
it ( 'Can handle network errors' , async function ( ) {
2023-04-07 10:37:01 +03:00
this . retries ( 1 ) ;
2023-01-19 19:35:10 +03:00
const scope = nock ( 'https://example.org' )
. persist ( )
. post ( '/webmentions-test' )
. replyWithError ( 'network error' ) ;
const service = new MentionSendingService ( { externalRequest } ) ;
await assert . rejects ( service . send ( {
source : new URL ( 'https://example.com/source' ) ,
target : new URL ( 'https://target.com/target' ) ,
endpoint : new URL ( 'https://example.org/webmentions-test' )
} ) , /network error/ ) ;
assert ( scope . isDone ( ) ) ;
} ) ;
it ( 'Does not send to private IP behind DNS' , async function ( ) {
2023-04-07 10:37:01 +03:00
this . retries ( 1 ) ;
2023-01-19 19:35:10 +03:00
// Test that we don't make a request when a domain resolves to a private IP
// domaincontrol.com -> 127.0.0.1
const service = new MentionSendingService ( { externalRequest } ) ;
await assert . rejects ( service . send ( {
source : new URL ( 'https://example.com/source' ) ,
target : new URL ( 'https://target.com/target' ) ,
endpoint : new URL ( 'http://domaincontrol.com/webmentions' )
} ) , /non-permitted private IP/ ) ;
} ) ;
} ) ;
} ) ;