Gab Social. All are welcome.
This commit is contained in:
160
spec/services/account_search_service_spec.rb
Normal file
160
spec/services/account_search_service_spec.rb
Normal file
@@ -0,0 +1,160 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe AccountSearchService, type: :service do
|
||||
describe '.call' do
|
||||
describe 'with a query to ignore' do
|
||||
it 'returns empty array for missing query' do
|
||||
results = subject.call('', nil, limit: 10)
|
||||
|
||||
expect(results).to eq []
|
||||
end
|
||||
it 'returns empty array for hashtag query' do
|
||||
results = subject.call('#tag', nil, limit: 10)
|
||||
|
||||
expect(results).to eq []
|
||||
end
|
||||
it 'returns empty array for limit zero' do
|
||||
Fabricate(:account, username: 'match')
|
||||
results = subject.call('match', nil, limit: 0)
|
||||
|
||||
expect(results).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
describe 'searching for a simple term that is not an exact match' do
|
||||
it 'does not return a nil entry in the array for the exact match' do
|
||||
match = Fabricate(:account, username: 'matchingusername')
|
||||
|
||||
results = subject.call('match', nil, limit: 5)
|
||||
expect(results).to eq [match]
|
||||
end
|
||||
end
|
||||
|
||||
describe 'searching local and remote users' do
|
||||
describe "when only '@'" do
|
||||
before do
|
||||
allow(Account).to receive(:find_local)
|
||||
allow(Account).to receive(:search_for)
|
||||
subject.call('@', nil, limit: 10)
|
||||
end
|
||||
|
||||
it 'uses find_local with empty query to look for local accounts' do
|
||||
expect(Account).to have_received(:find_local).with('')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when no domain' do
|
||||
before do
|
||||
allow(Account).to receive(:find_local)
|
||||
allow(Account).to receive(:search_for)
|
||||
subject.call('one', nil, limit: 10)
|
||||
end
|
||||
|
||||
it 'uses find_local to look for local accounts' do
|
||||
expect(Account).to have_received(:find_local).with('one')
|
||||
end
|
||||
|
||||
it 'uses search_for to find matches' do
|
||||
expect(Account).to have_received(:search_for).with('one', 10, 0)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when there is a domain' do
|
||||
before do
|
||||
allow(Account).to receive(:find_remote)
|
||||
end
|
||||
|
||||
it 'uses find_remote to look for remote accounts' do
|
||||
subject.call('two@example.com', nil, limit: 10)
|
||||
expect(Account).to have_received(:find_remote).with('two', 'example.com')
|
||||
end
|
||||
|
||||
describe 'and there is no account provided' do
|
||||
it 'uses search_for to find matches' do
|
||||
allow(Account).to receive(:search_for)
|
||||
subject.call('two@example.com', nil, limit: 10, resolve: false)
|
||||
|
||||
expect(Account).to have_received(:search_for).with('two example.com', 10, 0)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'and there is an account provided' do
|
||||
it 'uses advanced_search_for to find matches' do
|
||||
account = Fabricate(:account)
|
||||
allow(Account).to receive(:advanced_search_for)
|
||||
subject.call('two@example.com', account, limit: 10, resolve: false)
|
||||
|
||||
expect(Account).to have_received(:advanced_search_for).with('two example.com', account, 10, nil, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with an exact match' do
|
||||
it 'returns exact match first, and does not return duplicates' do
|
||||
partial = Fabricate(:account, username: 'exactness')
|
||||
exact = Fabricate(:account, username: 'exact')
|
||||
|
||||
results = subject.call('exact', nil, limit: 10)
|
||||
expect(results.size).to eq 2
|
||||
expect(results).to eq [exact, partial]
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when there is a local domain' do
|
||||
around do |example|
|
||||
before = Rails.configuration.x.local_domain
|
||||
example.run
|
||||
Rails.configuration.x.local_domain = before
|
||||
end
|
||||
|
||||
it 'returns exact match first' do
|
||||
remote = Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e')
|
||||
remote_too = Fabricate(:account, username: 'b', domain: 'remote', display_name: 'e')
|
||||
exact = Fabricate(:account, username: 'e')
|
||||
Rails.configuration.x.local_domain = 'example.com'
|
||||
|
||||
results = subject.call('e@example.com', nil, limit: 2)
|
||||
expect(results.size).to eq 2
|
||||
expect(results).to eq([exact, remote]).or eq([exact, remote_too])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when there is a domain but no exact match' do
|
||||
it 'follows the remote account when resolve is true' do
|
||||
service = double(call: nil)
|
||||
allow(ResolveAccountService).to receive(:new).and_return(service)
|
||||
|
||||
results = subject.call('newuser@remote.com', nil, limit: 10, resolve: true)
|
||||
expect(service).to have_received(:call).with('newuser@remote.com')
|
||||
end
|
||||
|
||||
it 'does not follow the remote account when resolve is false' do
|
||||
service = double(call: nil)
|
||||
allow(ResolveAccountService).to receive(:new).and_return(service)
|
||||
|
||||
results = subject.call('newuser@remote.com', nil, limit: 10, resolve: false)
|
||||
expect(service).not_to have_received(:call)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'should not include suspended accounts' do
|
||||
it 'returns the fuzzy match first, and does not return suspended exacts' do
|
||||
partial = Fabricate(:account, username: 'exactness')
|
||||
exact = Fabricate(:account, username: 'exact', suspended: true)
|
||||
|
||||
results = subject.call('exact', nil, limit: 10)
|
||||
expect(results.size).to eq 1
|
||||
expect(results).to eq [partial]
|
||||
end
|
||||
|
||||
it "does not return suspended remote accounts" do
|
||||
remote = Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e', suspended: true)
|
||||
|
||||
results = subject.call('a@example.com', nil, limit: 2)
|
||||
expect(results.size).to eq 0
|
||||
expect(results).to eq []
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
128
spec/services/activitypub/fetch_remote_account_service_spec.rb
Normal file
128
spec/services/activitypub/fetch_remote_account_service_spec.rb
Normal file
@@ -0,0 +1,128 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
|
||||
subject { ActivityPub::FetchRemoteAccountService.new }
|
||||
|
||||
let!(:actor) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'https://example.com/alice',
|
||||
type: 'Person',
|
||||
preferredUsername: 'alice',
|
||||
name: 'Alice',
|
||||
summary: 'Foo bar',
|
||||
inbox: 'http://example.com/alice/inbox',
|
||||
}
|
||||
end
|
||||
|
||||
describe '#call' do
|
||||
let(:account) { subject.call('https://example.com/alice', id: true) }
|
||||
|
||||
shared_examples 'sets profile data' do
|
||||
it 'returns an account' do
|
||||
expect(account).to be_an Account
|
||||
end
|
||||
|
||||
it 'sets display name' do
|
||||
expect(account.display_name).to eq 'Alice'
|
||||
end
|
||||
|
||||
it 'sets note' do
|
||||
expect(account.note).to eq 'Foo bar'
|
||||
end
|
||||
|
||||
it 'sets URL' do
|
||||
expect(account.url).to eq 'https://example.com/alice'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the account does not have a inbox' do
|
||||
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||
|
||||
before do
|
||||
actor[:inbox] = nil
|
||||
|
||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
end
|
||||
|
||||
it 'fetches resource' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'looks up webfinger' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(account).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when URI and WebFinger share the same host' do
|
||||
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
end
|
||||
|
||||
it 'fetches resource' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'looks up webfinger' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'sets username and domain from webfinger' do
|
||||
expect(account.username).to eq 'alice'
|
||||
expect(account.domain).to eq 'example.com'
|
||||
end
|
||||
|
||||
include_examples 'sets profile data'
|
||||
end
|
||||
|
||||
context 'when WebFinger presents different domain than URI' do
|
||||
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
end
|
||||
|
||||
it 'fetches resource' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'looks up webfinger' do
|
||||
account
|
||||
expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'looks up "redirected" webfinger' do
|
||||
account
|
||||
expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'sets username and domain from final webfinger' do
|
||||
expect(account.username).to eq 'alice'
|
||||
expect(account.domain).to eq 'iscool.af'
|
||||
end
|
||||
|
||||
include_examples 'sets profile data'
|
||||
end
|
||||
|
||||
context 'with wrong id' do
|
||||
it 'does not create account' do
|
||||
expect(subject.call('https://fake.address/@foo', prefetched_body: Oj.dump(actor))).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,96 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do
|
||||
include ActionView::Helpers::TextHelper
|
||||
|
||||
let(:sender) { Fabricate(:account) }
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
let(:valid_domain) { Rails.configuration.x.local_domain }
|
||||
|
||||
let(:note) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: "https://#{valid_domain}/@foo/1234",
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
}
|
||||
end
|
||||
|
||||
subject { described_class.new }
|
||||
|
||||
describe '#call' do
|
||||
before do
|
||||
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
|
||||
|
||||
stub_request(:head, 'https://example.com/watch?v=12345').to_return(status: 404, body: '')
|
||||
subject.call(object[:id], prefetched_body: Oj.dump(object))
|
||||
end
|
||||
|
||||
context 'with Note object' do
|
||||
let(:object) { note }
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.text).to eq 'Lorem ipsum'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with Video object' do
|
||||
let(:object) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: "https://#{valid_domain}/@foo/1234",
|
||||
type: 'Video',
|
||||
name: 'Nyan Cat 10 hours remix',
|
||||
attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
url: [
|
||||
{
|
||||
type: 'Link',
|
||||
mimeType: 'application/x-bittorrent',
|
||||
href: "https://#{valid_domain}/12345.torrent",
|
||||
},
|
||||
|
||||
{
|
||||
type: 'Link',
|
||||
mimeType: 'text/html',
|
||||
href: "https://#{valid_domain}/watch?v=12345",
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.url).to eq "https://#{valid_domain}/watch?v=12345"
|
||||
expect(strip_tags(status.text)).to eq "Nyan Cat 10 hours remix https://#{valid_domain}/watch?v=12345"
|
||||
end
|
||||
end
|
||||
|
||||
context 'with wrong id' do
|
||||
let(:note) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: "https://real.address/@foo/1234",
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
}
|
||||
end
|
||||
|
||||
let(:object) do
|
||||
temp = note.dup
|
||||
temp[:id] = 'https://fake.address/@foo/5678'
|
||||
temp
|
||||
end
|
||||
|
||||
it 'does not create status' do
|
||||
expect(sender.statuses.first).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
122
spec/services/activitypub/fetch_replies_service_spec.rb
Normal file
122
spec/services/activitypub/fetch_replies_service_spec.rb
Normal file
@@ -0,0 +1,122 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::FetchRepliesService, type: :service do
|
||||
let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') }
|
||||
let(:status) { Fabricate(:status, account: actor) }
|
||||
let(:collection_uri) { 'http://example.com/replies/1' }
|
||||
|
||||
let(:items) do
|
||||
[
|
||||
'http://example.com/self-reply-1',
|
||||
'http://example.com/self-reply-2',
|
||||
'http://example.com/self-reply-3',
|
||||
'http://other.com/other-reply-1',
|
||||
'http://other.com/other-reply-2',
|
||||
'http://other.com/other-reply-3',
|
||||
'http://example.com/self-reply-4',
|
||||
'http://example.com/self-reply-5',
|
||||
'http://example.com/self-reply-6',
|
||||
]
|
||||
end
|
||||
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'Collection',
|
||||
id: collection_uri,
|
||||
items: items,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
subject { described_class.new }
|
||||
|
||||
describe '#call' do
|
||||
context 'when the payload is a Collection with inlined replies' do
|
||||
context 'when passing the collection itself' do
|
||||
it 'spawns workers for up to 5 replies on the same server' do
|
||||
allow(FetchReplyWorker).to receive(:push_bulk)
|
||||
subject.call(status, payload)
|
||||
expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing the URL to the collection' do
|
||||
before do
|
||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
||||
end
|
||||
|
||||
it 'spawns workers for up to 5 replies on the same server' do
|
||||
allow(FetchReplyWorker).to receive(:push_bulk)
|
||||
subject.call(status, collection_uri)
|
||||
expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the payload is an OrderedCollection with inlined replies' do
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'OrderedCollection',
|
||||
id: collection_uri,
|
||||
orderedItems: items,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
context 'when passing the collection itself' do
|
||||
it 'spawns workers for up to 5 replies on the same server' do
|
||||
allow(FetchReplyWorker).to receive(:push_bulk)
|
||||
subject.call(status, payload)
|
||||
expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing the URL to the collection' do
|
||||
before do
|
||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
||||
end
|
||||
|
||||
it 'spawns workers for up to 5 replies on the same server' do
|
||||
allow(FetchReplyWorker).to receive(:push_bulk)
|
||||
subject.call(status, collection_uri)
|
||||
expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the payload is a paginated Collection with inlined replies' do
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'Collection',
|
||||
id: collection_uri,
|
||||
first: {
|
||||
type: 'CollectionPage',
|
||||
partOf: collection_uri,
|
||||
items: items,
|
||||
}
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
context 'when passing the collection itself' do
|
||||
it 'spawns workers for up to 5 replies on the same server' do
|
||||
allow(FetchReplyWorker).to receive(:push_bulk)
|
||||
subject.call(status, payload)
|
||||
expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing the URL to the collection' do
|
||||
before do
|
||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
||||
end
|
||||
|
||||
it 'spawns workers for up to 5 replies on the same server' do
|
||||
allow(FetchReplyWorker).to receive(:push_bulk)
|
||||
subject.call(status, collection_uri)
|
||||
expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
76
spec/services/activitypub/process_account_service_spec.rb
Normal file
76
spec/services/activitypub/process_account_service_spec.rb
Normal file
@@ -0,0 +1,76 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::ProcessAccountService, type: :service do
|
||||
subject { described_class.new }
|
||||
|
||||
context 'property values' do
|
||||
let(:payload) do
|
||||
{
|
||||
id: 'https://foo.test',
|
||||
type: 'Actor',
|
||||
inbox: 'https://foo.test/inbox',
|
||||
attachment: [
|
||||
{ type: 'PropertyValue', name: 'Pronouns', value: 'They/them' },
|
||||
{ type: 'PropertyValue', name: 'Occupation', value: 'Unit test' },
|
||||
],
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
it 'parses out of attachment' do
|
||||
account = subject.call('alice', 'example.com', payload)
|
||||
expect(account.fields).to be_a Array
|
||||
expect(account.fields.size).to eq 2
|
||||
expect(account.fields[0]).to be_a Account::Field
|
||||
expect(account.fields[0].name).to eq 'Pronouns'
|
||||
expect(account.fields[0].value).to eq 'They/them'
|
||||
expect(account.fields[1]).to be_a Account::Field
|
||||
expect(account.fields[1].name).to eq 'Occupation'
|
||||
expect(account.fields[1].value).to eq 'Unit test'
|
||||
end
|
||||
end
|
||||
|
||||
context 'identity proofs' do
|
||||
let(:payload) do
|
||||
{
|
||||
id: 'https://foo.test',
|
||||
type: 'Actor',
|
||||
inbox: 'https://foo.test/inbox',
|
||||
attachment: [
|
||||
{ type: 'IdentityProof', name: 'Alice', signatureAlgorithm: 'keybase', signatureValue: 'a' * 66 },
|
||||
],
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
it 'parses out of attachment' do
|
||||
allow(ProofProvider::Keybase::Worker).to receive(:perform_async)
|
||||
|
||||
account = subject.call('alice', 'example.com', payload)
|
||||
|
||||
expect(account.identity_proofs.count).to eq 1
|
||||
|
||||
proof = account.identity_proofs.first
|
||||
|
||||
expect(proof.provider).to eq 'keybase'
|
||||
expect(proof.provider_username).to eq 'Alice'
|
||||
expect(proof.token).to eq 'a' * 66
|
||||
end
|
||||
|
||||
it 'removes no longer present proofs' do
|
||||
allow(ProofProvider::Keybase::Worker).to receive(:perform_async)
|
||||
|
||||
account = Fabricate(:account, username: 'alice', domain: 'example.com')
|
||||
old_proof = Fabricate(:account_identity_proof, account: account, provider: 'keybase', provider_username: 'Bob', token: 'b' * 66)
|
||||
|
||||
subject.call('alice', 'example.com', payload)
|
||||
|
||||
expect(account.identity_proofs.count).to eq 1
|
||||
expect(account.identity_proofs.find_by(id: old_proof.id)).to be_nil
|
||||
end
|
||||
|
||||
it 'queues a validity check on the proof' do
|
||||
allow(ProofProvider::Keybase::Worker).to receive(:perform_async)
|
||||
account = subject.call('alice', 'example.com', payload)
|
||||
expect(ProofProvider::Keybase::Worker).to have_received(:perform_async)
|
||||
end
|
||||
end
|
||||
end
|
||||
55
spec/services/activitypub/process_collection_service_spec.rb
Normal file
55
spec/services/activitypub/process_collection_service_spec.rb
Normal file
@@ -0,0 +1,55 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
|
||||
let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') }
|
||||
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Create',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(actor),
|
||||
object: {
|
||||
id: 'bar',
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
let(:json) { Oj.dump(payload) }
|
||||
|
||||
subject { described_class.new }
|
||||
|
||||
describe '#call' do
|
||||
context 'when actor is the sender'
|
||||
context 'when actor differs from sender' do
|
||||
let(:forwarder) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/other_account') }
|
||||
|
||||
it 'does not process payload if no signature exists' do
|
||||
expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(nil)
|
||||
expect(ActivityPub::Activity).not_to receive(:factory)
|
||||
|
||||
subject.call(json, forwarder)
|
||||
end
|
||||
|
||||
it 'processes payload with actor if valid signature exists' do
|
||||
payload['signature'] = { 'type' => 'RsaSignature2017' }
|
||||
|
||||
expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(actor)
|
||||
expect(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), actor, instance_of(Hash))
|
||||
|
||||
subject.call(json, forwarder)
|
||||
end
|
||||
|
||||
it 'does not process payload if invalid signature exists' do
|
||||
payload['signature'] = { 'type' => 'RsaSignature2017' }
|
||||
|
||||
expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(nil)
|
||||
expect(ActivityPub::Activity).not_to receive(:factory)
|
||||
|
||||
subject.call(json, forwarder)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AfterBlockDomainFromAccountService, type: :service do
|
||||
let!(:wolf) { Fabricate(:account, username: 'wolf', domain: 'evil.org', inbox_url: 'https://evil.org/inbox', protocol: :activitypub) }
|
||||
let!(:alice) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
subject { AfterBlockDomainFromAccountService.new }
|
||||
|
||||
before do
|
||||
stub_jsonld_contexts!
|
||||
allow(ActivityPub::DeliveryWorker).to receive(:perform_async)
|
||||
end
|
||||
|
||||
it 'purge followers from blocked domain' do
|
||||
wolf.follow!(alice)
|
||||
subject.call(alice, 'evil.org')
|
||||
expect(wolf.following?(alice)).to be false
|
||||
end
|
||||
|
||||
it 'sends Reject->Follow to followers from blocked domain' do
|
||||
wolf.follow!(alice)
|
||||
subject.call(alice, 'evil.org')
|
||||
expect(ActivityPub::DeliveryWorker).to have_received(:perform_async).once
|
||||
end
|
||||
end
|
||||
29
spec/services/after_block_service_spec.rb
Normal file
29
spec/services/after_block_service_spec.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AfterBlockService, type: :service do
|
||||
subject do
|
||||
-> { described_class.new.call(account, target_account) }
|
||||
end
|
||||
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:target_account) { Fabricate(:account) }
|
||||
|
||||
describe 'home timeline' do
|
||||
let(:status) { Fabricate(:status, account: target_account) }
|
||||
let(:other_account_status) { Fabricate(:status) }
|
||||
let(:home_timeline_key) { FeedManager.instance.key(:home, account.id) }
|
||||
|
||||
before do
|
||||
Redis.current.del(home_timeline_key)
|
||||
end
|
||||
|
||||
it "clears account's statuses" do
|
||||
FeedManager.instance.push_to_home(account, status)
|
||||
FeedManager.instance.push_to_home(account, other_account_status)
|
||||
|
||||
is_expected.to change {
|
||||
Redis.current.zrange(home_timeline_key, 0, -1)
|
||||
}.from([status.id.to_s, other_account_status.id.to_s]).to([other_account_status.id.to_s])
|
||||
end
|
||||
end
|
||||
end
|
||||
43
spec/services/app_sign_up_service_spec.rb
Normal file
43
spec/services/app_sign_up_service_spec.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AppSignUpService, type: :service do
|
||||
let(:app) { Fabricate(:application, scopes: 'read write') }
|
||||
let(:good_params) { { username: 'alice', password: '12345678', email: 'good@email.com', agreement: true } }
|
||||
|
||||
subject { described_class.new }
|
||||
|
||||
describe '#call' do
|
||||
it 'returns nil when registrations are closed' do
|
||||
tmp = Setting.registrations_mode
|
||||
Setting.registrations_mode = 'none'
|
||||
expect(subject.call(app, good_params)).to be_nil
|
||||
Setting.registrations_mode = tmp
|
||||
end
|
||||
|
||||
it 'raises an error when params are missing' do
|
||||
expect { subject.call(app, {}) }.to raise_error ActiveRecord::RecordInvalid
|
||||
end
|
||||
|
||||
it 'creates an unconfirmed user with access token' do
|
||||
access_token = subject.call(app, good_params)
|
||||
expect(access_token).to_not be_nil
|
||||
user = User.find_by(id: access_token.resource_owner_id)
|
||||
expect(user).to_not be_nil
|
||||
expect(user.confirmed?).to be false
|
||||
end
|
||||
|
||||
it 'creates access token with the app\'s scopes' do
|
||||
access_token = subject.call(app, good_params)
|
||||
expect(access_token).to_not be_nil
|
||||
expect(access_token.scopes.to_s).to eq 'read write'
|
||||
end
|
||||
|
||||
it 'creates an account' do
|
||||
access_token = subject.call(app, good_params)
|
||||
expect(access_token).to_not be_nil
|
||||
user = User.find_by(id: access_token.resource_owner_id)
|
||||
expect(user).to_not be_nil
|
||||
expect(user.account).to_not be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
71
spec/services/authorize_follow_service_spec.rb
Normal file
71
spec/services/authorize_follow_service_spec.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AuthorizeFollowService, type: :service do
|
||||
let(:sender) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
subject { AuthorizeFollowService.new }
|
||||
|
||||
describe 'local' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||
|
||||
before do
|
||||
FollowRequest.create(account: bob, target_account: sender)
|
||||
subject.call(bob, sender)
|
||||
end
|
||||
|
||||
it 'removes follow request' do
|
||||
expect(bob.requested?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'creates follow relation' do
|
||||
expect(bob.following?(sender)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remote OStatus' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
|
||||
|
||||
before do
|
||||
FollowRequest.create(account: bob, target_account: sender)
|
||||
stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
|
||||
subject.call(bob, sender)
|
||||
end
|
||||
|
||||
it 'removes follow request' do
|
||||
expect(bob.requested?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'creates follow relation' do
|
||||
expect(bob.following?(sender)).to be true
|
||||
end
|
||||
|
||||
it 'sends a follow request authorization salmon slap' do
|
||||
expect(a_request(:post, "http://salmon.example.com/").with { |req|
|
||||
xml = OStatus2::Salmon.new.unpack(req.body)
|
||||
xml.match(OStatus::TagManager::VERBS[:authorize])
|
||||
}).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remote ActivityPub' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account }
|
||||
|
||||
before do
|
||||
FollowRequest.create(account: bob, target_account: sender)
|
||||
stub_request(:post, bob.inbox_url).to_return(status: 200)
|
||||
subject.call(bob, sender)
|
||||
end
|
||||
|
||||
it 'removes follow request' do
|
||||
expect(bob.requested?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'creates follow relation' do
|
||||
expect(bob.following?(sender)).to be true
|
||||
end
|
||||
|
||||
it 'sends an accept activity' do
|
||||
expect(a_request(:post, bob.inbox_url)).to have_been_made.once
|
||||
end
|
||||
end
|
||||
end
|
||||
68
spec/services/batched_remove_status_service_spec.rb
Normal file
68
spec/services/batched_remove_status_service_spec.rb
Normal file
@@ -0,0 +1,68 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe BatchedRemoveStatusService, type: :service do
|
||||
subject { BatchedRemoveStatusService.new }
|
||||
|
||||
let!(:alice) { Fabricate(:account) }
|
||||
let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://example.com/salmon') }
|
||||
let!(:jeff) { Fabricate(:user).account }
|
||||
let!(:hank) { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
|
||||
|
||||
let(:status1) { PostStatusService.new.call(alice, text: 'Hello @bob@example.com') }
|
||||
let(:status2) { PostStatusService.new.call(alice, text: 'Another status') }
|
||||
|
||||
before do
|
||||
allow(Redis.current).to receive_messages(publish: nil)
|
||||
|
||||
stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {})
|
||||
stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {})
|
||||
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
|
||||
|
||||
Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now)
|
||||
jeff.user.update(current_sign_in_at: Time.zone.now)
|
||||
jeff.follow!(alice)
|
||||
hank.follow!(alice)
|
||||
|
||||
status1
|
||||
status2
|
||||
|
||||
subject.call([status1, status2])
|
||||
end
|
||||
|
||||
it 'removes statuses from author\'s home feed' do
|
||||
expect(HomeFeed.new(alice).get(10)).to_not include([status1.id, status2.id])
|
||||
end
|
||||
|
||||
it 'removes statuses from local follower\'s home feed' do
|
||||
expect(HomeFeed.new(jeff).get(10)).to_not include([status1.id, status2.id])
|
||||
end
|
||||
|
||||
it 'notifies streaming API of followers' do
|
||||
expect(Redis.current).to have_received(:publish).with("timeline:#{jeff.id}", any_args).at_least(:once)
|
||||
end
|
||||
|
||||
it 'notifies streaming API of author' do
|
||||
expect(Redis.current).to have_received(:publish).with("timeline:#{alice.id}", any_args).at_least(:once)
|
||||
end
|
||||
|
||||
it 'notifies streaming API of public timeline' do
|
||||
expect(Redis.current).to have_received(:publish).with('timeline:public', any_args).at_least(:once)
|
||||
end
|
||||
|
||||
it 'sends PuSH update to PuSH subscribers' do
|
||||
expect(a_request(:post, 'http://example.com/push').with { |req|
|
||||
matches = req.body.match(OStatus::TagManager::VERBS[:delete])
|
||||
}).to have_been_made.at_least_once
|
||||
end
|
||||
|
||||
it 'sends Salmon slap to previously mentioned users' do
|
||||
expect(a_request(:post, "http://example.com/salmon").with { |req|
|
||||
xml = OStatus2::Salmon.new.unpack(req.body)
|
||||
xml.match(OStatus::TagManager::VERBS[:delete])
|
||||
}).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'sends delete activity to followers' do
|
||||
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.at_least_once
|
||||
end
|
||||
end
|
||||
76
spec/services/block_domain_service_spec.rb
Normal file
76
spec/services/block_domain_service_spec.rb
Normal file
@@ -0,0 +1,76 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe BlockDomainService, type: :service do
|
||||
let!(:bad_account) { Fabricate(:account, username: 'badguy666', domain: 'evil.org') }
|
||||
let!(:bad_status1) { Fabricate(:status, account: bad_account, text: 'You suck') }
|
||||
let!(:bad_status2) { Fabricate(:status, account: bad_account, text: 'Hahaha') }
|
||||
let!(:bad_attachment) { Fabricate(:media_attachment, account: bad_account, status: bad_status2, file: attachment_fixture('attachment.jpg')) }
|
||||
let!(:already_banned_account) { Fabricate(:account, username: 'badguy', domain: 'evil.org', suspended: true, silenced: true) }
|
||||
|
||||
subject { BlockDomainService.new }
|
||||
|
||||
describe 'for a suspension' do
|
||||
before do
|
||||
subject.call(DomainBlock.create!(domain: 'evil.org', severity: :suspend))
|
||||
end
|
||||
|
||||
it 'creates a domain block' do
|
||||
expect(DomainBlock.blocked?('evil.org')).to be true
|
||||
end
|
||||
|
||||
it 'removes remote accounts from that domain' do
|
||||
expect(Account.find_remote('badguy666', 'evil.org').suspended?).to be true
|
||||
end
|
||||
|
||||
it 'records suspension date appropriately' do
|
||||
expect(Account.find_remote('badguy666', 'evil.org').suspended_at).to eq DomainBlock.find_by(domain: 'evil.org').created_at
|
||||
end
|
||||
|
||||
it 'keeps already-banned accounts banned' do
|
||||
expect(Account.find_remote('badguy', 'evil.org').suspended?).to be true
|
||||
end
|
||||
|
||||
it 'does not overwrite suspension date of already-banned accounts' do
|
||||
expect(Account.find_remote('badguy', 'evil.org').suspended_at).to_not eq DomainBlock.find_by(domain: 'evil.org').created_at
|
||||
end
|
||||
|
||||
it 'removes the remote accounts\'s statuses and media attachments' do
|
||||
expect { bad_status1.reload }.to raise_exception ActiveRecord::RecordNotFound
|
||||
expect { bad_status2.reload }.to raise_exception ActiveRecord::RecordNotFound
|
||||
expect { bad_attachment.reload }.to raise_exception ActiveRecord::RecordNotFound
|
||||
end
|
||||
end
|
||||
|
||||
describe 'for a silence with reject media' do
|
||||
before do
|
||||
subject.call(DomainBlock.create!(domain: 'evil.org', severity: :silence, reject_media: true))
|
||||
end
|
||||
|
||||
it 'does not create a domain block' do
|
||||
expect(DomainBlock.blocked?('evil.org')).to be false
|
||||
end
|
||||
|
||||
it 'silences remote accounts from that domain' do
|
||||
expect(Account.find_remote('badguy666', 'evil.org').silenced?).to be true
|
||||
end
|
||||
|
||||
it 'records suspension date appropriately' do
|
||||
expect(Account.find_remote('badguy666', 'evil.org').silenced_at).to eq DomainBlock.find_by(domain: 'evil.org').created_at
|
||||
end
|
||||
|
||||
it 'keeps already-banned accounts banned' do
|
||||
expect(Account.find_remote('badguy', 'evil.org').silenced?).to be true
|
||||
end
|
||||
|
||||
it 'does not overwrite suspension date of already-banned accounts' do
|
||||
expect(Account.find_remote('badguy', 'evil.org').silenced_at).to_not eq DomainBlock.find_by(domain: 'evil.org').created_at
|
||||
end
|
||||
|
||||
it 'leaves the domains status and attachements, but clears media' do
|
||||
expect { bad_status1.reload }.not_to raise_error
|
||||
expect { bad_status2.reload }.not_to raise_error
|
||||
expect { bad_attachment.reload }.not_to raise_error
|
||||
expect(bad_attachment.file.exists?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
56
spec/services/block_service_spec.rb
Normal file
56
spec/services/block_service_spec.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe BlockService, type: :service do
|
||||
let(:sender) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
subject { BlockService.new }
|
||||
|
||||
describe 'local' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||
|
||||
before do
|
||||
subject.call(sender, bob)
|
||||
end
|
||||
|
||||
it 'creates a blocking relation' do
|
||||
expect(sender.blocking?(bob)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remote OStatus' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
|
||||
|
||||
before do
|
||||
stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
|
||||
subject.call(sender, bob)
|
||||
end
|
||||
|
||||
it 'creates a blocking relation' do
|
||||
expect(sender.blocking?(bob)).to be true
|
||||
end
|
||||
|
||||
it 'sends a block salmon slap' do
|
||||
expect(a_request(:post, "http://salmon.example.com/").with { |req|
|
||||
xml = OStatus2::Salmon.new.unpack(req.body)
|
||||
xml.match(OStatus::TagManager::VERBS[:block])
|
||||
}).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remote ActivityPub' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account }
|
||||
|
||||
before do
|
||||
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
|
||||
subject.call(sender, bob)
|
||||
end
|
||||
|
||||
it 'creates a blocking relation' do
|
||||
expect(sender.blocking?(bob)).to be true
|
||||
end
|
||||
|
||||
it 'sends a block activity' do
|
||||
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
|
||||
end
|
||||
end
|
||||
end
|
||||
37
spec/services/bootstrap_timeline_service_spec.rb
Normal file
37
spec/services/bootstrap_timeline_service_spec.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe BootstrapTimelineService, type: :service do
|
||||
subject { described_class.new }
|
||||
|
||||
describe '#call' do
|
||||
let(:source_account) { Fabricate(:account) }
|
||||
|
||||
context 'when setting is empty' do
|
||||
let!(:admin) { Fabricate(:user, admin: true) }
|
||||
|
||||
before do
|
||||
Setting.bootstrap_timeline_accounts = nil
|
||||
subject.call(source_account)
|
||||
end
|
||||
|
||||
it 'follows admin accounts from account' do
|
||||
expect(source_account.following?(admin.account)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when setting is set' do
|
||||
let!(:alice) { Fabricate(:account, username: 'alice') }
|
||||
let!(:bob) { Fabricate(:account, username: 'bob') }
|
||||
|
||||
before do
|
||||
Setting.bootstrap_timeline_accounts = 'alice, bob'
|
||||
subject.call(source_account)
|
||||
end
|
||||
|
||||
it 'follows found accounts from account' do
|
||||
expect(source_account.following?(alice)).to be true
|
||||
expect(source_account.following?(bob)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
37
spec/services/fan_out_on_write_service_spec.rb
Normal file
37
spec/services/fan_out_on_write_service_spec.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe FanOutOnWriteService, type: :service do
|
||||
let(:author) { Fabricate(:account, username: 'tom') }
|
||||
let(:status) { Fabricate(:status, text: 'Hello @alice #test', account: author) }
|
||||
let(:alice) { Fabricate(:user, account: Fabricate(:account, username: 'alice')).account }
|
||||
let(:follower) { Fabricate(:account, username: 'bob') }
|
||||
|
||||
subject { FanOutOnWriteService.new }
|
||||
|
||||
before do
|
||||
alice
|
||||
follower.follow!(author)
|
||||
|
||||
ProcessMentionsService.new.call(status)
|
||||
ProcessHashtagsService.new.call(status)
|
||||
|
||||
subject.call(status)
|
||||
end
|
||||
|
||||
it 'delivers status to home timeline' do
|
||||
expect(HomeFeed.new(author).get(10).map(&:id)).to include status.id
|
||||
end
|
||||
|
||||
it 'delivers status to local followers' do
|
||||
pending 'some sort of problem in test environment causes this to sometimes fail'
|
||||
expect(HomeFeed.new(follower).get(10).map(&:id)).to include status.id
|
||||
end
|
||||
|
||||
it 'delivers status to hashtag' do
|
||||
expect(Tag.find_by!(name: 'test').statuses.pluck(:id)).to include status.id
|
||||
end
|
||||
|
||||
it 'delivers status to public timeline' do
|
||||
expect(Status.as_public_timeline(alice).map(&:id)).to include status.id
|
||||
end
|
||||
end
|
||||
59
spec/services/favourite_service_spec.rb
Normal file
59
spec/services/favourite_service_spec.rb
Normal file
@@ -0,0 +1,59 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe FavouriteService, type: :service do
|
||||
let(:sender) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
subject { FavouriteService.new }
|
||||
|
||||
describe 'local' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||
let(:status) { Fabricate(:status, account: bob) }
|
||||
|
||||
before do
|
||||
subject.call(sender, status)
|
||||
end
|
||||
|
||||
it 'creates a favourite' do
|
||||
expect(status.favourites.first).to_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remote OStatus' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
|
||||
let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com:blahblah') }
|
||||
|
||||
before do
|
||||
stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
|
||||
subject.call(sender, status)
|
||||
end
|
||||
|
||||
it 'creates a favourite' do
|
||||
expect(status.favourites.first).to_not be_nil
|
||||
end
|
||||
|
||||
it 'sends a salmon slap' do
|
||||
expect(a_request(:post, "http://salmon.example.com/").with { |req|
|
||||
xml = OStatus2::Salmon.new.unpack(req.body)
|
||||
xml.match(OStatus::TagManager::VERBS[:favorite])
|
||||
}).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remote ActivityPub' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :activitypub, username: 'bob', domain: 'example.com', inbox_url: 'http://example.com/inbox')).account }
|
||||
let(:status) { Fabricate(:status, account: bob) }
|
||||
|
||||
before do
|
||||
stub_request(:post, "http://example.com/inbox").to_return(:status => 200, :body => "", :headers => {})
|
||||
subject.call(sender, status)
|
||||
end
|
||||
|
||||
it 'creates a favourite' do
|
||||
expect(status.favourites.first).to_not be_nil
|
||||
end
|
||||
|
||||
it 'sends a like activity' do
|
||||
expect(a_request(:post, "http://example.com/inbox")).to have_been_made.once
|
||||
end
|
||||
end
|
||||
end
|
||||
96
spec/services/fetch_atom_service_spec.rb
Normal file
96
spec/services/fetch_atom_service_spec.rb
Normal file
@@ -0,0 +1,96 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe FetchAtomService, type: :service do
|
||||
describe '#call' do
|
||||
let(:url) { 'http://example.com' }
|
||||
subject { FetchAtomService.new.call(url) }
|
||||
|
||||
context 'url is blank' do
|
||||
let(:url) { '' }
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'request failed' do
|
||||
before do
|
||||
WebMock.stub_request(:get, url).to_return(status: 500, body: '', headers: {})
|
||||
end
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'raise OpenSSL::SSL::SSLError' do
|
||||
before do
|
||||
allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(OpenSSL::SSL::SSLError)
|
||||
end
|
||||
|
||||
it 'output log and return nil' do
|
||||
expect_any_instance_of(ActiveSupport::Logger).to receive(:debug).with('SSL error: OpenSSL::SSL::SSLError')
|
||||
is_expected.to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'raise HTTP::ConnectionError' do
|
||||
before do
|
||||
allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(HTTP::ConnectionError)
|
||||
end
|
||||
|
||||
it 'output log and return nil' do
|
||||
expect_any_instance_of(ActiveSupport::Logger).to receive(:debug).with('HTTP ConnectionError: HTTP::ConnectionError')
|
||||
is_expected.to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'response success' do
|
||||
let(:body) { '' }
|
||||
let(:headers) { { 'Content-Type' => content_type } }
|
||||
let(:json) {
|
||||
{ id: 1,
|
||||
'@context': ActivityPub::TagManager::CONTEXT,
|
||||
type: 'Note',
|
||||
}.to_json
|
||||
}
|
||||
|
||||
before do
|
||||
WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers)
|
||||
end
|
||||
|
||||
context 'content type is application/atom+xml' do
|
||||
let(:content_type) { 'application/atom+xml' }
|
||||
|
||||
it { is_expected.to eq [url, { :prefetched_body => "" }, :ostatus] }
|
||||
end
|
||||
|
||||
context 'content_type is activity+json' do
|
||||
let(:content_type) { 'application/activity+json; charset=utf-8' }
|
||||
let(:body) { json }
|
||||
|
||||
it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] }
|
||||
end
|
||||
|
||||
context 'content_type is ld+json with profile' do
|
||||
let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }
|
||||
let(:body) { json }
|
||||
|
||||
it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] }
|
||||
end
|
||||
|
||||
before do
|
||||
WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers)
|
||||
WebMock.stub_request(:get, 'http://example.com/foo').to_return(status: 200, body: json, headers: { 'Content-Type' => 'application/activity+json' })
|
||||
end
|
||||
|
||||
context 'has link header' do
|
||||
let(:headers) { { 'Link' => '<http://example.com/foo>; rel="alternate"; type="application/activity+json"', } }
|
||||
|
||||
it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] }
|
||||
end
|
||||
|
||||
context 'content type is text/html' do
|
||||
let(:content_type) { 'text/html' }
|
||||
let(:body) { '<html><head><link rel="alternate" href="http://example.com/foo" type="application/activity+json"/></head></html>' }
|
||||
|
||||
it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
100
spec/services/fetch_link_card_service_spec.rb
Normal file
100
spec/services/fetch_link_card_service_spec.rb
Normal file
@@ -0,0 +1,100 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe FetchLinkCardService, type: :service do
|
||||
subject { FetchLinkCardService.new }
|
||||
|
||||
before do
|
||||
stub_request(:head, 'http://example.xn--fiqs8s/').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
|
||||
stub_request(:get, 'http://example.xn--fiqs8s/').to_return(request_fixture('idn.txt'))
|
||||
stub_request(:head, 'http://example.com/sjis').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
|
||||
stub_request(:get, 'http://example.com/sjis').to_return(request_fixture('sjis.txt'))
|
||||
stub_request(:head, 'http://example.com/sjis_with_wrong_charset').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
|
||||
stub_request(:get, 'http://example.com/sjis_with_wrong_charset').to_return(request_fixture('sjis_with_wrong_charset.txt'))
|
||||
stub_request(:head, 'http://example.com/koi8-r').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
|
||||
stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt'))
|
||||
stub_request(:head, 'http://example.com/日本語').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
|
||||
stub_request(:get, 'http://example.com/日本語').to_return(request_fixture('sjis.txt'))
|
||||
stub_request(:head, 'https://github.com/qbi/WannaCry').to_return(status: 404)
|
||||
stub_request(:head, 'http://example.com/test-').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
|
||||
stub_request(:get, 'http://example.com/test-').to_return(request_fixture('idn.txt'))
|
||||
stub_request(:head, 'http://example.com/windows-1251').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
|
||||
stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.txt'))
|
||||
|
||||
subject.call(status)
|
||||
end
|
||||
|
||||
context 'in a local status' do
|
||||
context do
|
||||
let(:status) { Fabricate(:status, text: 'Check out http://example.中国') }
|
||||
|
||||
it 'works with IDN URLs' do
|
||||
expect(a_request(:get, 'http://example.xn--fiqs8s/')).to have_been_made.at_least_once
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis') }
|
||||
|
||||
it 'works with SJIS' do
|
||||
expect(a_request(:get, 'http://example.com/sjis')).to have_been_made.at_least_once
|
||||
expect(status.preview_cards.first.title).to eq("SJISのページ")
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis_with_wrong_charset') }
|
||||
|
||||
it 'works with SJIS even with wrong charset header' do
|
||||
expect(a_request(:get, 'http://example.com/sjis_with_wrong_charset')).to have_been_made.at_least_once
|
||||
expect(status.preview_cards.first.title).to eq("SJISのページ")
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
let(:status) { Fabricate(:status, text: 'Check out http://example.com/koi8-r') }
|
||||
|
||||
it 'works with koi8-r' do
|
||||
expect(a_request(:get, 'http://example.com/koi8-r')).to have_been_made.at_least_once
|
||||
expect(status.preview_cards.first.title).to eq("Московя начинаетъ только въ XVI ст. привлекать внимане иностранцевъ.")
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
let(:status) { Fabricate(:status, text: 'Check out http://example.com/windows-1251') }
|
||||
|
||||
it 'works with windows-1251' do
|
||||
expect(a_request(:get, 'http://example.com/windows-1251')).to have_been_made.at_least_once
|
||||
expect(status.preview_cards.first.title).to eq('сэмпл текст')
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
let(:status) { Fabricate(:status, text: 'テストhttp://example.com/日本語') }
|
||||
|
||||
it 'works with Japanese path string' do
|
||||
expect(a_request(:get, 'http://example.com/日本語')).to have_been_made.at_least_once
|
||||
expect(status.preview_cards.first.title).to eq("SJISのページ")
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
let(:status) { Fabricate(:status, text: 'test http://example.com/test-') }
|
||||
|
||||
it 'works with a URL ending with a hyphen' do
|
||||
expect(a_request(:get, 'http://example.com/test-')).to have_been_made.at_least_once
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'in a remote status' do
|
||||
let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen? Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener" title="http://sn.jonkman.ca/group/416/id">security</a> ') }
|
||||
|
||||
it 'parses out URLs' do
|
||||
expect(a_request(:head, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once
|
||||
end
|
||||
|
||||
it 'ignores URLs to hashtags' do
|
||||
expect(a_request(:head, 'https://quitter.se/tag/wannacry')).to_not have_been_made
|
||||
end
|
||||
end
|
||||
end
|
||||
143
spec/services/fetch_oembed_service_spec.rb
Normal file
143
spec/services/fetch_oembed_service_spec.rb
Normal file
@@ -0,0 +1,143 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe FetchOEmbedService, type: :service do
|
||||
subject { described_class.new }
|
||||
|
||||
before do
|
||||
stub_request(:get, "https://host.test/provider.json").to_return(status: 404)
|
||||
stub_request(:get, "https://host.test/provider.xml").to_return(status: 404)
|
||||
stub_request(:get, "https://host.test/empty_provider.json").to_return(status: 200)
|
||||
end
|
||||
|
||||
describe 'discover_provider' do
|
||||
context 'when status code is 200 and MIME type is text/html' do
|
||||
context 'Both of JSON and XML provider are discoverable' do
|
||||
before do
|
||||
stub_request(:get, 'https://host.test/oembed.html').to_return(
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
body: request_fixture('oembed_json_xml.html')
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns new OEmbed::Provider for JSON provider if :format option is set to :json' do
|
||||
subject.call('https://host.test/oembed.html', format: :json)
|
||||
expect(subject.endpoint_url).to eq 'https://host.test/provider.json'
|
||||
expect(subject.format).to eq :json
|
||||
end
|
||||
|
||||
it 'returns new OEmbed::Provider for XML provider if :format option is set to :xml' do
|
||||
subject.call('https://host.test/oembed.html', format: :xml)
|
||||
expect(subject.endpoint_url).to eq 'https://host.test/provider.xml'
|
||||
expect(subject.format).to eq :xml
|
||||
end
|
||||
end
|
||||
|
||||
context 'JSON provider is discoverable while XML provider is not' do
|
||||
before do
|
||||
stub_request(:get, 'https://host.test/oembed.html').to_return(
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
body: request_fixture('oembed_json.html')
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns new OEmbed::Provider for JSON provider' do
|
||||
subject.call('https://host.test/oembed.html')
|
||||
expect(subject.endpoint_url).to eq 'https://host.test/provider.json'
|
||||
expect(subject.format).to eq :json
|
||||
end
|
||||
end
|
||||
|
||||
context 'XML provider is discoverable while JSON provider is not' do
|
||||
before do
|
||||
stub_request(:get, 'https://host.test/oembed.html').to_return(
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
body: request_fixture('oembed_xml.html')
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns new OEmbed::Provider for XML provider' do
|
||||
subject.call('https://host.test/oembed.html')
|
||||
expect(subject.endpoint_url).to eq 'https://host.test/provider.xml'
|
||||
expect(subject.format).to eq :xml
|
||||
end
|
||||
end
|
||||
|
||||
context 'Invalid XML provider is discoverable while JSON provider is not' do
|
||||
before do
|
||||
stub_request(:get, 'https://host.test/oembed.html').to_return(
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
body: request_fixture('oembed_invalid_xml.html')
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.call('https://host.test/oembed.html')).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'Neither of JSON and XML provider is discoverable' do
|
||||
before do
|
||||
stub_request(:get, 'https://host.test/oembed.html').to_return(
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
body: request_fixture('oembed_undiscoverable.html')
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.call('https://host.test/oembed.html')).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'Empty JSON provider is discoverable' do
|
||||
before do
|
||||
stub_request(:get, 'https://host.test/oembed.html').to_return(
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
body: request_fixture('oembed_json_empty.html')
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns new OEmbed::Provider for JSON provider' do
|
||||
subject.call('https://host.test/oembed.html')
|
||||
expect(subject.endpoint_url).to eq 'https://host.test/empty_provider.json'
|
||||
expect(subject.format).to eq :json
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context 'when status code is not 200' do
|
||||
before do
|
||||
stub_request(:get, 'https://host.test/oembed.html').to_return(
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
body: request_fixture('oembed_xml.html')
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.call('https://host.test/oembed.html')).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when MIME type is not text/html' do
|
||||
before do
|
||||
stub_request(:get, 'https://host.test/oembed.html').to_return(
|
||||
status: 200,
|
||||
body: request_fixture('oembed_xml.html')
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.call('https://host.test/oembed.html')).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
89
spec/services/fetch_remote_account_service_spec.rb
Normal file
89
spec/services/fetch_remote_account_service_spec.rb
Normal file
@@ -0,0 +1,89 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe FetchRemoteAccountService, type: :service do
|
||||
let(:url) { 'https://example.com/alice' }
|
||||
let(:prefetched_body) { nil }
|
||||
let(:protocol) { :ostatus }
|
||||
subject { FetchRemoteAccountService.new.call(url, prefetched_body, protocol) }
|
||||
|
||||
let(:actor) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'https://example.com/alice',
|
||||
type: 'Person',
|
||||
preferredUsername: 'alice',
|
||||
name: 'Alice',
|
||||
summary: 'Foo bar',
|
||||
inbox: 'http://example.com/alice/inbox',
|
||||
}
|
||||
end
|
||||
|
||||
let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||
let(:xml) { File.read(Rails.root.join('spec', 'fixtures', 'xml', 'gabsocial.atom')) }
|
||||
|
||||
shared_examples 'return Account' do
|
||||
it { is_expected.to be_an Account }
|
||||
end
|
||||
|
||||
context 'protocol is :activitypub' do
|
||||
let(:prefetched_body) { Oj.dump(actor) }
|
||||
let(:protocol) { :activitypub }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
end
|
||||
|
||||
include_examples 'return Account'
|
||||
end
|
||||
|
||||
context 'protocol is :ostatus' do
|
||||
let(:prefetched_body) { xml }
|
||||
let(:protocol) { :ostatus }
|
||||
|
||||
before do
|
||||
stub_request(:get, "https://kickass.zone/.well-known/webfinger?resource=acct:localhost@kickass.zone").to_return(request_fixture('webfinger-hacker3.txt'))
|
||||
stub_request(:get, "https://kickass.zone/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt'))
|
||||
end
|
||||
|
||||
include_examples 'return Account'
|
||||
|
||||
it 'does not update account information if XML comes from an unverified domain' do
|
||||
feed_xml = <<-XML.squish
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/">
|
||||
<author>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||
<uri>http://kickass.zone/users/localhost</uri>
|
||||
<name>localhost</name>
|
||||
<poco:preferredUsername>localhost</poco:preferredUsername>
|
||||
<poco:displayName>Villain!!!</poco:displayName>
|
||||
</author>
|
||||
</feed>
|
||||
XML
|
||||
|
||||
returned_account = described_class.new.call('https://real-fake-domains.com/alice', feed_xml, :ostatus)
|
||||
expect(returned_account.display_name).to_not eq 'Villain!!!'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when prefetched_body is nil' do
|
||||
context 'protocol is :activitypub' do
|
||||
before do
|
||||
stub_request(:get, url).to_return(status: 200, body: Oj.dump(actor), headers: { 'Content-Type' => 'application/activity+json' })
|
||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||
end
|
||||
|
||||
include_examples 'return Account'
|
||||
end
|
||||
|
||||
context 'protocol is :ostatus' do
|
||||
before do
|
||||
stub_request(:get, url).to_return(status: 200, body: xml, headers: { 'Content-Type' => 'application/atom+xml' })
|
||||
stub_request(:get, "https://kickass.zone/.well-known/webfinger?resource=acct:localhost@kickass.zone").to_return(request_fixture('webfinger-hacker3.txt'))
|
||||
stub_request(:get, "https://kickass.zone/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt'))
|
||||
end
|
||||
|
||||
include_examples 'return Account'
|
||||
end
|
||||
end
|
||||
end
|
||||
87
spec/services/fetch_remote_status_service_spec.rb
Normal file
87
spec/services/fetch_remote_status_service_spec.rb
Normal file
@@ -0,0 +1,87 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe FetchRemoteStatusService, type: :service do
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:prefetched_body) { nil }
|
||||
let(:valid_domain) { Rails.configuration.x.local_domain }
|
||||
|
||||
let(:note) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: "https://#{valid_domain}/@foo/1234",
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
attributedTo: ActivityPub::TagManager.instance.uri_for(account),
|
||||
}
|
||||
end
|
||||
|
||||
context 'protocol is :activitypub' do
|
||||
subject { described_class.new.call(note[:id], prefetched_body, protocol) }
|
||||
let(:prefetched_body) { Oj.dump(note) }
|
||||
let(:protocol) { :activitypub }
|
||||
|
||||
before do
|
||||
account.update(uri: ActivityPub::TagManager.instance.uri_for(account))
|
||||
subject
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = account.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.text).to eq 'Lorem ipsum'
|
||||
end
|
||||
end
|
||||
|
||||
context 'protocol is :ostatus' do
|
||||
subject { described_class.new }
|
||||
|
||||
before do
|
||||
Fabricate(:account, username: 'tracer', domain: 'real.domain', remote_url: 'https://real.domain/users/tracer')
|
||||
end
|
||||
|
||||
it 'does not create status with author at different domain' do
|
||||
status_body = <<-XML.squish
|
||||
<?xml version="1.0"?>
|
||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:gabsocial="http://gab.com/schema/1.0">
|
||||
<id>tag:real.domain,2017-04-27:objectId=4487555:objectType=Status</id>
|
||||
<published>2017-04-27T13:49:25Z</published>
|
||||
<updated>2017-04-27T13:49:25Z</updated>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
|
||||
<author>
|
||||
<id>https://real.domain/users/tracer</id>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||
<uri>https://real.domain/users/tracer</uri>
|
||||
<name>tracer</name>
|
||||
</author>
|
||||
<content type="html">Overwatch rocks</content>
|
||||
</entry>
|
||||
XML
|
||||
|
||||
expect(subject.call('https://fake.domain/foo', status_body, :ostatus)).to be_nil
|
||||
end
|
||||
|
||||
it 'does not create status with wrong id when id uses http format' do
|
||||
status_body = <<-XML.squish
|
||||
<?xml version="1.0"?>
|
||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:gabsocial="http://gab.com/schema/1.0">
|
||||
<id>https://other-real.domain/statuses/123</id>
|
||||
<published>2017-04-27T13:49:25Z</published>
|
||||
<updated>2017-04-27T13:49:25Z</updated>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
|
||||
<author>
|
||||
<id>https://real.domain/users/tracer</id>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||
<uri>https://real.domain/users/tracer</uri>
|
||||
<name>tracer</name>
|
||||
</author>
|
||||
<content type="html">Overwatch rocks</content>
|
||||
</entry>
|
||||
XML
|
||||
|
||||
expect(subject.call('https://real.domain/statuses/456', status_body, :ostatus)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
183
spec/services/follow_service_spec.rb
Normal file
183
spec/services/follow_service_spec.rb
Normal file
@@ -0,0 +1,183 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe FollowService, type: :service do
|
||||
let(:sender) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
subject { FollowService.new }
|
||||
|
||||
context 'local account' do
|
||||
describe 'locked account' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob')).account }
|
||||
|
||||
before do
|
||||
subject.call(sender, bob.acct)
|
||||
end
|
||||
|
||||
it 'creates a follow request with reblogs' do
|
||||
expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: true)).to_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'locked account, no reblogs' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob')).account }
|
||||
|
||||
before do
|
||||
subject.call(sender, bob.acct, reblogs: false)
|
||||
end
|
||||
|
||||
it 'creates a follow request without reblogs' do
|
||||
expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: false)).to_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'unlocked account' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||
|
||||
before do
|
||||
subject.call(sender, bob.acct)
|
||||
end
|
||||
|
||||
it 'creates a following relation with reblogs' do
|
||||
expect(sender.following?(bob)).to be true
|
||||
expect(sender.muting_reblogs?(bob)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'unlocked account, no reblogs' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||
|
||||
before do
|
||||
subject.call(sender, bob.acct, reblogs: false)
|
||||
end
|
||||
|
||||
it 'creates a following relation without reblogs' do
|
||||
expect(sender.following?(bob)).to be true
|
||||
expect(sender.muting_reblogs?(bob)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'already followed account' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||
|
||||
before do
|
||||
sender.follow!(bob)
|
||||
subject.call(sender, bob.acct)
|
||||
end
|
||||
|
||||
it 'keeps a following relation' do
|
||||
expect(sender.following?(bob)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'already followed account, turning reblogs off' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||
|
||||
before do
|
||||
sender.follow!(bob, reblogs: true)
|
||||
subject.call(sender, bob.acct, reblogs: false)
|
||||
end
|
||||
|
||||
it 'disables reblogs' do
|
||||
expect(sender.muting_reblogs?(bob)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'already followed account, turning reblogs on' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||
|
||||
before do
|
||||
sender.follow!(bob, reblogs: false)
|
||||
subject.call(sender, bob.acct, reblogs: true)
|
||||
end
|
||||
|
||||
it 'disables reblogs' do
|
||||
expect(sender.muting_reblogs?(bob)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'remote OStatus account' do
|
||||
describe 'locked account' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, locked: true, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
|
||||
|
||||
before do
|
||||
stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
|
||||
subject.call(sender, bob.acct)
|
||||
end
|
||||
|
||||
it 'creates a follow request' do
|
||||
expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil
|
||||
end
|
||||
|
||||
it 'sends a follow request salmon slap' do
|
||||
expect(a_request(:post, "http://salmon.example.com/").with { |req|
|
||||
xml = OStatus2::Salmon.new.unpack(req.body)
|
||||
xml.match(OStatus::TagManager::VERBS[:request_friend])
|
||||
}).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
describe 'unlocked account' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account }
|
||||
|
||||
before do
|
||||
stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
|
||||
stub_request(:post, "http://hub.example.com/").to_return(status: 202)
|
||||
subject.call(sender, bob.acct)
|
||||
end
|
||||
|
||||
it 'creates a following relation' do
|
||||
expect(sender.following?(bob)).to be true
|
||||
end
|
||||
|
||||
it 'sends a follow salmon slap' do
|
||||
expect(a_request(:post, "http://salmon.example.com/").with { |req|
|
||||
xml = OStatus2::Salmon.new.unpack(req.body)
|
||||
xml.match(OStatus::TagManager::VERBS[:follow])
|
||||
}).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'subscribes to PuSH' do
|
||||
expect(a_request(:post, "http://hub.example.com/")).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
describe 'already followed account' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account }
|
||||
|
||||
before do
|
||||
sender.follow!(bob)
|
||||
subject.call(sender, bob.acct)
|
||||
end
|
||||
|
||||
it 'keeps a following relation' do
|
||||
expect(sender.following?(bob)).to be true
|
||||
end
|
||||
|
||||
it 'does not send a follow salmon slap' do
|
||||
expect(a_request(:post, "http://salmon.example.com/")).not_to have_been_made
|
||||
end
|
||||
|
||||
it 'does not subscribe to PuSH' do
|
||||
expect(a_request(:post, "http://hub.example.com/")).not_to have_been_made
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'remote ActivityPub account' do
|
||||
let(:bob) { Fabricate(:user, account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account }
|
||||
|
||||
before do
|
||||
stub_request(:post, "http://example.com/inbox").to_return(:status => 200, :body => "", :headers => {})
|
||||
subject.call(sender, bob.acct)
|
||||
end
|
||||
|
||||
it 'creates follow request' do
|
||||
expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil
|
||||
end
|
||||
|
||||
it 'sends a follow activity to the inbox' do
|
||||
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
|
||||
end
|
||||
end
|
||||
end
|
||||
60
spec/services/hashtag_query_service_spec.rb
Normal file
60
spec/services/hashtag_query_service_spec.rb
Normal file
@@ -0,0 +1,60 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe HashtagQueryService, type: :service do
|
||||
describe '.call' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:tag1) { Fabricate(:tag) }
|
||||
let(:tag2) { Fabricate(:tag) }
|
||||
let!(:status1) { Fabricate(:status, tags: [tag1]) }
|
||||
let!(:status2) { Fabricate(:status, tags: [tag2]) }
|
||||
let!(:both) { Fabricate(:status, tags: [tag1, tag2]) }
|
||||
|
||||
it 'can add tags in "any" mode' do
|
||||
results = subject.call(tag1, { any: [tag2.name] })
|
||||
expect(results).to include status1
|
||||
expect(results).to include status2
|
||||
expect(results).to include both
|
||||
end
|
||||
|
||||
it 'can remove tags in "all" mode' do
|
||||
results = subject.call(tag1, { all: [tag2.name] })
|
||||
expect(results).to_not include status1
|
||||
expect(results).to_not include status2
|
||||
expect(results).to include both
|
||||
end
|
||||
|
||||
it 'can remove tags in "none" mode' do
|
||||
results = subject.call(tag1, { none: [tag2.name] })
|
||||
expect(results).to include status1
|
||||
expect(results).to_not include status2
|
||||
expect(results).to_not include both
|
||||
end
|
||||
|
||||
it 'ignores an invalid mode' do
|
||||
results = subject.call(tag1, { wark: [tag2.name] })
|
||||
expect(results).to include status1
|
||||
expect(results).to_not include status2
|
||||
expect(results).to include both
|
||||
end
|
||||
|
||||
it 'handles being passed non existant tag names' do
|
||||
results = subject.call(tag1, { any: ['wark'] })
|
||||
expect(results).to include status1
|
||||
expect(results).to_not include status2
|
||||
expect(results).to include both
|
||||
end
|
||||
|
||||
it 'can restrict to an account' do
|
||||
BlockService.new.call(account, status1.account)
|
||||
results = subject.call(tag1, { none: [tag2.name] }, account)
|
||||
expect(results).to_not include status1
|
||||
end
|
||||
|
||||
it 'can restrict to local' do
|
||||
status1.account.update(domain: 'example.com')
|
||||
status1.update(local: false, uri: 'example.com/toot')
|
||||
results = subject.call(tag1, { any: [tag2.name] }, nil, true)
|
||||
expect(results).to_not include status1
|
||||
end
|
||||
end
|
||||
end
|
||||
169
spec/services/import_service_spec.rb
Normal file
169
spec/services/import_service_spec.rb
Normal file
@@ -0,0 +1,169 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ImportService, type: :service do
|
||||
let!(:account) { Fabricate(:account, locked: false) }
|
||||
let!(:bob) { Fabricate(:account, username: 'bob', locked: false) }
|
||||
let!(:eve) { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false) }
|
||||
|
||||
context 'import old-style list of muted users' do
|
||||
subject { ImportService.new }
|
||||
|
||||
let(:csv) { attachment_fixture('mute-imports.txt') }
|
||||
|
||||
describe 'when no accounts are muted' do
|
||||
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
|
||||
it 'mutes the listed accounts, including notifications' do
|
||||
subject.call(import)
|
||||
expect(account.muting.count).to eq 2
|
||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are muted and overwrite is not set' do
|
||||
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
|
||||
|
||||
it 'mutes the listed accounts, including notifications' do
|
||||
account.mute!(bob, notifications: false)
|
||||
subject.call(import)
|
||||
expect(account.muting.count).to eq 2
|
||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are muted and overwrite is set' do
|
||||
let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) }
|
||||
|
||||
it 'mutes the listed accounts, including notifications' do
|
||||
account.mute!(bob, notifications: false)
|
||||
subject.call(import)
|
||||
expect(account.muting.count).to eq 2
|
||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'import new-style list of muted users' do
|
||||
subject { ImportService.new }
|
||||
|
||||
let(:csv) { attachment_fixture('new-mute-imports.txt') }
|
||||
|
||||
describe 'when no accounts are muted' do
|
||||
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
|
||||
it 'mutes the listed accounts, respecting notifications' do
|
||||
subject.call(import)
|
||||
expect(account.muting.count).to eq 2
|
||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
||||
expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are muted and overwrite is not set' do
|
||||
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
|
||||
|
||||
it 'mutes the listed accounts, respecting notifications' do
|
||||
account.mute!(bob, notifications: true)
|
||||
subject.call(import)
|
||||
expect(account.muting.count).to eq 2
|
||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
||||
expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are muted and overwrite is set' do
|
||||
let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) }
|
||||
|
||||
it 'mutes the listed accounts, respecting notifications' do
|
||||
account.mute!(bob, notifications: true)
|
||||
subject.call(import)
|
||||
expect(account.muting.count).to eq 2
|
||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
||||
expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'import old-style list of followed users' do
|
||||
subject { ImportService.new }
|
||||
|
||||
let(:csv) { attachment_fixture('mute-imports.txt') }
|
||||
|
||||
before do
|
||||
allow(NotificationWorker).to receive(:perform_async)
|
||||
end
|
||||
|
||||
describe 'when no accounts are followed' do
|
||||
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
||||
it 'follows the listed accounts, including reposts' do
|
||||
subject.call(import)
|
||||
expect(account.following.count).to eq 2
|
||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are already followed and overwrite is not set' do
|
||||
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
||||
|
||||
it 'follows the listed accounts, including notifications' do
|
||||
account.follow!(bob, reblogs: false)
|
||||
subject.call(import)
|
||||
expect(account.following.count).to eq 2
|
||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are already followed and overwrite is set' do
|
||||
let(:import) { Import.create(account: account, type: 'following', data: csv, overwrite: true) }
|
||||
|
||||
it 'mutes the listed accounts, including notifications' do
|
||||
account.follow!(bob, reblogs: false)
|
||||
subject.call(import)
|
||||
expect(account.following.count).to eq 2
|
||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'import new-style list of followed users' do
|
||||
subject { ImportService.new }
|
||||
|
||||
let(:csv) { attachment_fixture('new-following-imports.txt') }
|
||||
|
||||
before do
|
||||
allow(NotificationWorker).to receive(:perform_async)
|
||||
end
|
||||
|
||||
describe 'when no accounts are followed' do
|
||||
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
||||
it 'follows the listed accounts, respecting reposts' do
|
||||
subject.call(import)
|
||||
expect(account.following.count).to eq 2
|
||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
||||
expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are already followed and overwrite is not set' do
|
||||
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
||||
|
||||
it 'mutes the listed accounts, respecting notifications' do
|
||||
account.follow!(bob, reblogs: true)
|
||||
subject.call(import)
|
||||
expect(account.following.count).to eq 2
|
||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
||||
expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when some accounts are already followed and overwrite is set' do
|
||||
let(:import) { Import.create(account: account, type: 'following', data: csv, overwrite: true) }
|
||||
|
||||
it 'mutes the listed accounts, respecting notifications' do
|
||||
account.follow!(bob, reblogs: true)
|
||||
subject.call(import)
|
||||
expect(account.following.count).to eq 2
|
||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
||||
expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
67
spec/services/mute_service_spec.rb
Normal file
67
spec/services/mute_service_spec.rb
Normal file
@@ -0,0 +1,67 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe MuteService, type: :service do
|
||||
subject do
|
||||
-> { described_class.new.call(account, target_account) }
|
||||
end
|
||||
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:target_account) { Fabricate(:account) }
|
||||
|
||||
describe 'home timeline' do
|
||||
let(:status) { Fabricate(:status, account: target_account) }
|
||||
let(:other_account_status) { Fabricate(:status) }
|
||||
let(:home_timeline_key) { FeedManager.instance.key(:home, account.id) }
|
||||
|
||||
before do
|
||||
Redis.current.del(home_timeline_key)
|
||||
end
|
||||
|
||||
it "clears account's statuses" do
|
||||
FeedManager.instance.push_to_home(account, status)
|
||||
FeedManager.instance.push_to_home(account, other_account_status)
|
||||
|
||||
is_expected.to change {
|
||||
Redis.current.zrange(home_timeline_key, 0, -1)
|
||||
}.from([status.id.to_s, other_account_status.id.to_s]).to([other_account_status.id.to_s])
|
||||
end
|
||||
end
|
||||
|
||||
it 'mutes account' do
|
||||
is_expected.to change {
|
||||
account.muting?(target_account)
|
||||
}.from(false).to(true)
|
||||
end
|
||||
|
||||
context 'without specifying a notifications parameter' do
|
||||
it 'mutes notifications from the account' do
|
||||
is_expected.to change {
|
||||
account.muting_notifications?(target_account)
|
||||
}.from(false).to(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a true notifications parameter' do
|
||||
subject do
|
||||
-> { described_class.new.call(account, target_account, notifications: true) }
|
||||
end
|
||||
|
||||
it 'mutes notifications from the account' do
|
||||
is_expected.to change {
|
||||
account.muting_notifications?(target_account)
|
||||
}.from(false).to(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a false notifications parameter' do
|
||||
subject do
|
||||
-> { described_class.new.call(account, target_account, notifications: false) }
|
||||
end
|
||||
|
||||
it 'does not mute notifications from the account' do
|
||||
is_expected.to_not change {
|
||||
account.muting_notifications?(target_account)
|
||||
}.from(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
161
spec/services/notify_service_spec.rb
Normal file
161
spec/services/notify_service_spec.rb
Normal file
@@ -0,0 +1,161 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe NotifyService, type: :service do
|
||||
subject do
|
||||
-> { described_class.new.call(recipient, activity) }
|
||||
end
|
||||
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:recipient) { user.account }
|
||||
let(:sender) { Fabricate(:account, domain: 'example.com') }
|
||||
let(:activity) { Fabricate(:follow, account: sender, target_account: recipient) }
|
||||
|
||||
it { is_expected.to change(Notification, :count).by(1) }
|
||||
|
||||
it 'does not notify when sender is blocked' do
|
||||
recipient.block!(sender)
|
||||
is_expected.to_not change(Notification, :count)
|
||||
end
|
||||
|
||||
it 'does not notify when sender is muted with hide_notifications' do
|
||||
recipient.mute!(sender, notifications: true)
|
||||
is_expected.to_not change(Notification, :count)
|
||||
end
|
||||
|
||||
it 'does notify when sender is muted without hide_notifications' do
|
||||
recipient.mute!(sender, notifications: false)
|
||||
is_expected.to change(Notification, :count)
|
||||
end
|
||||
|
||||
it 'does not notify when sender\'s domain is blocked' do
|
||||
recipient.block_domain!(sender.domain)
|
||||
is_expected.to_not change(Notification, :count)
|
||||
end
|
||||
|
||||
it 'does still notify when sender\'s domain is blocked but sender is followed' do
|
||||
recipient.block_domain!(sender.domain)
|
||||
recipient.follow!(sender)
|
||||
is_expected.to change(Notification, :count)
|
||||
end
|
||||
|
||||
it 'does not notify when sender is silenced and not followed' do
|
||||
sender.silence!
|
||||
is_expected.to_not change(Notification, :count)
|
||||
end
|
||||
|
||||
it 'does not notify when recipient is suspended' do
|
||||
recipient.suspend!
|
||||
is_expected.to_not change(Notification, :count)
|
||||
end
|
||||
|
||||
context 'for direct messages' do
|
||||
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct)) }
|
||||
|
||||
before do
|
||||
user.settings.interactions = user.settings.interactions.merge('must_be_following_dm' => enabled)
|
||||
end
|
||||
|
||||
context 'if recipient is supposed to be following sender' do
|
||||
let(:enabled) { true }
|
||||
|
||||
it 'does not notify' do
|
||||
is_expected.to_not change(Notification, :count)
|
||||
end
|
||||
|
||||
context 'if the message chain initiated by recipient, but is not direct message' do
|
||||
let(:reply_to) { Fabricate(:status, account: recipient) }
|
||||
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }
|
||||
|
||||
it 'does not notify' do
|
||||
is_expected.to_not change(Notification, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'if the message chain initiated by recipient and is direct message' do
|
||||
let(:reply_to) { Fabricate(:status, account: recipient, visibility: :direct) }
|
||||
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }
|
||||
|
||||
it 'does notify' do
|
||||
is_expected.to change(Notification, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'if recipient is NOT supposed to be following sender' do
|
||||
let(:enabled) { false }
|
||||
|
||||
it 'does notify' do
|
||||
is_expected.to change(Notification, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'reblogs' do
|
||||
let(:status) { Fabricate(:status, account: Fabricate(:account)) }
|
||||
let(:activity) { Fabricate(:status, account: sender, reblog: status) }
|
||||
|
||||
it 'shows reblogs by default' do
|
||||
recipient.follow!(sender)
|
||||
is_expected.to change(Notification, :count)
|
||||
end
|
||||
|
||||
it 'shows reblogs when explicitly enabled' do
|
||||
recipient.follow!(sender, reblogs: true)
|
||||
is_expected.to change(Notification, :count)
|
||||
end
|
||||
|
||||
it 'shows reblogs when disabled' do
|
||||
recipient.follow!(sender, reblogs: false)
|
||||
is_expected.to change(Notification, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
let(:asshole) { Fabricate(:account, username: 'asshole') }
|
||||
let(:reply_to) { Fabricate(:status, account: asshole) }
|
||||
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, thread: reply_to)) }
|
||||
|
||||
it 'does not notify when conversation is muted' do
|
||||
recipient.mute_conversation!(activity.status.conversation)
|
||||
is_expected.to_not change(Notification, :count)
|
||||
end
|
||||
|
||||
it 'does not notify when it is a reply to a blocked user' do
|
||||
recipient.block!(asshole)
|
||||
is_expected.to_not change(Notification, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context do
|
||||
let(:sender) { recipient }
|
||||
|
||||
it 'does not notify when recipient is the sender' do
|
||||
is_expected.to_not change(Notification, :count)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'email' do
|
||||
before do
|
||||
ActionMailer::Base.deliveries.clear
|
||||
|
||||
notification_emails = user.settings.notification_emails
|
||||
user.settings.notification_emails = notification_emails.merge('follow' => enabled)
|
||||
end
|
||||
|
||||
context 'when email notification is enabled' do
|
||||
let(:enabled) { true }
|
||||
|
||||
it 'sends email' do
|
||||
is_expected.to change(ActionMailer::Base.deliveries, :count).by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when email notification is disabled' do
|
||||
let(:enabled) { false }
|
||||
|
||||
it "doesn't send email" do
|
||||
is_expected.to_not change(ActionMailer::Base.deliveries, :count).from(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
243
spec/services/post_status_service_spec.rb
Normal file
243
spec/services/post_status_service_spec.rb
Normal file
@@ -0,0 +1,243 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PostStatusService, type: :service do
|
||||
subject { PostStatusService.new }
|
||||
|
||||
it 'creates a new status' do
|
||||
account = Fabricate(:account)
|
||||
text = "test status update"
|
||||
|
||||
status = subject.call(account, text: text)
|
||||
|
||||
expect(status).to be_persisted
|
||||
expect(status.text).to eq text
|
||||
end
|
||||
|
||||
it 'creates a new response status' do
|
||||
in_reply_to_status = Fabricate(:status)
|
||||
account = Fabricate(:account)
|
||||
text = "test status update"
|
||||
|
||||
status = subject.call(account, text: text, thread: in_reply_to_status)
|
||||
|
||||
expect(status).to be_persisted
|
||||
expect(status.text).to eq text
|
||||
expect(status.thread).to eq in_reply_to_status
|
||||
end
|
||||
|
||||
it 'schedules a status' do
|
||||
account = Fabricate(:account)
|
||||
future = Time.now.utc + 2.hours
|
||||
|
||||
status = subject.call(account, text: 'Hi future!', scheduled_at: future)
|
||||
|
||||
expect(status).to be_a ScheduledStatus
|
||||
expect(status.scheduled_at).to eq future
|
||||
expect(status.params['text']).to eq 'Hi future!'
|
||||
end
|
||||
|
||||
it 'does not immediately create a status when scheduling a status' do
|
||||
account = Fabricate(:account)
|
||||
media = Fabricate(:media_attachment)
|
||||
future = Time.now.utc + 2.hours
|
||||
|
||||
status = subject.call(account, text: 'Hi future!', media_ids: [media.id], scheduled_at: future)
|
||||
|
||||
expect(status).to be_a ScheduledStatus
|
||||
expect(status.scheduled_at).to eq future
|
||||
expect(status.params['text']).to eq 'Hi future!'
|
||||
expect(media.reload.status).to be_nil
|
||||
expect(Status.where(text: 'Hi future!').exists?).to be_falsey
|
||||
end
|
||||
|
||||
it 'creates response to the original status of repost' do
|
||||
reposted_status = Fabricate(:status)
|
||||
in_reply_to_status = Fabricate(:status, reblog: reposted_status)
|
||||
account = Fabricate(:account)
|
||||
text = "test status update"
|
||||
|
||||
status = subject.call(account, text: text, thread: in_reply_to_status)
|
||||
|
||||
expect(status).to be_persisted
|
||||
expect(status.text).to eq text
|
||||
expect(status.thread).to eq reposted_status
|
||||
end
|
||||
|
||||
it 'creates a sensitive status' do
|
||||
status = create_status_with_options(sensitive: true)
|
||||
|
||||
expect(status).to be_persisted
|
||||
expect(status).to be_sensitive
|
||||
end
|
||||
|
||||
it 'creates a status with spoiler text' do
|
||||
spoiler_text = "spoiler text"
|
||||
|
||||
status = create_status_with_options(spoiler_text: spoiler_text)
|
||||
|
||||
expect(status).to be_persisted
|
||||
expect(status.spoiler_text).to eq spoiler_text
|
||||
end
|
||||
|
||||
it 'creates a status with empty default spoiler text' do
|
||||
status = create_status_with_options(spoiler_text: nil)
|
||||
|
||||
expect(status).to be_persisted
|
||||
expect(status.spoiler_text).to eq ''
|
||||
end
|
||||
|
||||
it 'creates a status with the given visibility' do
|
||||
status = create_status_with_options(visibility: :private)
|
||||
|
||||
expect(status).to be_persisted
|
||||
expect(status.visibility).to eq "private"
|
||||
end
|
||||
|
||||
it 'creates a status with limited visibility for silenced users' do
|
||||
status = subject.call(Fabricate(:account, silenced: true), text: 'test', visibility: :public)
|
||||
|
||||
expect(status).to be_persisted
|
||||
expect(status.visibility).to eq "unlisted"
|
||||
end
|
||||
|
||||
it 'creates a status for the given application' do
|
||||
application = Fabricate(:application)
|
||||
|
||||
status = create_status_with_options(application: application)
|
||||
|
||||
expect(status).to be_persisted
|
||||
expect(status.application).to eq application
|
||||
end
|
||||
|
||||
it 'creates a status with a language set' do
|
||||
account = Fabricate(:account)
|
||||
text = 'This is an English text.'
|
||||
|
||||
status = subject.call(account, text: text)
|
||||
|
||||
expect(status.language).to eq 'en'
|
||||
end
|
||||
|
||||
it 'processes mentions' do
|
||||
mention_service = double(:process_mentions_service)
|
||||
allow(mention_service).to receive(:call)
|
||||
allow(ProcessMentionsService).to receive(:new).and_return(mention_service)
|
||||
account = Fabricate(:account)
|
||||
|
||||
status = subject.call(account, text: "test status update")
|
||||
|
||||
expect(ProcessMentionsService).to have_received(:new)
|
||||
expect(mention_service).to have_received(:call).with(status)
|
||||
end
|
||||
|
||||
it 'processes hashtags' do
|
||||
hashtags_service = double(:process_hashtags_service)
|
||||
allow(hashtags_service).to receive(:call)
|
||||
allow(ProcessHashtagsService).to receive(:new).and_return(hashtags_service)
|
||||
account = Fabricate(:account)
|
||||
|
||||
status = subject.call(account, text: "test status update")
|
||||
|
||||
expect(ProcessHashtagsService).to have_received(:new)
|
||||
expect(hashtags_service).to have_received(:call).with(status)
|
||||
end
|
||||
|
||||
it 'gets distributed' do
|
||||
allow(DistributionWorker).to receive(:perform_async)
|
||||
allow(Pubsubhubbub::DistributionWorker).to receive(:perform_async)
|
||||
allow(ActivityPub::DistributionWorker).to receive(:perform_async)
|
||||
|
||||
account = Fabricate(:account)
|
||||
|
||||
status = subject.call(account, text: "test status update")
|
||||
|
||||
expect(DistributionWorker).to have_received(:perform_async).with(status.id)
|
||||
expect(Pubsubhubbub::DistributionWorker).to have_received(:perform_async).with(status.stream_entry.id)
|
||||
expect(ActivityPub::DistributionWorker).to have_received(:perform_async).with(status.id)
|
||||
end
|
||||
|
||||
it 'crawls links' do
|
||||
allow(LinkCrawlWorker).to receive(:perform_async)
|
||||
account = Fabricate(:account)
|
||||
|
||||
status = subject.call(account, text: "test status update")
|
||||
|
||||
expect(LinkCrawlWorker).to have_received(:perform_async).with(status.id)
|
||||
end
|
||||
|
||||
it 'attaches the given media to the created status' do
|
||||
account = Fabricate(:account)
|
||||
media = Fabricate(:media_attachment, account: account)
|
||||
|
||||
status = subject.call(
|
||||
account,
|
||||
text: "test status update",
|
||||
media_ids: [media.id],
|
||||
)
|
||||
|
||||
expect(media.reload.status).to eq status
|
||||
end
|
||||
|
||||
it 'does not attach media from another account to the created status' do
|
||||
account = Fabricate(:account)
|
||||
media = Fabricate(:media_attachment, account: Fabricate(:account))
|
||||
|
||||
status = subject.call(
|
||||
account,
|
||||
text: "test status update",
|
||||
media_ids: [media.id],
|
||||
)
|
||||
|
||||
expect(media.reload.status).to eq nil
|
||||
end
|
||||
|
||||
it 'does not allow attaching more than 4 files' do
|
||||
account = Fabricate(:account)
|
||||
|
||||
expect do
|
||||
subject.call(
|
||||
account,
|
||||
text: "test status update",
|
||||
media_ids: [
|
||||
Fabricate(:media_attachment, account: account),
|
||||
Fabricate(:media_attachment, account: account),
|
||||
Fabricate(:media_attachment, account: account),
|
||||
Fabricate(:media_attachment, account: account),
|
||||
Fabricate(:media_attachment, account: account),
|
||||
].map(&:id),
|
||||
)
|
||||
end.to raise_error(
|
||||
GabSocial::ValidationError,
|
||||
I18n.t('media_attachments.validations.too_many'),
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not allow attaching both videos and images' do
|
||||
account = Fabricate(:account)
|
||||
|
||||
expect do
|
||||
subject.call(
|
||||
account,
|
||||
text: "test status update",
|
||||
media_ids: [
|
||||
Fabricate(:media_attachment, type: :video, account: account),
|
||||
Fabricate(:media_attachment, type: :image, account: account),
|
||||
].map(&:id),
|
||||
)
|
||||
end.to raise_error(
|
||||
GabSocial::ValidationError,
|
||||
I18n.t('media_attachments.validations.images_and_video'),
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns existing status when used twice with idempotency key' do
|
||||
account = Fabricate(:account)
|
||||
status1 = subject.call(account, text: 'test', idempotency: 'meepmeep')
|
||||
status2 = subject.call(account, text: 'test', idempotency: 'meepmeep')
|
||||
expect(status2.id).to eq status1.id
|
||||
end
|
||||
|
||||
def create_status_with_options(**options)
|
||||
subject.call(Fabricate(:account), options.merge(text: 'test'))
|
||||
end
|
||||
end
|
||||
36
spec/services/precompute_feed_service_spec.rb
Normal file
36
spec/services/precompute_feed_service_spec.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PrecomputeFeedService, type: :service do
|
||||
subject { PrecomputeFeedService.new }
|
||||
|
||||
describe 'call' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
it 'fills a user timeline with statuses' do
|
||||
account = Fabricate(:account)
|
||||
status = Fabricate(:status, account: account)
|
||||
|
||||
subject.call(account)
|
||||
|
||||
expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), status.id)).to be_within(0.1).of(status.id.to_f)
|
||||
end
|
||||
|
||||
it 'does not raise an error even if it could not find any status' do
|
||||
account = Fabricate(:account)
|
||||
subject.call(account)
|
||||
end
|
||||
|
||||
it 'filters statuses' do
|
||||
account = Fabricate(:account)
|
||||
muted_account = Fabricate(:account)
|
||||
Fabricate(:mute, account: account, target_account: muted_account)
|
||||
reblog = Fabricate(:status, account: muted_account)
|
||||
status = Fabricate(:status, account: account, reblog: reblog)
|
||||
|
||||
subject.call(account)
|
||||
|
||||
expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to eq nil
|
||||
end
|
||||
end
|
||||
end
|
||||
252
spec/services/process_feed_service_spec.rb
Normal file
252
spec/services/process_feed_service_spec.rb
Normal file
@@ -0,0 +1,252 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ProcessFeedService, type: :service do
|
||||
subject { ProcessFeedService.new }
|
||||
|
||||
describe 'processing a feed' do
|
||||
let(:body) { File.read(Rails.root.join('spec', 'fixtures', 'xml', 'gabsocial.atom')) }
|
||||
let(:account) { Fabricate(:account, username: 'localhost', domain: 'kickass.zone') }
|
||||
|
||||
before do
|
||||
stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {})
|
||||
stub_request(:head, "http://kickass.zone/media/2").to_return(:status => 404)
|
||||
stub_request(:head, "http://kickass.zone/media/3").to_return(:status => 404)
|
||||
stub_request(:get, "http://kickass.zone/system/accounts/avatars/000/000/001/large/eris.png").to_return(request_fixture('avatar.txt'))
|
||||
stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/002/original/morpheus_linux.jpg?1476059910").to_return(request_fixture('attachment1.txt'))
|
||||
stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/003/original/gizmo.jpg?1476060065").to_return(request_fixture('attachment2.txt'))
|
||||
end
|
||||
|
||||
context 'when domain does not reject media' do
|
||||
before do
|
||||
subject.call(body, account)
|
||||
end
|
||||
|
||||
it 'updates remote user\'s account information' do
|
||||
account.reload
|
||||
expect(account.display_name).to eq '::1'
|
||||
expect(account).to have_attached_file(:avatar)
|
||||
expect(account.avatar_file_name).not_to be_nil
|
||||
end
|
||||
|
||||
it 'creates posts' do
|
||||
expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Status')).to_not be_nil
|
||||
expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')).to_not be_nil
|
||||
end
|
||||
|
||||
it 'marks replies as replies' do
|
||||
status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')
|
||||
expect(status.reply?).to be true
|
||||
end
|
||||
|
||||
it 'sets account being replied to when possible' do
|
||||
status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')
|
||||
expect(status.in_reply_to_account_id).to eq status.account_id
|
||||
end
|
||||
|
||||
it 'ignores delete statuses unless they existed before' do
|
||||
expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=3:objectType=Status')).to be_nil
|
||||
expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=12:objectType=Status')).to be_nil
|
||||
end
|
||||
|
||||
it 'does not create statuses for follows' do
|
||||
expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Follow')).to be_nil
|
||||
expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Follow')).to be_nil
|
||||
expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=4:objectType=Follow')).to be_nil
|
||||
expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=7:objectType=Follow')).to be_nil
|
||||
end
|
||||
|
||||
it 'does not create statuses for favourites' do
|
||||
expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Favourite')).to be_nil
|
||||
expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=3:objectType=Favourite')).to be_nil
|
||||
end
|
||||
|
||||
it 'creates posts with media' do
|
||||
status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=14:objectType=Status')
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.media_attachments.first).to have_attached_file(:file)
|
||||
expect(status.media_attachments.first.image?).to be true
|
||||
expect(status.media_attachments.first.file_file_name).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when domain is set to reject media' do
|
||||
let!(:domain_block) { Fabricate(:domain_block, domain: 'kickass.zone', reject_media: true) }
|
||||
|
||||
before do
|
||||
subject.call(body, account)
|
||||
end
|
||||
|
||||
it 'updates remote user\'s account information' do
|
||||
account.reload
|
||||
expect(account.display_name).to eq '::1'
|
||||
end
|
||||
|
||||
it 'rejects remote user\'s avatar' do
|
||||
account.reload
|
||||
expect(account.display_name).to eq '::1'
|
||||
expect(account.avatar_file_name).to be_nil
|
||||
end
|
||||
|
||||
it 'creates posts' do
|
||||
expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Status')).to_not be_nil
|
||||
expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')).to_not be_nil
|
||||
end
|
||||
|
||||
it 'creates posts with remote-only media' do
|
||||
status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=14:objectType=Status')
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.media_attachments.first.file_file_name).to be_nil
|
||||
expect(status.media_attachments.first.unknown?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not accept tampered reblogs' do
|
||||
good_actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com')
|
||||
|
||||
real_body = <<XML
|
||||
<?xml version="1.0"?>
|
||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:gabsocial="http://gab.com/schema/1.0">
|
||||
<id>tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status</id>
|
||||
<published>2017-04-27T13:49:25Z</published>
|
||||
<updated>2017-04-27T13:49:25Z</updated>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
|
||||
<author>
|
||||
<id>https://overwatch.com/users/tracer</id>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||
<uri>https://overwatch.com/users/tracer</uri>
|
||||
<name>tracer</name>
|
||||
</author>
|
||||
<content type="html">Overwatch rocks</content>
|
||||
</entry>
|
||||
XML
|
||||
|
||||
stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 200, body: real_body, headers: { 'Content-Type' => 'application/atom+xml' })
|
||||
|
||||
bad_actor = Fabricate(:account, username: 'sombra', domain: 'talon.xyz')
|
||||
|
||||
body = <<XML
|
||||
<?xml version="1.0"?>
|
||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:gabsocial="http://gab.com/schema/1.0">
|
||||
<id>tag:talon.xyz,2017-04-27:objectId=4467137:objectType=Status</id>
|
||||
<published>2017-04-27T13:49:25Z</published>
|
||||
<updated>2017-04-27T13:49:25Z</updated>
|
||||
<author>
|
||||
<id>https://talon.xyz/users/sombra</id>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||
<uri>https://talon.xyz/users/sombra</uri>
|
||||
<name>sombra</name>
|
||||
</author>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
|
||||
<content type="html">Overwatch SUCKS AHAHA</content>
|
||||
<activity:object>
|
||||
<id>tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status</id>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
|
||||
<author>
|
||||
<id>https://overwatch.com/users/tracer</id>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||
<uri>https://overwatch.com/users/tracer</uri>
|
||||
<name>tracer</name>
|
||||
</author>
|
||||
<content type="html">Overwatch SUCKS AHAHA</content>
|
||||
<link rel="alternate" type="text/html" href="https://overwatch.com/users/tracer/updates/1" />
|
||||
</activity:object>
|
||||
</entry>
|
||||
XML
|
||||
created_statuses = subject.call(body, bad_actor)
|
||||
|
||||
expect(created_statuses.first.reblog?).to be true
|
||||
expect(created_statuses.first.account_id).to eq bad_actor.id
|
||||
expect(created_statuses.first.reblog.account_id).to eq good_actor.id
|
||||
expect(created_statuses.first.reblog.text).to eq 'Overwatch rocks'
|
||||
end
|
||||
|
||||
it 'ignores reblogs if it failed to retrieve reblogged statuses' do
|
||||
stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 404)
|
||||
|
||||
actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com')
|
||||
|
||||
body = <<XML
|
||||
<?xml version="1.0"?>
|
||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:gabsocial="http://gab.com/schema/1.0">
|
||||
<id>tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status</id>
|
||||
<published>2017-04-27T13:49:25Z</published>
|
||||
<updated>2017-04-27T13:49:25Z</updated>
|
||||
<author>
|
||||
<id>https://overwatch.com/users/tracer</id>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||
<uri>https://overwatch.com/users/tracer</uri>
|
||||
<name>tracer</name>
|
||||
</author>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
|
||||
<content type="html">Overwatch rocks</content>
|
||||
<activity:object>
|
||||
<id>tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status</id>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
|
||||
<author>
|
||||
<id>https://overwatch.com/users/tracer</id>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||
<uri>https://overwatch.com/users/tracer</uri>
|
||||
<name>tracer</name>
|
||||
</author>
|
||||
<content type="html">Overwatch rocks</content>
|
||||
<link rel="alternate" type="text/html" href="https://overwatch.com/users/tracer/updates/1" />
|
||||
</activity:object>
|
||||
XML
|
||||
|
||||
created_statuses = subject.call(body, actor)
|
||||
|
||||
expect(created_statuses).to eq []
|
||||
end
|
||||
|
||||
it 'ignores statuses with an out-of-order delete' do
|
||||
sender = Fabricate(:account, username: 'tracer', domain: 'overwatch.com')
|
||||
|
||||
delete_body = <<XML
|
||||
<?xml version="1.0"?>
|
||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:gabsocial="http://gab.com/schema/1.0">
|
||||
<id>tag:overwatch.com,2017-04-27:objectId=4487555:objectType=Status</id>
|
||||
<published>2017-04-27T13:49:25Z</published>
|
||||
<updated>2017-04-27T13:49:25Z</updated>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/delete</activity:verb>
|
||||
<author>
|
||||
<id>https://overwatch.com/users/tracer</id>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||
<uri>https://overwatch.com/users/tracer</uri>
|
||||
<name>tracer</name>
|
||||
</author>
|
||||
</entry>
|
||||
XML
|
||||
|
||||
status_body = <<XML
|
||||
<?xml version="1.0"?>
|
||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:gabsocial="http://gab.com/schema/1.0">
|
||||
<id>tag:overwatch.com,2017-04-27:objectId=4487555:objectType=Status</id>
|
||||
<published>2017-04-27T13:49:25Z</published>
|
||||
<updated>2017-04-27T13:49:25Z</updated>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
|
||||
<author>
|
||||
<id>https://overwatch.com/users/tracer</id>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||
<uri>https://overwatch.com/users/tracer</uri>
|
||||
<name>tracer</name>
|
||||
</author>
|
||||
<content type="html">Overwatch rocks</content>
|
||||
</entry>
|
||||
XML
|
||||
|
||||
subject.call(delete_body, sender)
|
||||
created_statuses = subject.call(status_body, sender)
|
||||
|
||||
expect(created_statuses).to be_empty
|
||||
end
|
||||
end
|
||||
151
spec/services/process_interaction_service_spec.rb
Normal file
151
spec/services/process_interaction_service_spec.rb
Normal file
@@ -0,0 +1,151 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ProcessInteractionService, type: :service do
|
||||
let(:receiver) { Fabricate(:user, email: 'alice@example.com', account: Fabricate(:account, username: 'alice')).account }
|
||||
let(:sender) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||
let(:remote_sender) { Fabricate(:account, username: 'carol', domain: 'localdomain.com', uri: 'https://webdomain.com/users/carol') }
|
||||
|
||||
subject { ProcessInteractionService.new }
|
||||
|
||||
describe 'status delete slap' do
|
||||
let(:remote_status) { Fabricate(:status, account: remote_sender) }
|
||||
let(:envelope) { OStatus2::Salmon.new.pack(payload, sender.keypair) }
|
||||
let(:payload) {
|
||||
<<~XML
|
||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/">
|
||||
<author>
|
||||
<email>carol@localdomain.com</email>
|
||||
<name>carol</name>
|
||||
<uri>https://webdomain.com/users/carol</uri>
|
||||
</author>
|
||||
|
||||
<id>#{remote_status.id}</id>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/delete</activity:verb>
|
||||
</entry>
|
||||
XML
|
||||
}
|
||||
|
||||
before do
|
||||
receiver.update(locked: true)
|
||||
remote_sender.update(private_key: sender.private_key, public_key: remote_sender.public_key)
|
||||
end
|
||||
|
||||
it 'deletes a record' do
|
||||
expect(RemovalWorker).to receive(:perform_async).with(remote_status.id)
|
||||
subject.call(envelope, receiver)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'follow request slap' do
|
||||
before do
|
||||
receiver.update(locked: true)
|
||||
|
||||
payload = <<XML
|
||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/">
|
||||
<author>
|
||||
<name>bob</name>
|
||||
<uri>https://cb6e6126.ngrok.io/users/bob</uri>
|
||||
</author>
|
||||
|
||||
<id>someIdHere</id>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/request-friend</activity:verb>
|
||||
</entry>
|
||||
XML
|
||||
|
||||
envelope = OStatus2::Salmon.new.pack(payload, sender.keypair)
|
||||
subject.call(envelope, receiver)
|
||||
end
|
||||
|
||||
it 'creates a record' do
|
||||
expect(FollowRequest.find_by(account: sender, target_account: receiver)).to_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'follow request slap from known remote user identified by email' do
|
||||
before do
|
||||
receiver.update(locked: true)
|
||||
# Copy already-generated key
|
||||
remote_sender.update(private_key: sender.private_key, public_key: remote_sender.public_key)
|
||||
|
||||
payload = <<XML
|
||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/">
|
||||
<author>
|
||||
<email>carol@localdomain.com</email>
|
||||
<name>carol</name>
|
||||
<uri>https://webdomain.com/users/carol</uri>
|
||||
</author>
|
||||
|
||||
<id>someIdHere</id>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/request-friend</activity:verb>
|
||||
</entry>
|
||||
XML
|
||||
|
||||
envelope = OStatus2::Salmon.new.pack(payload, remote_sender.keypair)
|
||||
subject.call(envelope, receiver)
|
||||
end
|
||||
|
||||
it 'creates a record' do
|
||||
expect(FollowRequest.find_by(account: remote_sender, target_account: receiver)).to_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'follow request authorization slap' do
|
||||
before do
|
||||
receiver.update(locked: true)
|
||||
FollowRequest.create(account: sender, target_account: receiver)
|
||||
|
||||
payload = <<XML
|
||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/">
|
||||
<author>
|
||||
<name>alice</name>
|
||||
<uri>https://cb6e6126.ngrok.io/users/alice</uri>
|
||||
</author>
|
||||
|
||||
<id>someIdHere</id>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/authorize</activity:verb>
|
||||
</entry>
|
||||
XML
|
||||
|
||||
envelope = OStatus2::Salmon.new.pack(payload, receiver.keypair)
|
||||
subject.call(envelope, sender)
|
||||
end
|
||||
|
||||
it 'creates a follow relationship' do
|
||||
expect(Follow.find_by(account: sender, target_account: receiver)).to_not be_nil
|
||||
end
|
||||
|
||||
it 'removes the follow request' do
|
||||
expect(FollowRequest.find_by(account: sender, target_account: receiver)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'follow request rejection slap' do
|
||||
before do
|
||||
receiver.update(locked: true)
|
||||
FollowRequest.create(account: sender, target_account: receiver)
|
||||
|
||||
payload = <<XML
|
||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/">
|
||||
<author>
|
||||
<name>alice</name>
|
||||
<uri>https://cb6e6126.ngrok.io/users/alice</uri>
|
||||
</author>
|
||||
|
||||
<id>someIdHere</id>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/reject</activity:verb>
|
||||
</entry>
|
||||
XML
|
||||
|
||||
envelope = OStatus2::Salmon.new.pack(payload, receiver.keypair)
|
||||
subject.call(envelope, sender)
|
||||
end
|
||||
|
||||
it 'does not create a follow relationship' do
|
||||
expect(Follow.find_by(account: sender, target_account: receiver)).to be_nil
|
||||
end
|
||||
|
||||
it 'removes the follow request' do
|
||||
expect(FollowRequest.find_by(account: sender, target_account: receiver)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
86
spec/services/process_mentions_service_spec.rb
Normal file
86
spec/services/process_mentions_service_spec.rb
Normal file
@@ -0,0 +1,86 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ProcessMentionsService, type: :service do
|
||||
let(:account) { Fabricate(:account, username: 'alice') }
|
||||
let(:visibility) { :public }
|
||||
let(:status) { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}", visibility: visibility) }
|
||||
|
||||
context 'OStatus with public gab' do
|
||||
let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') }
|
||||
|
||||
subject { ProcessMentionsService.new }
|
||||
|
||||
before do
|
||||
stub_request(:post, remote_user.salmon_url)
|
||||
subject.call(status)
|
||||
end
|
||||
|
||||
it 'creates a mention' do
|
||||
expect(remote_user.mentions.where(status: status).count).to eq 1
|
||||
end
|
||||
|
||||
it 'posts to remote user\'s Salmon end point' do
|
||||
expect(a_request(:post, remote_user.salmon_url)).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
context 'OStatus with private gab' do
|
||||
let(:visibility) { :private }
|
||||
let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') }
|
||||
|
||||
subject { ProcessMentionsService.new }
|
||||
|
||||
before do
|
||||
stub_request(:post, remote_user.salmon_url)
|
||||
subject.call(status)
|
||||
end
|
||||
|
||||
it 'does not create a mention' do
|
||||
expect(remote_user.mentions.where(status: status).count).to eq 0
|
||||
end
|
||||
|
||||
it 'does not post to remote user\'s Salmon end point' do
|
||||
expect(a_request(:post, remote_user.salmon_url)).to_not have_been_made
|
||||
end
|
||||
end
|
||||
|
||||
context 'ActivityPub' do
|
||||
let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
|
||||
|
||||
subject { ProcessMentionsService.new }
|
||||
|
||||
before do
|
||||
stub_request(:post, remote_user.inbox_url)
|
||||
subject.call(status)
|
||||
end
|
||||
|
||||
it 'creates a mention' do
|
||||
expect(remote_user.mentions.where(status: status).count).to eq 1
|
||||
end
|
||||
|
||||
it 'sends activity to the inbox' do
|
||||
expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
context 'Temporarily-unreachable ActivityPub user' do
|
||||
let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox', last_webfingered_at: nil) }
|
||||
|
||||
subject { ProcessMentionsService.new }
|
||||
|
||||
before do
|
||||
stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404)
|
||||
stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:remote_user@example.com").to_return(status: 500)
|
||||
stub_request(:post, remote_user.inbox_url)
|
||||
subject.call(status)
|
||||
end
|
||||
|
||||
it 'creates a mention' do
|
||||
expect(remote_user.mentions.where(status: status).count).to eq 1
|
||||
end
|
||||
|
||||
it 'sends activity to the inbox' do
|
||||
expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once
|
||||
end
|
||||
end
|
||||
end
|
||||
71
spec/services/pubsubhubbub/subscribe_service_spec.rb
Normal file
71
spec/services/pubsubhubbub/subscribe_service_spec.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Pubsubhubbub::SubscribeService, type: :service do
|
||||
describe '#call' do
|
||||
subject { described_class.new }
|
||||
let(:user_account) { Fabricate(:account) }
|
||||
|
||||
context 'with a nil account' do
|
||||
it 'returns the invalid topic status results' do
|
||||
result = service_call(account: nil)
|
||||
|
||||
expect(result).to eq invalid_topic_status
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an invalid callback url' do
|
||||
it 'returns invalid callback status when callback is blank' do
|
||||
result = service_call(callback: '')
|
||||
|
||||
expect(result).to eq invalid_callback_status
|
||||
end
|
||||
it 'returns invalid callback status when callback is not a URI' do
|
||||
result = service_call(callback: 'invalid-hostname')
|
||||
|
||||
expect(result).to eq invalid_callback_status
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a blocked domain in the callback' do
|
||||
it 'returns callback not allowed' do
|
||||
Fabricate(:domain_block, domain: 'test.host', severity: :suspend)
|
||||
result = service_call(callback: 'https://test.host/api')
|
||||
|
||||
expect(result).to eq not_allowed_callback_status
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a valid account and callback' do
|
||||
it 'returns success status and confirms subscription' do
|
||||
allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil)
|
||||
subscription = Fabricate(:subscription, account: user_account)
|
||||
|
||||
result = service_call(callback: subscription.callback_url)
|
||||
expect(result).to eq success_status
|
||||
expect(Pubsubhubbub::ConfirmationWorker).to have_received(:perform_async).with(subscription.id, 'subscribe', 'asdf', 3600)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def service_call(account: user_account, callback: 'https://callback.host', secret: 'asdf', lease_seconds: 3600)
|
||||
subject.call(account, callback, secret, lease_seconds)
|
||||
end
|
||||
|
||||
def invalid_topic_status
|
||||
['Invalid topic URL', 422]
|
||||
end
|
||||
|
||||
def invalid_callback_status
|
||||
['Invalid callback URL', 422]
|
||||
end
|
||||
|
||||
def not_allowed_callback_status
|
||||
['Callback URL not allowed', 403]
|
||||
end
|
||||
|
||||
def success_status
|
||||
['', 202]
|
||||
end
|
||||
end
|
||||
46
spec/services/pubsubhubbub/unsubscribe_service_spec.rb
Normal file
46
spec/services/pubsubhubbub/unsubscribe_service_spec.rb
Normal file
@@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Pubsubhubbub::UnsubscribeService, type: :service do
|
||||
describe '#call' do
|
||||
subject { described_class.new }
|
||||
|
||||
context 'with a nil account' do
|
||||
it 'returns an invalid topic status' do
|
||||
result = subject.call(nil, 'callback.host')
|
||||
|
||||
expect(result).to eq invalid_topic_status
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a valid account' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
it 'returns a valid topic status and does not run confirm when no subscription' do
|
||||
allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil)
|
||||
result = subject.call(account, 'callback.host')
|
||||
|
||||
expect(result).to eq valid_topic_status
|
||||
expect(Pubsubhubbub::ConfirmationWorker).not_to have_received(:perform_async)
|
||||
end
|
||||
|
||||
it 'returns a valid topic status and does run confirm when there is a subscription' do
|
||||
subscription = Fabricate(:subscription, account: account, callback_url: 'callback.host')
|
||||
allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil)
|
||||
result = subject.call(account, 'callback.host')
|
||||
|
||||
expect(result).to eq valid_topic_status
|
||||
expect(Pubsubhubbub::ConfirmationWorker).to have_received(:perform_async).with(subscription.id, 'unsubscribe')
|
||||
end
|
||||
end
|
||||
|
||||
def invalid_topic_status
|
||||
['Invalid topic URL', 422]
|
||||
end
|
||||
|
||||
def valid_topic_status
|
||||
['', 202]
|
||||
end
|
||||
end
|
||||
end
|
||||
85
spec/services/reblog_service_spec.rb
Normal file
85
spec/services/reblog_service_spec.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ReblogService, type: :service do
|
||||
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
context 'creates a reblog with appropriate visibility' do
|
||||
let(:visibility) { :public }
|
||||
let(:reblog_visibility) { :public }
|
||||
let(:status) { Fabricate(:status, account: alice, visibility: visibility) }
|
||||
|
||||
subject { ReblogService.new }
|
||||
|
||||
before do
|
||||
subject.call(alice, status, visibility: reblog_visibility)
|
||||
end
|
||||
|
||||
describe 'reposting privately' do
|
||||
let(:reblog_visibility) { :private }
|
||||
|
||||
it 'reblogs privately' do
|
||||
expect(status.reblogs.first.visibility).to eq 'private'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'public reblogs of private toots should remain private' do
|
||||
let(:visibility) { :private }
|
||||
let(:reblog_visibility) { :public }
|
||||
|
||||
it 'reblogs privately' do
|
||||
expect(status.reblogs.first.visibility).to eq 'private'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'OStatus' do
|
||||
let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com') }
|
||||
let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com;something:something') }
|
||||
|
||||
subject { ReblogService.new }
|
||||
|
||||
before do
|
||||
stub_request(:post, 'http://salmon.example.com')
|
||||
subject.call(alice, status)
|
||||
end
|
||||
|
||||
it 'creates a reblog' do
|
||||
expect(status.reblogs.count).to eq 1
|
||||
end
|
||||
|
||||
it 'sends a Salmon slap for a remote reblog' do
|
||||
expect(a_request(:post, 'http://salmon.example.com')).to have_been_made
|
||||
end
|
||||
end
|
||||
|
||||
context 'ActivityPub' do
|
||||
let(:bob) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
|
||||
let(:status) { Fabricate(:status, account: bob) }
|
||||
|
||||
subject { ReblogService.new }
|
||||
|
||||
before do
|
||||
stub_request(:post, bob.inbox_url)
|
||||
allow(ActivityPub::DistributionWorker).to receive(:perform_async)
|
||||
subject.call(alice, status)
|
||||
end
|
||||
|
||||
it 'creates a reblog' do
|
||||
expect(status.reblogs.count).to eq 1
|
||||
end
|
||||
|
||||
describe 'after_create_commit :store_uri' do
|
||||
it 'keeps consistent reblog count' do
|
||||
expect(status.reblogs.count).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
it 'distributes to followers' do
|
||||
expect(ActivityPub::DistributionWorker).to have_received(:perform_async)
|
||||
end
|
||||
|
||||
it 'sends an announce activity to the author' do
|
||||
expect(a_request(:post, bob.inbox_url)).to have_been_made.once
|
||||
end
|
||||
end
|
||||
end
|
||||
71
spec/services/reject_follow_service_spec.rb
Normal file
71
spec/services/reject_follow_service_spec.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe RejectFollowService, type: :service do
|
||||
let(:sender) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
subject { RejectFollowService.new }
|
||||
|
||||
describe 'local' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||
|
||||
before do
|
||||
FollowRequest.create(account: bob, target_account: sender)
|
||||
subject.call(bob, sender)
|
||||
end
|
||||
|
||||
it 'removes follow request' do
|
||||
expect(bob.requested?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'does not create follow relation' do
|
||||
expect(bob.following?(sender)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remote OStatus' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
|
||||
|
||||
before do
|
||||
FollowRequest.create(account: bob, target_account: sender)
|
||||
stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
|
||||
subject.call(bob, sender)
|
||||
end
|
||||
|
||||
it 'removes follow request' do
|
||||
expect(bob.requested?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'does not create follow relation' do
|
||||
expect(bob.following?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'sends a follow request rejection salmon slap' do
|
||||
expect(a_request(:post, "http://salmon.example.com/").with { |req|
|
||||
xml = OStatus2::Salmon.new.unpack(req.body)
|
||||
xml.match(OStatus::TagManager::VERBS[:reject])
|
||||
}).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remote ActivityPub' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account }
|
||||
|
||||
before do
|
||||
FollowRequest.create(account: bob, target_account: sender)
|
||||
stub_request(:post, bob.inbox_url).to_return(status: 200)
|
||||
subject.call(bob, sender)
|
||||
end
|
||||
|
||||
it 'removes follow request' do
|
||||
expect(bob.requested?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'does not create follow relation' do
|
||||
expect(bob.following?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'sends a reject activity' do
|
||||
expect(a_request(:post, bob.inbox_url)).to have_been_made.once
|
||||
end
|
||||
end
|
||||
end
|
||||
55
spec/services/remove_status_service_spec.rb
Normal file
55
spec/services/remove_status_service_spec.rb
Normal file
@@ -0,0 +1,55 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe RemoveStatusService, type: :service do
|
||||
subject { RemoveStatusService.new }
|
||||
|
||||
let!(:alice) { Fabricate(:account) }
|
||||
let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://example.com/salmon') }
|
||||
let!(:jeff) { Fabricate(:account) }
|
||||
let!(:hank) { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
|
||||
let!(:bill) { Fabricate(:account, username: 'bill', protocol: :activitypub, domain: 'example2.com', inbox_url: 'http://example2.com/inbox') }
|
||||
|
||||
before do
|
||||
stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {})
|
||||
stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {})
|
||||
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
|
||||
stub_request(:post, 'http://example2.com/inbox').to_return(status: 200)
|
||||
|
||||
Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now)
|
||||
jeff.follow!(alice)
|
||||
hank.follow!(alice)
|
||||
|
||||
@status = PostStatusService.new.call(alice, text: 'Hello @bob@example.com')
|
||||
Fabricate(:status, account: bill, reblog: @status, uri: 'hoge')
|
||||
subject.call(@status)
|
||||
end
|
||||
|
||||
it 'removes status from author\'s home feed' do
|
||||
expect(HomeFeed.new(alice).get(10)).to_not include(@status.id)
|
||||
end
|
||||
|
||||
it 'removes status from local follower\'s home feed' do
|
||||
expect(HomeFeed.new(jeff).get(10)).to_not include(@status.id)
|
||||
end
|
||||
|
||||
it 'sends PuSH update to PuSH subscribers' do
|
||||
expect(a_request(:post, 'http://example.com/push').with { |req|
|
||||
req.body.match(OStatus::TagManager::VERBS[:delete])
|
||||
}).to have_been_made
|
||||
end
|
||||
|
||||
it 'sends delete activity to followers' do
|
||||
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.twice
|
||||
end
|
||||
|
||||
it 'sends Salmon slap to previously mentioned users' do
|
||||
expect(a_request(:post, "http://example.com/salmon").with { |req|
|
||||
xml = OStatus2::Salmon.new.unpack(req.body)
|
||||
xml.match(OStatus::TagManager::VERBS[:delete])
|
||||
}).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'sends delete activity to rebloggers' do
|
||||
expect(a_request(:post, 'http://example2.com/inbox')).to have_been_made
|
||||
end
|
||||
end
|
||||
48
spec/services/report_service_spec.rb
Normal file
48
spec/services/report_service_spec.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ReportService, type: :service do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:source_account) { Fabricate(:user).account }
|
||||
|
||||
context 'for a remote account' do
|
||||
let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') }
|
||||
|
||||
before do
|
||||
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
|
||||
end
|
||||
|
||||
it 'sends ActivityPub payload when forward is true' do
|
||||
subject.call(source_account, remote_account, forward: true)
|
||||
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made
|
||||
end
|
||||
|
||||
it 'does not send anything when forward is false' do
|
||||
subject.call(source_account, remote_account, forward: false)
|
||||
expect(a_request(:post, 'http://example.com/inbox')).to_not have_been_made
|
||||
end
|
||||
|
||||
it 'has an uri' do
|
||||
report = subject.call(source_account, remote_account, forward: true)
|
||||
expect(report.uri).to_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when other reports already exist for the same target' do
|
||||
let!(:target_account) { Fabricate(:account) }
|
||||
let!(:other_report) { Fabricate(:report, target_account: target_account) }
|
||||
|
||||
subject do
|
||||
-> { described_class.new.call(source_account, target_account) }
|
||||
end
|
||||
|
||||
before do
|
||||
ActionMailer::Base.deliveries.clear
|
||||
source_account.user.settings.notification_emails['report'] = true
|
||||
end
|
||||
|
||||
it 'does not send an e-mail' do
|
||||
is_expected.to_not change(ActionMailer::Base.deliveries, :count).from(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
146
spec/services/resolve_account_service_spec.rb
Normal file
146
spec/services/resolve_account_service_spec.rb
Normal file
@@ -0,0 +1,146 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ResolveAccountService, type: :service do
|
||||
subject { described_class.new }
|
||||
|
||||
before do
|
||||
stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt'))
|
||||
stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:catsrgr8@example.com").to_return(status: 404)
|
||||
stub_request(:get, "https://redirected.com/.well-known/host-meta").to_return(request_fixture('redirected.host-meta.txt'))
|
||||
stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404)
|
||||
stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:robcolbert@quitter.no").to_return(request_fixture('webfinger.txt'))
|
||||
stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:robcolbert@redirected.com").to_return(request_fixture('webfinger.txt'))
|
||||
stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:hacker1@redirected.com").to_return(request_fixture('webfinger-hacker1.txt'))
|
||||
stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:hacker2@redirected.com").to_return(request_fixture('webfinger-hacker2.txt'))
|
||||
stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no").to_return(status: 404)
|
||||
stub_request(:get, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt'))
|
||||
stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt'))
|
||||
stub_request(:get, "https://localdomain.com/.well-known/host-meta").to_return(request_fixture('localdomain-hostmeta.txt'))
|
||||
stub_request(:get, "https://localdomain.com/.well-known/webfinger?resource=acct:foo@localdomain.com").to_return(status: 404)
|
||||
stub_request(:get, "https://webdomain.com/.well-known/webfinger?resource=acct:foo@localdomain.com").to_return(request_fixture('localdomain-webfinger.txt'))
|
||||
stub_request(:get, "https://webdomain.com/users/foo.atom").to_return(request_fixture('localdomain-feed.txt'))
|
||||
end
|
||||
|
||||
it 'raises error if no such user can be resolved via webfinger' do
|
||||
expect(subject.call('catsrgr8@quitter.no')).to be_nil
|
||||
end
|
||||
|
||||
it 'raises error if the domain does not have webfinger' do
|
||||
expect(subject.call('catsrgr8@example.com')).to be_nil
|
||||
end
|
||||
|
||||
it 'prevents hijacking existing accounts' do
|
||||
account = subject.call('hacker1@redirected.com')
|
||||
expect(account.salmon_url).to_not eq 'https://hacker.com/main/salmon/user/7477'
|
||||
end
|
||||
|
||||
it 'prevents hijacking inexisting accounts' do
|
||||
expect(subject.call('hacker2@redirected.com')).to be_nil
|
||||
end
|
||||
|
||||
context 'with an OStatus account' do
|
||||
it 'returns an already existing remote account' do
|
||||
old_account = Fabricate(:account, username: 'robcolbert', domain: 'quitter.no')
|
||||
returned_account = subject.call('robcolbert@quitter.no')
|
||||
|
||||
expect(old_account.id).to eq returned_account.id
|
||||
end
|
||||
|
||||
it 'returns a new remote account' do
|
||||
account = subject.call('robcolbert@quitter.no')
|
||||
|
||||
expect(account.username).to eq 'robcolbert'
|
||||
expect(account.domain).to eq 'quitter.no'
|
||||
expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom'
|
||||
end
|
||||
|
||||
it 'follows a legitimate account redirection' do
|
||||
account = subject.call('robcolbert@redirected.com')
|
||||
|
||||
expect(account.username).to eq 'robcolbert'
|
||||
expect(account.domain).to eq 'quitter.no'
|
||||
expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom'
|
||||
end
|
||||
|
||||
it 'returns a new remote account' do
|
||||
account = subject.call('foo@localdomain.com')
|
||||
|
||||
expect(account.username).to eq 'foo'
|
||||
expect(account.domain).to eq 'localdomain.com'
|
||||
expect(account.remote_url).to eq 'https://webdomain.com/users/foo.atom'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an ActivityPub account' do
|
||||
before do
|
||||
stub_request(:get, "https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com").to_return(request_fixture('activitypub-webfinger.txt'))
|
||||
stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor.txt'))
|
||||
stub_request(:get, "https://ap.example.com/users/foo.atom").to_return(request_fixture('activitypub-feed.txt'))
|
||||
stub_request(:get, %r{https://ap.example.com/users/foo/\w+}).to_return(status: 404)
|
||||
end
|
||||
|
||||
it 'fallback to OStatus if actor json could not be fetched' do
|
||||
stub_request(:get, "https://ap.example.com/users/foo").to_return(status: 404)
|
||||
|
||||
account = subject.call('foo@ap.example.com')
|
||||
|
||||
expect(account.ostatus?).to eq true
|
||||
expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom'
|
||||
end
|
||||
|
||||
it 'fallback to OStatus if actor json did not have inbox_url' do
|
||||
stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor-noinbox.txt'))
|
||||
|
||||
account = subject.call('foo@ap.example.com')
|
||||
|
||||
expect(account.ostatus?).to eq true
|
||||
expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom'
|
||||
end
|
||||
|
||||
it 'returns new remote account' do
|
||||
account = subject.call('foo@ap.example.com')
|
||||
|
||||
expect(account.activitypub?).to eq true
|
||||
expect(account.domain).to eq 'ap.example.com'
|
||||
expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
|
||||
end
|
||||
|
||||
context 'with multiple types' do
|
||||
before do
|
||||
stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor-individual.txt'))
|
||||
end
|
||||
|
||||
it 'returns new remote account' do
|
||||
account = subject.call('foo@ap.example.com')
|
||||
|
||||
expect(account.activitypub?).to eq true
|
||||
expect(account.domain).to eq 'ap.example.com'
|
||||
expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
|
||||
expect(account.actor_type).to eq 'Person'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'processes one remote account at a time using locks' do
|
||||
wait_for_start = true
|
||||
fail_occurred = false
|
||||
return_values = []
|
||||
|
||||
threads = Array.new(5) do
|
||||
Thread.new do
|
||||
true while wait_for_start
|
||||
begin
|
||||
return_values << described_class.new.call('foo@localdomain.com')
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
fail_occurred = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
wait_for_start = false
|
||||
threads.each(&:join)
|
||||
|
||||
expect(fail_occurred).to be false
|
||||
expect(return_values).to_not include(nil)
|
||||
end
|
||||
end
|
||||
53
spec/services/resolve_url_service_spec.rb
Normal file
53
spec/services/resolve_url_service_spec.rb
Normal file
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe ResolveURLService, type: :service do
|
||||
subject { described_class.new }
|
||||
|
||||
describe '#call' do
|
||||
it 'returns nil when there is no atom url' do
|
||||
url = 'http://example.com/missing-atom'
|
||||
service = double
|
||||
allow(FetchAtomService).to receive(:new).and_return service
|
||||
allow(service).to receive(:call).with(url).and_return(nil)
|
||||
|
||||
result = subject.call(url)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
|
||||
it 'fetches remote accounts for feed types' do
|
||||
url = 'http://example.com/atom-feed'
|
||||
service = double
|
||||
allow(FetchAtomService).to receive(:new).and_return service
|
||||
feed_url = 'http://feed-url'
|
||||
feed_content = '<feed>contents</feed>'
|
||||
allow(service).to receive(:call).with(url).and_return([feed_url, { prefetched_body: feed_content }])
|
||||
|
||||
account_service = double
|
||||
allow(FetchRemoteAccountService).to receive(:new).and_return(account_service)
|
||||
allow(account_service).to receive(:call)
|
||||
|
||||
_result = subject.call(url)
|
||||
|
||||
expect(account_service).to have_received(:call).with(feed_url, feed_content, nil)
|
||||
end
|
||||
|
||||
it 'fetches remote statuses for entry types' do
|
||||
url = 'http://example.com/atom-entry'
|
||||
service = double
|
||||
allow(FetchAtomService).to receive(:new).and_return service
|
||||
feed_url = 'http://feed-url'
|
||||
feed_content = '<entry>contents</entry>'
|
||||
allow(service).to receive(:call).with(url).and_return([feed_url, { prefetched_body: feed_content }])
|
||||
|
||||
account_service = double
|
||||
allow(FetchRemoteStatusService).to receive(:new).and_return(account_service)
|
||||
allow(account_service).to receive(:call)
|
||||
|
||||
_result = subject.call(url)
|
||||
|
||||
expect(account_service).to have_received(:call).with(feed_url, feed_content, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
101
spec/services/search_service_spec.rb
Normal file
101
spec/services/search_service_spec.rb
Normal file
@@ -0,0 +1,101 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe SearchService, type: :service do
|
||||
subject { described_class.new }
|
||||
|
||||
describe '#call' do
|
||||
describe 'with a blank query' do
|
||||
it 'returns empty results without searching' do
|
||||
allow(AccountSearchService).to receive(:new)
|
||||
allow(Tag).to receive(:search_for)
|
||||
results = subject.call('', nil, 10)
|
||||
|
||||
expect(results).to eq(empty_results)
|
||||
expect(AccountSearchService).not_to have_received(:new)
|
||||
expect(Tag).not_to have_received(:search_for)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with an url query' do
|
||||
before do
|
||||
@query = 'http://test.host/query'
|
||||
end
|
||||
|
||||
context 'that does not find anything' do
|
||||
it 'returns the empty results' do
|
||||
service = double(call: nil)
|
||||
allow(ResolveURLService).to receive(:new).and_return(service)
|
||||
results = subject.call(@query, nil, 10)
|
||||
|
||||
expect(service).to have_received(:call).with(@query, on_behalf_of: nil)
|
||||
expect(results).to eq empty_results
|
||||
end
|
||||
end
|
||||
|
||||
context 'that finds an account' do
|
||||
it 'includes the account in the results' do
|
||||
account = Account.new
|
||||
service = double(call: account)
|
||||
allow(ResolveURLService).to receive(:new).and_return(service)
|
||||
|
||||
results = subject.call(@query, nil, 10)
|
||||
expect(service).to have_received(:call).with(@query, on_behalf_of: nil)
|
||||
expect(results).to eq empty_results.merge(accounts: [account])
|
||||
end
|
||||
end
|
||||
|
||||
context 'that finds a status' do
|
||||
it 'includes the status in the results' do
|
||||
status = Status.new
|
||||
service = double(call: status)
|
||||
allow(ResolveURLService).to receive(:new).and_return(service)
|
||||
|
||||
results = subject.call(@query, nil, 10)
|
||||
expect(service).to have_received(:call).with(@query, on_behalf_of: nil)
|
||||
expect(results).to eq empty_results.merge(statuses: [status])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with a non-url query' do
|
||||
context 'that matches an account' do
|
||||
it 'includes the account in the results' do
|
||||
query = 'username'
|
||||
account = Account.new
|
||||
service = double(call: [account])
|
||||
allow(AccountSearchService).to receive(:new).and_return(service)
|
||||
|
||||
results = subject.call(query, nil, 10)
|
||||
expect(service).to have_received(:call).with(query, nil, limit: 10, offset: 0, resolve: false)
|
||||
expect(results).to eq empty_results.merge(accounts: [account])
|
||||
end
|
||||
end
|
||||
|
||||
context 'that matches a tag' do
|
||||
it 'includes the tag in the results' do
|
||||
query = '#tag'
|
||||
tag = Tag.new
|
||||
allow(Tag).to receive(:search_for).with('tag', 10, 0).and_return([tag])
|
||||
|
||||
results = subject.call(query, nil, 10)
|
||||
expect(Tag).to have_received(:search_for).with('tag', 10, 0)
|
||||
expect(results).to eq empty_results.merge(hashtags: [tag])
|
||||
end
|
||||
it 'does not include tag when starts with @ character' do
|
||||
query = '@username'
|
||||
allow(Tag).to receive(:search_for)
|
||||
|
||||
results = subject.call(query, nil, 10)
|
||||
expect(Tag).not_to have_received(:search_for)
|
||||
expect(results).to eq empty_results
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def empty_results
|
||||
{ accounts: [], hashtags: [], statuses: [] }
|
||||
end
|
||||
end
|
||||
7
spec/services/send_interaction_service_spec.rb
Normal file
7
spec/services/send_interaction_service_spec.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SendInteractionService, type: :service do
|
||||
subject { SendInteractionService.new }
|
||||
|
||||
it 'sends an XML envelope to the Salmon end point of remote user'
|
||||
end
|
||||
43
spec/services/subscribe_service_spec.rb
Normal file
43
spec/services/subscribe_service_spec.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SubscribeService, type: :service do
|
||||
let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com', hub_url: 'http://hub.example.com') }
|
||||
subject { SubscribeService.new }
|
||||
|
||||
it 'sends subscription request to PuSH hub' do
|
||||
stub_request(:post, 'http://hub.example.com/').to_return(status: 202)
|
||||
subject.call(account)
|
||||
expect(a_request(:post, 'http://hub.example.com/')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'generates and keeps PuSH secret on successful call' do
|
||||
stub_request(:post, 'http://hub.example.com/').to_return(status: 202)
|
||||
subject.call(account)
|
||||
expect(account.secret).to_not be_blank
|
||||
end
|
||||
|
||||
it 'fails silently if PuSH hub forbids subscription' do
|
||||
stub_request(:post, 'http://hub.example.com/').to_return(status: 403)
|
||||
subject.call(account)
|
||||
end
|
||||
|
||||
it 'fails silently if PuSH hub is not found' do
|
||||
stub_request(:post, 'http://hub.example.com/').to_return(status: 404)
|
||||
subject.call(account)
|
||||
end
|
||||
|
||||
it 'fails loudly if there is a network error' do
|
||||
stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error)
|
||||
expect { subject.call(account) }.to raise_error HTTP::Error
|
||||
end
|
||||
|
||||
it 'fails loudly if PuSH hub is unavailable' do
|
||||
stub_request(:post, 'http://hub.example.com/').to_return(status: 503)
|
||||
expect { subject.call(account) }.to raise_error GabSocial::UnexpectedResponseError
|
||||
end
|
||||
|
||||
it 'fails loudly if rate limited' do
|
||||
stub_request(:post, 'http://hub.example.com/').to_return(status: 429)
|
||||
expect { subject.call(account) }.to raise_error GabSocial::UnexpectedResponseError
|
||||
end
|
||||
end
|
||||
88
spec/services/suspend_account_service_spec.rb
Normal file
88
spec/services/suspend_account_service_spec.rb
Normal file
@@ -0,0 +1,88 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SuspendAccountService, type: :service do
|
||||
describe '#call on local account' do
|
||||
before do
|
||||
stub_request(:post, "https://alice.com/inbox").to_return(status: 201)
|
||||
stub_request(:post, "https://bob.com/inbox").to_return(status: 201)
|
||||
end
|
||||
|
||||
subject do
|
||||
-> { described_class.new.call(account) }
|
||||
end
|
||||
|
||||
let!(:account) { Fabricate(:account) }
|
||||
let!(:status) { Fabricate(:status, account: account) }
|
||||
let!(:media_attachment) { Fabricate(:media_attachment, account: account) }
|
||||
let!(:notification) { Fabricate(:notification, account: account) }
|
||||
let!(:favourite) { Fabricate(:favourite, account: account) }
|
||||
let!(:active_relationship) { Fabricate(:follow, account: account) }
|
||||
let!(:passive_relationship) { Fabricate(:follow, target_account: account) }
|
||||
let!(:subscription) { Fabricate(:subscription, account: account) }
|
||||
let!(:remote_alice) { Fabricate(:account, inbox_url: 'https://alice.com/inbox', protocol: :activitypub) }
|
||||
let!(:remote_bob) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
|
||||
|
||||
it 'deletes associated records' do
|
||||
is_expected.to change {
|
||||
[
|
||||
account.statuses,
|
||||
account.media_attachments,
|
||||
account.stream_entries,
|
||||
account.notifications,
|
||||
account.favourites,
|
||||
account.active_relationships,
|
||||
account.passive_relationships,
|
||||
account.subscriptions
|
||||
].map(&:count)
|
||||
}.from([1, 1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0])
|
||||
end
|
||||
|
||||
it 'sends a delete actor activity to all known inboxes' do
|
||||
subject.call
|
||||
expect(a_request(:post, "https://alice.com/inbox")).to have_been_made.once
|
||||
expect(a_request(:post, "https://bob.com/inbox")).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
describe '#call on remote account' do
|
||||
before do
|
||||
stub_request(:post, "https://alice.com/inbox").to_return(status: 201)
|
||||
stub_request(:post, "https://bob.com/inbox").to_return(status: 201)
|
||||
end
|
||||
|
||||
subject do
|
||||
-> { described_class.new.call(remote_bob) }
|
||||
end
|
||||
|
||||
let!(:account) { Fabricate(:account) }
|
||||
let!(:remote_alice) { Fabricate(:account, inbox_url: 'https://alice.com/inbox', protocol: :activitypub) }
|
||||
let!(:remote_bob) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
|
||||
let!(:status) { Fabricate(:status, account: remote_bob) }
|
||||
let!(:media_attachment) { Fabricate(:media_attachment, account: remote_bob) }
|
||||
let!(:notification) { Fabricate(:notification, account: remote_bob) }
|
||||
let!(:favourite) { Fabricate(:favourite, account: remote_bob) }
|
||||
let!(:active_relationship) { Fabricate(:follow, account: remote_bob, target_account: account) }
|
||||
let!(:passive_relationship) { Fabricate(:follow, target_account: remote_bob) }
|
||||
let!(:subscription) { Fabricate(:subscription, account: remote_bob) }
|
||||
|
||||
it 'deletes associated records' do
|
||||
is_expected.to change {
|
||||
[
|
||||
remote_bob.statuses,
|
||||
remote_bob.media_attachments,
|
||||
remote_bob.stream_entries,
|
||||
remote_bob.notifications,
|
||||
remote_bob.favourites,
|
||||
remote_bob.active_relationships,
|
||||
remote_bob.passive_relationships,
|
||||
remote_bob.subscriptions
|
||||
].map(&:count)
|
||||
}.from([1, 1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0])
|
||||
end
|
||||
|
||||
it 'sends a reject follow to follwer inboxes' do
|
||||
subject.call
|
||||
expect(a_request(:post, remote_bob.inbox_url)).to have_been_made.once
|
||||
end
|
||||
end
|
||||
end
|
||||
43
spec/services/unblock_domain_service_spec.rb
Normal file
43
spec/services/unblock_domain_service_spec.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe UnblockDomainService, type: :service do
|
||||
subject { described_class.new }
|
||||
|
||||
describe 'call' do
|
||||
before do
|
||||
@independently_suspended = Fabricate(:account, domain: 'example.com', suspended_at: 1.hour.ago)
|
||||
@independently_silenced = Fabricate(:account, domain: 'example.com', silenced_at: 1.hour.ago)
|
||||
@domain_block = Fabricate(:domain_block, domain: 'example.com')
|
||||
@silenced = Fabricate(:account, domain: 'example.com', silenced_at: @domain_block.created_at)
|
||||
@suspended = Fabricate(:account, domain: 'example.com', suspended_at: @domain_block.created_at)
|
||||
end
|
||||
|
||||
it 'unsilences accounts and removes block' do
|
||||
@domain_block.update(severity: :silence)
|
||||
|
||||
subject.call(@domain_block)
|
||||
expect_deleted_domain_block
|
||||
expect(@silenced.reload.silenced?).to be false
|
||||
expect(@suspended.reload.suspended?).to be true
|
||||
expect(@independently_suspended.reload.suspended?).to be true
|
||||
expect(@independently_silenced.reload.silenced?).to be true
|
||||
end
|
||||
|
||||
it 'unsuspends accounts and removes block' do
|
||||
@domain_block.update(severity: :suspend)
|
||||
|
||||
subject.call(@domain_block)
|
||||
expect_deleted_domain_block
|
||||
expect(@suspended.reload.suspended?).to be false
|
||||
expect(@silenced.reload.silenced?).to be true
|
||||
expect(@independently_suspended.reload.suspended?).to be true
|
||||
expect(@independently_silenced.reload.silenced?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
def expect_deleted_domain_block
|
||||
expect { @domain_block.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
59
spec/services/unblock_service_spec.rb
Normal file
59
spec/services/unblock_service_spec.rb
Normal file
@@ -0,0 +1,59 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe UnblockService, type: :service do
|
||||
let(:sender) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
subject { UnblockService.new }
|
||||
|
||||
describe 'local' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||
|
||||
before do
|
||||
sender.block!(bob)
|
||||
subject.call(sender, bob)
|
||||
end
|
||||
|
||||
it 'destroys the blocking relation' do
|
||||
expect(sender.blocking?(bob)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remote OStatus' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
|
||||
|
||||
before do
|
||||
sender.block!(bob)
|
||||
stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
|
||||
subject.call(sender, bob)
|
||||
end
|
||||
|
||||
it 'destroys the blocking relation' do
|
||||
expect(sender.blocking?(bob)).to be false
|
||||
end
|
||||
|
||||
it 'sends an unblock salmon slap' do
|
||||
expect(a_request(:post, "http://salmon.example.com/").with { |req|
|
||||
xml = OStatus2::Salmon.new.unpack(req.body)
|
||||
xml.match(OStatus::TagManager::VERBS[:unblock])
|
||||
}).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remote ActivityPub' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account }
|
||||
|
||||
before do
|
||||
sender.block!(bob)
|
||||
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
|
||||
subject.call(sender, bob)
|
||||
end
|
||||
|
||||
it 'destroys the blocking relation' do
|
||||
expect(sender.blocking?(bob)).to be false
|
||||
end
|
||||
|
||||
it 'sends an unblock activity' do
|
||||
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
|
||||
end
|
||||
end
|
||||
end
|
||||
77
spec/services/unfollow_service_spec.rb
Normal file
77
spec/services/unfollow_service_spec.rb
Normal file
@@ -0,0 +1,77 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe UnfollowService, type: :service do
|
||||
let(:sender) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
subject { UnfollowService.new }
|
||||
|
||||
describe 'local' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||
|
||||
before do
|
||||
sender.follow!(bob)
|
||||
subject.call(sender, bob)
|
||||
end
|
||||
|
||||
it 'destroys the following relation' do
|
||||
expect(sender.following?(bob)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remote OStatus' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
|
||||
|
||||
before do
|
||||
sender.follow!(bob)
|
||||
stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
|
||||
subject.call(sender, bob)
|
||||
end
|
||||
|
||||
it 'destroys the following relation' do
|
||||
expect(sender.following?(bob)).to be false
|
||||
end
|
||||
|
||||
it 'sends an unfollow salmon slap' do
|
||||
expect(a_request(:post, "http://salmon.example.com/").with { |req|
|
||||
xml = OStatus2::Salmon.new.unpack(req.body)
|
||||
xml.match(OStatus::TagManager::VERBS[:unfollow])
|
||||
}).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remote ActivityPub' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account }
|
||||
|
||||
before do
|
||||
sender.follow!(bob)
|
||||
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
|
||||
subject.call(sender, bob)
|
||||
end
|
||||
|
||||
it 'destroys the following relation' do
|
||||
expect(sender.following?(bob)).to be false
|
||||
end
|
||||
|
||||
it 'sends an unfollow activity' do
|
||||
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remote ActivityPub (reverse)' do
|
||||
let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account }
|
||||
|
||||
before do
|
||||
bob.follow!(sender)
|
||||
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
|
||||
subject.call(bob, sender)
|
||||
end
|
||||
|
||||
it 'destroys the following relation' do
|
||||
expect(bob.following?(sender)).to be false
|
||||
end
|
||||
|
||||
it 'sends a reject activity' do
|
||||
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
|
||||
end
|
||||
end
|
||||
end
|
||||
5
spec/services/unmute_service_spec.rb
Normal file
5
spec/services/unmute_service_spec.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe UnmuteService, type: :service do
|
||||
subject { UnmuteService.new }
|
||||
end
|
||||
37
spec/services/unsubscribe_service_spec.rb
Normal file
37
spec/services/unsubscribe_service_spec.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe UnsubscribeService, type: :service do
|
||||
let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com', hub_url: 'http://hub.example.com') }
|
||||
subject { UnsubscribeService.new }
|
||||
|
||||
it 'removes the secret and resets expiration on account' do
|
||||
stub_request(:post, 'http://hub.example.com/').to_return(status: 204)
|
||||
subject.call(account)
|
||||
account.reload
|
||||
|
||||
expect(account.secret).to be_blank
|
||||
expect(account.subscription_expires_at).to be_blank
|
||||
end
|
||||
|
||||
it 'logs error on subscription failure' do
|
||||
logger = stub_logger
|
||||
stub_request(:post, 'http://hub.example.com/').to_return(status: 404)
|
||||
subject.call(account)
|
||||
|
||||
expect(logger).to have_received(:debug).with(/unsubscribe for bob@example.com failed/)
|
||||
end
|
||||
|
||||
it 'logs error on connection failure' do
|
||||
logger = stub_logger
|
||||
stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error)
|
||||
subject.call(account)
|
||||
|
||||
expect(logger).to have_received(:debug).with(/unsubscribe for bob@example.com failed/)
|
||||
end
|
||||
|
||||
def stub_logger
|
||||
double(debug: nil).tap do |logger|
|
||||
allow(Rails).to receive(:logger).and_return(logger)
|
||||
end
|
||||
end
|
||||
end
|
||||
84
spec/services/update_remote_profile_service_spec.rb
Normal file
84
spec/services/update_remote_profile_service_spec.rb
Normal file
@@ -0,0 +1,84 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe UpdateRemoteProfileService, type: :service do
|
||||
let(:xml) { File.read(Rails.root.join('spec', 'fixtures', 'push', 'feed.atom')) }
|
||||
|
||||
subject { UpdateRemoteProfileService.new }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png').to_return(request_fixture('avatar.txt'))
|
||||
end
|
||||
|
||||
context 'with updated details' do
|
||||
let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com') }
|
||||
|
||||
before do
|
||||
subject.call(xml, remote_account)
|
||||
end
|
||||
|
||||
it 'downloads new avatar' do
|
||||
expect(a_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png')).to have_been_made
|
||||
end
|
||||
|
||||
it 'sets the avatar remote url' do
|
||||
expect(remote_account.reload.avatar_remote_url).to eq 'https://quitter.no/avatar/7477-300-20160211190340.png'
|
||||
end
|
||||
|
||||
it 'sets display name' do
|
||||
expect(remote_account.reload.display_name).to eq 'DIGITAL CAT'
|
||||
end
|
||||
|
||||
it 'sets note' do
|
||||
expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unchanged details' do
|
||||
let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com', display_name: 'DIGITAL CAT', note: 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes', avatar_remote_url: 'https://quitter.no/avatar/7477-300-20160211190340.png') }
|
||||
|
||||
before do
|
||||
subject.call(xml, remote_account)
|
||||
end
|
||||
|
||||
it 'does not re-download avatar' do
|
||||
expect(a_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png')).to have_been_made.once
|
||||
end
|
||||
|
||||
it 'sets the avatar remote url' do
|
||||
expect(remote_account.reload.avatar_remote_url).to eq 'https://quitter.no/avatar/7477-300-20160211190340.png'
|
||||
end
|
||||
|
||||
it 'sets display name' do
|
||||
expect(remote_account.reload.display_name).to eq 'DIGITAL CAT'
|
||||
end
|
||||
|
||||
it 'sets note' do
|
||||
expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with updated details from a domain set to reject media' do
|
||||
let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com') }
|
||||
let!(:domain_block) { Fabricate(:domain_block, domain: 'example.com', reject_media: true) }
|
||||
|
||||
before do
|
||||
subject.call(xml, remote_account)
|
||||
end
|
||||
|
||||
it 'does not the avatar remote url' do
|
||||
expect(remote_account.reload.avatar_remote_url).to be_nil
|
||||
end
|
||||
|
||||
it 'sets display name' do
|
||||
expect(remote_account.reload.display_name).to eq 'DIGITAL CAT'
|
||||
end
|
||||
|
||||
it 'sets note' do
|
||||
expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes'
|
||||
end
|
||||
|
||||
it 'does not set store the avatar' do
|
||||
expect(remote_account.reload.avatar_file_name).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
109
spec/services/verify_link_service_spec.rb
Normal file
109
spec/services/verify_link_service_spec.rb
Normal file
@@ -0,0 +1,109 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe VerifyLinkService, type: :service do
|
||||
subject { described_class.new }
|
||||
|
||||
context 'given a local account' do
|
||||
let(:account) { Fabricate(:account, username: 'alice') }
|
||||
let(:field) { Account::Field.new(account, 'name' => 'Website', 'value' => 'http://example.com') }
|
||||
|
||||
before do
|
||||
stub_request(:head, 'https://redirect.me/abc').to_return(status: 301, headers: { 'Location' => ActivityPub::TagManager.instance.url_for(account) })
|
||||
stub_request(:get, 'http://example.com').to_return(status: 200, body: html)
|
||||
subject.call(field)
|
||||
end
|
||||
|
||||
context 'when a link contains an <a> back' do
|
||||
let(:html) do
|
||||
<<-HTML
|
||||
<!doctype html>
|
||||
<body>
|
||||
<a href="#{ActivityPub::TagManager.instance.url_for(account)}" rel="me">Follow me on Gab Social</a>
|
||||
</body>
|
||||
HTML
|
||||
end
|
||||
|
||||
it 'marks the field as verified' do
|
||||
expect(field.verified?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a link contains an <a rel="noopener"> back' do
|
||||
let(:html) do
|
||||
<<-HTML
|
||||
<!doctype html>
|
||||
<body>
|
||||
<a href="#{ActivityPub::TagManager.instance.url_for(account)}" rel="noopener me" target="_blank">Follow me on Gab Social</a>
|
||||
</body>
|
||||
HTML
|
||||
end
|
||||
|
||||
it 'marks the field as verified' do
|
||||
expect(field.verified?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a link contains a <link> back' do
|
||||
let(:html) do
|
||||
<<-HTML
|
||||
<!doctype html>
|
||||
<head>
|
||||
<link type="text/html" href="#{ActivityPub::TagManager.instance.url_for(account)}" rel="me" />
|
||||
</head>
|
||||
HTML
|
||||
end
|
||||
|
||||
it 'marks the field as verified' do
|
||||
expect(field.verified?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a link goes through a redirect back' do
|
||||
let(:html) do
|
||||
<<-HTML
|
||||
<!doctype html>
|
||||
<head>
|
||||
<link type="text/html" href="https://redirect.me/abc" rel="me" />
|
||||
</head>
|
||||
HTML
|
||||
end
|
||||
|
||||
it 'marks the field as verified' do
|
||||
expect(field.verified?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a link does not contain a link back' do
|
||||
let(:html) { '' }
|
||||
|
||||
it 'marks the field as verified' do
|
||||
expect(field.verified?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'given a remote account' do
|
||||
let(:account) { Fabricate(:account, username: 'alice', domain: 'example.com', url: 'https://profile.example.com/alice') }
|
||||
let(:field) { Account::Field.new(account, 'name' => 'Website', 'value' => '<a href="http://example.com" rel="me"><span class="invisible">http://</span><span class="">example.com</span><span class="invisible"></span></a>') }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'http://example.com').to_return(status: 200, body: html)
|
||||
subject.call(field)
|
||||
end
|
||||
|
||||
context 'when a link contains an <a> back' do
|
||||
let(:html) do
|
||||
<<-HTML
|
||||
<!doctype html>
|
||||
<body>
|
||||
<a href="https://profile.example.com/alice" rel="me">Follow me on Gab Social</a>
|
||||
</body>
|
||||
HTML
|
||||
end
|
||||
|
||||
it 'marks the field as verified' do
|
||||
expect(field.verified?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user