Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
- Gemfile.lock
- app/controllers/accounts_controller.rb
- app/controllers/admin/dashboard_controller.rb
- app/controllers/follower_accounts_controller.rb
- app/controllers/following_accounts_controller.rb
- app/controllers/remote_follow_controller.rb
- app/controllers/stream_entries_controller.rb
- app/controllers/tags_controller.rb
- app/javascript/packs/public.js
- app/lib/sanitize_config.rb
- app/models/account.rb
- app/models/form/admin_settings.rb
- app/models/media_attachment.rb
- app/models/stream_entry.rb
- app/models/user.rb
- app/serializers/initial_state_serializer.rb
- app/services/batched_remove_status_service.rb
- app/services/post_status_service.rb
- app/services/process_mentions_service.rb
- app/services/reblog_service.rb
- app/services/remove_status_service.rb
- app/views/admin/settings/edit.html.haml
- config/locales/simple_form.pl.yml
- config/settings.yml
- docker-compose.yml
This commit is contained in:
Thibaut Girka
2019-07-19 18:26:49 +02:00
411 changed files with 3416 additions and 8150 deletions

View File

@@ -48,37 +48,6 @@ RSpec.describe AccountsController, type: :controller do
end
end
context 'atom' do
let(:format) { 'atom' }
let(:content_type) { 'application/atom+xml' }
shared_examples 'responsed streams' do
it 'assigns @entries' do
entries = assigns(:entries).to_a
expect(entries.size).to eq expected_statuses.size
entries.each.zip(expected_statuses.each) do |entry, expected_status|
expect(entry.status).to eq expected_status
end
end
end
include_examples 'responses'
context 'without max_id nor since_id' do
let(:expected_statuses) { [status7, status6, status5, status4, status3, status2, status1] }
include_examples 'responsed streams'
end
context 'with max_id and since_id' do
let(:max_id) { status4.stream_entry.id }
let(:since_id) { status1.stream_entry.id }
let(:expected_statuses) { [status3, status2] }
include_examples 'responsed streams'
end
end
context 'activitystreams2' do
let(:format) { 'json' }
let(:content_type) { 'application/activity+json' }

View File

@@ -4,7 +4,7 @@ require 'rails_helper'
RSpec.describe ActivityPub::InboxesController, type: :controller do
describe 'POST #create' do
context 'if signed_request_account' do
context 'with signed_request_account' do
it 'returns 202' do
allow(controller).to receive(:signed_request_account) do
Fabricate(:account)
@@ -15,7 +15,7 @@ RSpec.describe ActivityPub::InboxesController, type: :controller do
end
end
context 'not signed_request_account' do
context 'without signed_request_account' do
it 'returns 401' do
allow(controller).to receive(:signed_request_account) do
false

View File

@@ -75,44 +75,6 @@ RSpec.describe Admin::AccountsController, type: :controller do
end
end
describe 'POST #subscribe' do
subject { post :subscribe, params: { id: account.id } }
let(:current_user) { Fabricate(:user, admin: admin) }
let(:account) { Fabricate(:account) }
context 'when user is admin' do
let(:admin) { true }
it { is_expected.to redirect_to admin_account_path(account.id) }
end
context 'when user is not admin' do
let(:admin) { false }
it { is_expected.to have_http_status :forbidden }
end
end
describe 'POST #unsubscribe' do
subject { post :unsubscribe, params: { id: account.id } }
let(:current_user) { Fabricate(:user, admin: admin) }
let(:account) { Fabricate(:account) }
context 'when user is admin' do
let(:admin) { true }
it { is_expected.to redirect_to admin_account_path(account.id) }
end
context 'when user is not admin' do
let(:admin) { false }
it { is_expected.to have_http_status :forbidden }
end
end
describe 'POST #memorialize' do
subject { post :memorialize, params: { id: account.id } }

View File

@@ -1,32 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Admin::SubscriptionsController, type: :controller do
render_views
describe 'GET #index' do
around do |example|
default_per_page = Subscription.default_per_page
Subscription.paginates_per 1
example.run
Subscription.paginates_per default_per_page
end
before do
sign_in Fabricate(:user, admin: true), scope: :user
end
it 'renders subscriptions' do
Fabricate(:subscription)
specified = Fabricate(:subscription)
get :index
subscriptions = assigns(:subscriptions)
expect(subscriptions.count).to eq 1
expect(subscriptions[0]).to eq specified
expect(response).to have_http_status(200)
end
end
end

View File

@@ -9,7 +9,7 @@ RSpec.describe Api::OEmbedController, type: :controller do
describe 'GET #show' do
before do
request.host = Rails.configuration.x.local_domain
get :show, params: { url: account_stream_entry_url(alice, status.stream_entry) }, format: :json
get :show, params: { url: short_account_status_url(alice, status) }, format: :json
end
it 'returns http success' do

View File

@@ -1,59 +0,0 @@
require 'rails_helper'
RSpec.describe Api::PushController, type: :controller do
describe 'POST #update' do
context 'with hub.mode=subscribe' do
it 'creates a subscription' do
service = double(call: ['', 202])
allow(Pubsubhubbub::SubscribeService).to receive(:new).and_return(service)
account = Fabricate(:account)
account_topic_url = "https://#{Rails.configuration.x.local_domain}/users/#{account.username}.atom"
post :update, params: {
'hub.mode' => 'subscribe',
'hub.topic' => account_topic_url,
'hub.callback' => 'https://callback.host/api',
'hub.lease_seconds' => '3600',
'hub.secret' => 'as1234df',
}
expect(service).to have_received(:call).with(
account,
'https://callback.host/api',
'as1234df',
'3600',
nil
)
expect(response).to have_http_status(202)
end
end
context 'with hub.mode=unsubscribe' do
it 'unsubscribes the account' do
service = double(call: ['', 202])
allow(Pubsubhubbub::UnsubscribeService).to receive(:new).and_return(service)
account = Fabricate(:account)
account_topic_url = "https://#{Rails.configuration.x.local_domain}/users/#{account.username}.atom"
post :update, params: {
'hub.mode' => 'unsubscribe',
'hub.topic' => account_topic_url,
'hub.callback' => 'https://callback.host/api',
}
expect(service).to have_received(:call).with(
account,
'https://callback.host/api',
)
expect(response).to have_http_status(202)
end
end
context 'with unknown mode' do
it 'returns an unknown mode error' do
post :update, params: { 'hub.mode' => 'fake' }
expect(response).to have_http_status(422)
expect(response.body).to match(/Unknown mode/)
end
end
end
end

View File

@@ -1,65 +0,0 @@
require 'rails_helper'
RSpec.describe Api::SalmonController, type: :controller do
render_views
let(:account) { Fabricate(:user, account: Fabricate(:account, username: 'catsrgr8')).account }
before do
stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt'))
stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt'))
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'))
end
describe 'POST #update' do
context 'with valid post data' do
before do
post :update, params: { id: account.id }, body: File.read(Rails.root.join('spec', 'fixtures', 'salmon', 'mention.xml'))
end
it 'contains XML in the request body' do
expect(request.body.read).to be_a String
end
it 'returns http success' do
expect(response).to have_http_status(202)
end
it 'creates remote account' do
expect(Account.find_by(username: 'gargron', domain: 'quitter.no')).to_not be_nil
end
it 'creates status' do
expect(Status.find_by(uri: 'tag:quitter.no,2016-03-20:noticeId=1276923:objectType=note')).to_not be_nil
end
it 'creates mention for target account' do
expect(account.mentions.count).to eq 1
end
end
context 'with empty post data' do
before do
post :update, params: { id: account.id }, body: ''
end
it 'returns http client error' do
expect(response).to have_http_status(400)
end
end
context 'with invalid post data' do
before do
service = double(call: false)
allow(VerifySalmonService).to receive(:new).and_return(service)
post :update, params: { id: account.id }, body: File.read(Rails.root.join('spec', 'fixtures', 'salmon', 'mention.xml'))
end
it 'returns http client error' do
expect(response).to have_http_status(401)
end
end
end
end

View File

@@ -1,68 +0,0 @@
require 'rails_helper'
RSpec.describe Api::SubscriptionsController, type: :controller do
render_views
let(:account) { Fabricate(:account, username: 'gargron', domain: 'quitter.no', remote_url: 'topic_url', secret: 'abc') }
describe 'GET #show' do
context 'with valid subscription' do
before do
get :show, params: { :id => account.id, 'hub.topic' => 'topic_url', 'hub.challenge' => '456', 'hub.lease_seconds' => "#{86400 * 30}" }
end
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'echoes back the challenge' do
expect(response.body).to match '456'
end
end
context 'with invalid subscription' do
before do
expect_any_instance_of(Account).to receive_message_chain(:subscription, :valid?).and_return(false)
get :show, params: { :id => account.id }
end
it 'returns http success' do
expect(response).to have_http_status(404)
end
end
end
describe 'POST #update' do
let(:feed) { File.read(Rails.root.join('spec', 'fixtures', 'push', 'feed.atom')) }
before do
stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {})
stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt'))
stub_request(:get, "https://quitter.no/notice/1269244").to_return(status: 404)
stub_request(:get, "https://quitter.no/notice/1265331").to_return(status: 404)
stub_request(:get, "https://community.highlandarrow.com/notice/54411").to_return(status: 404)
stub_request(:get, "https://community.highlandarrow.com/notice/53857").to_return(status: 404)
stub_request(:get, "https://community.highlandarrow.com/notice/51852").to_return(status: 404)
stub_request(:get, "https://social.umeahackerspace.se/notice/424348").to_return(status: 404)
stub_request(:get, "https://community.highlandarrow.com/notice/50467").to_return(status: 404)
stub_request(:get, "https://quitter.no/notice/1243309").to_return(status: 404)
stub_request(:get, "https://quitter.no/user/7477").to_return(status: 404)
stub_request(:any, "https://community.highlandarrow.com/user/1").to_return(status: 404)
stub_request(:any, "https://social.umeahackerspace.se/user/2").to_return(status: 404)
stub_request(:any, "https://gs.kawa-kun.com/user/2").to_return(status: 404)
stub_request(:any, "https://mastodon.social/users/Gargron").to_return(status: 404)
request.env['HTTP_X_HUB_SIGNATURE'] = "sha1=#{OpenSSL::HMAC.hexdigest('sha1', 'abc', feed)}"
post :update, params: { id: account.id }, body: feed
end
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'creates statuses for feed' do
expect(account.statuses.count).to_not eq 0
end
end
end

View File

@@ -1,51 +0,0 @@
require 'rails_helper'
RSpec.describe Api::V1::FollowsController, type: :controller do
render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:follows') }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'POST #create' do
before do
stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt'))
stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt'))
stub_request(:head, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(:status => 405, :body => "", :headers => {})
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(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {})
stub_request(:post, "https://quitter.no/main/salmon/user/7477").to_return(:status => 200, :body => "", :headers => {})
post :create, params: { uri: 'gargron@quitter.no' }
end
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'creates account for remote user' do
expect(Account.find_by(username: 'gargron', domain: 'quitter.no')).to_not be_nil
end
it 'creates a follow relation between user and remote user' do
expect(user.account.following?(Account.find_by(username: 'gargron', domain: 'quitter.no'))).to be true
end
it 'sends a salmon slap to the remote user' do
expect(a_request(:post, "https://quitter.no/main/salmon/user/7477")).to have_been_made
end
it 'subscribes to remote hub' do
expect(a_request(:post, "https://quitter.no/main/push/hub")).to have_been_made
end
it 'returns http success if already following, too' do
post :create, params: { uri: 'gargron@quitter.no' }
expect(response).to have_http_status(200)
end
end
end

View File

@@ -364,9 +364,5 @@ describe ApplicationController, type: :controller do
context 'Status' do
include_examples 'cacheable', :status, Status
end
context 'StreamEntry' do
include_examples 'receives :with_includes', :stream_entry, StreamEntry
end
end
end

View File

@@ -41,7 +41,7 @@ describe ApplicationController, type: :controller do
it 'sets link headers' do
account = Fabricate(:account, username: 'username', user: Fabricate(:user))
get 'success', params: { account_username: 'username' }
expect(response.headers['Link'].to_s).to eq '<http://test.host/.well-known/webfinger?resource=acct%3Ausername%40cb6e6126.ngrok.io>; rel="lrdd"; type="application/xrd+xml", <http://test.host/users/username.atom>; rel="alternate"; type="application/atom+xml", <https://cb6e6126.ngrok.io/users/username>; rel="alternate"; type="application/activity+json"'
expect(response.headers['Link'].to_s).to eq '<http://test.host/.well-known/webfinger?resource=acct%3Ausername%40cb6e6126.ngrok.io>; rel="lrdd"; type="application/jrd+json", <https://cb6e6126.ngrok.io/users/username>; rel="alternate"; type="application/activity+json"'
end
it 'returns http success' do

View File

@@ -38,7 +38,7 @@ describe ApplicationController, type: :controller do
end
context 'with signature header' do
let!(:author) { Fabricate(:account) }
let!(:author) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor') }
context 'without body' do
before do

View File

@@ -1,38 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe RemoteUnfollowsController do
render_views
describe '#create' do
subject { post :create, params: { acct: acct } }
let(:current_user) { Fabricate(:user, account: current_account) }
let(:current_account) { Fabricate(:account) }
let(:remote_account) { 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
sign_in current_user
current_account.follow!(remote_account)
stub_request(:post, 'http://example.com/inbox') { { status: 200 } }
end
context 'when successfully unfollow remote account' do
let(:acct) { "acct:#{remote_account.username}@#{remote_account.domain}" }
it do
is_expected.to render_template :success
expect(current_account.following?(remote_account)).to be false
end
end
context 'when fails to unfollow remote account' do
let(:acct) { "acct:#{remote_account.username + '_test'}@#{remote_account.domain}" }
it do
is_expected.to render_template :error
expect(current_account.following?(remote_account)).to be true
end
end
end
end

View File

@@ -55,18 +55,6 @@ describe StatusesController do
expect(assigns(:status)).to eq status
end
it 'assigns @stream_entry' do
status = Fabricate(:status)
get :show, params: { account_username: status.account.username, id: status.id }
expect(assigns(:stream_entry)).to eq status.stream_entry
end
it 'assigns @type' do
status = Fabricate(:status)
get :show, params: { account_username: status.account.username, id: status.id }
expect(assigns(:type)).to eq 'status'
end
it 'assigns @ancestors for ancestors of the status if it is a reply' do
ancestor = Fabricate(:status)
status = Fabricate(:status, in_reply_to_id: ancestor.id)
@@ -104,7 +92,7 @@ describe StatusesController do
end
it 'assigns @max_descendant_thread_id for the last thread if it is hitting the status limit' do
stub_const 'StatusesController::DESCENDANTS_LIMIT', 1
stub_const 'StatusControllerConcern::DESCENDANTS_LIMIT', 1
status = Fabricate(:status)
child = Fabricate(:status, in_reply_to_id: status.id)
@@ -115,7 +103,7 @@ describe StatusesController do
end
it 'assigns @descendant_threads for threads with :next_status key if they are hitting the depth limit' do
stub_const 'StatusesController::DESCENDANTS_DEPTH_LIMIT', 2
stub_const 'StatusControllerConcern::DESCENDANTS_DEPTH_LIMIT', 2
status = Fabricate(:status)
child0 = Fabricate(:status, in_reply_to_id: status.id)
child1 = Fabricate(:status, in_reply_to_id: child0.id)
@@ -135,10 +123,10 @@ describe StatusesController do
expect(response).to have_http_status(200)
end
it 'renders stream_entries/show' do
it 'renders statuses/show' do
status = Fabricate(:status)
get :show, params: { account_username: status.account.username, id: status.id }
expect(response).to render_template 'stream_entries/show'
expect(response).to render_template 'statuses/show'
end
end
end

View File

@@ -1,95 +0,0 @@
require 'rails_helper'
RSpec.describe StreamEntriesController, type: :controller do
render_views
shared_examples 'before_action' do |route|
context 'when account is not suspended and stream_entry is available' do
it 'assigns instance variables' do
status = Fabricate(:status)
get route, params: { account_username: status.account.username, id: status.stream_entry.id }
expect(assigns(:account)).to eq status.account
expect(assigns(:stream_entry)).to eq status.stream_entry
expect(assigns(:type)).to eq 'status'
end
it 'sets Link headers' do
alice = Fabricate(:account, username: 'alice')
status = Fabricate(:status, account: alice)
get route, params: { account_username: alice.username, id: status.stream_entry.id }
expect(response.headers['Link'].to_s).to eq "<http://test.host/users/alice/updates/#{status.stream_entry.id}.atom>; rel=\"alternate\"; type=\"application/atom+xml\", <https://cb6e6126.ngrok.io/users/alice/statuses/#{status.id}>; rel=\"alternate\"; type=\"application/activity+json\""
end
end
context 'when account is suspended' do
it 'returns http status 410' do
account = Fabricate(:account, suspended: true)
status = Fabricate(:status, account: account)
get route, params: { account_username: account.username, id: status.stream_entry.id }
expect(response).to have_http_status(410)
end
end
context 'when activity is nil' do
it 'raises ActiveRecord::RecordNotFound' do
account = Fabricate(:account)
stream_entry = Fabricate.build(:stream_entry, account: account, activity: nil, activity_type: 'Status')
stream_entry.save!(validate: false)
get route, params: { account_username: account.username, id: stream_entry.id }
expect(response).to have_http_status(404)
end
end
context 'when it is hidden and it is not permitted' do
it 'raises ActiveRecord::RecordNotFound' do
status = Fabricate(:status)
user = Fabricate(:user)
status.account.block!(user.account)
status.stream_entry.update!(hidden: true)
sign_in(user)
get route, params: { account_username: status.account.username, id: status.stream_entry.id }
expect(response).to have_http_status(404)
end
end
end
describe 'GET #show' do
include_examples 'before_action', :show
it 'redirects to status page' do
status = Fabricate(:status)
get :show, params: { account_username: status.account.username, id: status.stream_entry.id }
expect(response).to redirect_to(short_account_status_url(status.account, status))
end
it 'returns http success with Atom' do
status = Fabricate(:status)
get :show, params: { account_username: status.account.username, id: status.stream_entry.id }, format: 'atom'
expect(response).to have_http_status(200)
end
end
describe 'GET #embed' do
include_examples 'before_action', :embed
it 'redirects to new embed page' do
status = Fabricate(:status)
get :embed, params: { account_username: status.account.username, id: status.stream_entry.id }
expect(response).to redirect_to(embed_short_account_status_url(status.account, status))
end
end
end

View File

@@ -1,5 +0,0 @@
Fabricator(:stream_entry) do
account
activity { Fabricate(:status) }
hidden { [true, false].sample }
end

View File

@@ -8,4 +8,4 @@ Access-Control-Allow-Origin: *
Vary: Accept-Encoding,Cookie
Strict-Transport-Security: max-age=31536000; includeSubdomains;
{"subject":"acct:gargron@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron","https:\/\/quitter.no\/index.php\/user\/7477","https:\/\/quitter.no\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]}
{"subject":"acct:gargron@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron","https:\/\/quitter.no\/index.php\/user\/7477","https:\/\/quitter.no\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]}

View File

@@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe Admin::AccountModerationNotesHelper, type: :helper do
include StreamEntriesHelper
include StatusesHelper
describe '#admin_account_link_to' do
context 'account is nil' do

View File

@@ -1,6 +1,6 @@
require 'rails_helper'
RSpec.describe StreamEntriesHelper, type: :helper do
RSpec.describe StatusesHelper, type: :helper do
describe '#display_name' do
it 'uses the display name when it exists' do
account = Account.new(display_name: "Display", username: "Username")
@@ -70,13 +70,13 @@ RSpec.describe StreamEntriesHelper, type: :helper do
end
def set_not_embedded_view
params[:controller] = "not_#{StreamEntriesHelper::EMBEDDED_CONTROLLER}"
params[:action] = "not_#{StreamEntriesHelper::EMBEDDED_ACTION}"
params[:controller] = "not_#{StatusesHelper::EMBEDDED_CONTROLLER}"
params[:action] = "not_#{StatusesHelper::EMBEDDED_ACTION}"
end
def set_embedded_view
params[:controller] = StreamEntriesHelper::EMBEDDED_CONTROLLER
params[:action] = StreamEntriesHelper::EMBEDDED_ACTION
params[:controller] = StatusesHelper::EMBEDDED_CONTROLLER
params[:action] = StatusesHelper::EMBEDDED_ACTION
end
describe '#style_classes' do

View File

@@ -143,12 +143,6 @@ RSpec.describe ActivityPub::TagManager do
expect(subject.uri_to_resource(OStatus::TagManager.instance.uri_for(status), Status)).to eq status
end
it 'returns the local status for OStatus StreamEntry URL' do
status = Fabricate(:status)
stream_entry_url = account_stream_entry_url(status.account, status.stream_entry)
expect(subject.uri_to_resource(stream_entry_url, Status)).to eq status
end
it 'returns the remote status by matching URI without fragment part' do
status = Fabricate(:status, uri: 'https://example.com/123')
expect(subject.uri_to_resource('https://example.com/123#456', Status)).to eq status

View File

@@ -32,11 +32,11 @@ describe LanguageDetector do
expect(result).to eq 'Our website is and also'
end
it 'strips #hashtags from strings before detection' do
string = 'Hey look at all the #animals and #fish'
it 'converts #hashtags back to normal text before detection' do
string = 'Hey look at all the #animals and #FishAndChips'
result = described_class.instance.send(:prepare_text, string)
expect(result).to eq 'Hey look at all the and'
expect(result).to eq 'Hey look at all the animals and fish and chips'
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -14,5 +14,9 @@ describe Sanitize::Config do
it 'keeps ul' do
expect(Sanitize.fragment('<p>Check out:</p><ul><li>Foo</li><li>Bar</li></ul>', subject)).to eq '<p>Check out:</p><ul><li>Foo</li><li>Bar</li></ul>'
end
it 'keep links in lists' do
expect(Sanitize.fragment('<p>Check out:</p><ul><li><a href="https://joinmastodon.org" rel="nofollow noopener" target="_blank">joinmastodon.org</a></li><li>Bar</li></ul>', subject)).to eq '<p>Check out:</p><p><a href="https://joinmastodon.org" rel="nofollow noopener" target="_blank">joinmastodon.org</a><br>Bar</p>'
end
end
end

160
spec/lib/spam_check_spec.rb Normal file
View File

@@ -0,0 +1,160 @@
require 'rails_helper'
RSpec.describe SpamCheck do
let!(:sender) { Fabricate(:account) }
let!(:alice) { Fabricate(:account, username: 'alice') }
let!(:bob) { Fabricate(:account, username: 'bob') }
def status_with_html(text, options = {})
status = PostStatusService.new.call(sender, { text: text }.merge(options))
status.update_columns(text: Formatter.instance.format(status), local: false)
status
end
describe '#hashable_text' do
it 'removes mentions from HTML for remote statuses' do
status = status_with_html('@alice Hello')
expect(described_class.new(status).hashable_text).to eq 'hello'
end
it 'removes mentions from text for local statuses' do
status = PostStatusService.new.call(alice, text: "Hey @#{sender.username}, how are you?")
expect(described_class.new(status).hashable_text).to eq 'hey , how are you?'
end
end
describe '#insufficient_data?' do
it 'returns true when there is no text' do
status = status_with_html('@alice')
expect(described_class.new(status).insufficient_data?).to be true
end
it 'returns false when there is text' do
status = status_with_html('@alice h')
expect(described_class.new(status).insufficient_data?).to be false
end
end
describe '#digest' do
it 'returns a string' do
status = status_with_html('@alice Hello world')
expect(described_class.new(status).digest).to be_a String
end
end
describe '#spam?' do
it 'returns false for a unique status' do
status = status_with_html('@alice Hello')
expect(described_class.new(status).spam?).to be false
end
it 'returns false for different statuses to the same recipient' do
status1 = status_with_html('@alice Hello')
described_class.new(status1).remember!
status2 = status_with_html('@alice Are you available to talk?')
expect(described_class.new(status2).spam?).to be false
end
it 'returns false for statuses with different content warnings' do
status1 = status_with_html('@alice Are you available to talk?')
described_class.new(status1).remember!
status2 = status_with_html('@alice Are you available to talk?', spoiler_text: 'This is a completely different matter than what I was talking about previously, I swear!')
expect(described_class.new(status2).spam?).to be false
end
it 'returns false for different statuses to different recipients' do
status1 = status_with_html('@alice How is it going?')
described_class.new(status1).remember!
status2 = status_with_html('@bob Are you okay?')
expect(described_class.new(status2).spam?).to be false
end
it 'returns false for very short different statuses to different recipients' do
status1 = status_with_html('@alice 🙄')
described_class.new(status1).remember!
status2 = status_with_html('@bob Huh?')
expect(described_class.new(status2).spam?).to be false
end
it 'returns false for statuses with no text' do
status1 = status_with_html('@alice')
described_class.new(status1).remember!
status2 = status_with_html('@bob')
expect(described_class.new(status2).spam?).to be false
end
it 'returns true for duplicate statuses to the same recipient' do
status1 = status_with_html('@alice Hello')
described_class.new(status1).remember!
status2 = status_with_html('@alice Hello')
expect(described_class.new(status2).spam?).to be true
end
it 'returns true for duplicate statuses to different recipients' do
status1 = status_with_html('@alice Hello')
described_class.new(status1).remember!
status2 = status_with_html('@bob Hello')
expect(described_class.new(status2).spam?).to be true
end
it 'returns true for nearly identical statuses with random numbers' do
source_text = 'Sodium, atomic number 11, was first isolated by Humphry Davy in 1807. A chemical component of salt, he named it Na in honor of the saltiest region on earth, North America.'
status1 = status_with_html('@alice ' + source_text + ' 1234')
described_class.new(status1).remember!
status2 = status_with_html('@bob ' + source_text + ' 9568')
expect(described_class.new(status2).spam?).to be true
end
end
describe '#skip?' do
it 'returns true when the sender is already silenced' do
status = status_with_html('@alice Hello')
sender.silence!
expect(described_class.new(status).skip?).to be true
end
it 'returns true when the mentioned person follows the sender' do
status = status_with_html('@alice Hello')
alice.follow!(sender)
expect(described_class.new(status).skip?).to be true
end
it 'returns false when even one mentioned person doesn\'t follow the sender' do
status = status_with_html('@alice @bob Hello')
alice.follow!(sender)
expect(described_class.new(status).skip?).to be false
end
it 'returns true when the sender is replying to a status that mentions the sender' do
parent = PostStatusService.new.call(alice, text: "Hey @#{sender.username}, how are you?")
status = status_with_html('@alice @bob Hello', thread: parent)
expect(described_class.new(status).skip?).to be true
end
end
describe '#remember!' do
pending
end
describe '#flag!' do
let!(:status1) { status_with_html('@alice General Kenobi you are a bold one') }
let!(:status2) { status_with_html('@alice @bob General Kenobi, you are a bold one') }
before do
described_class.new(status1).remember!
described_class.new(status2).flag!
end
it 'silences the account' do
expect(sender.silenced?).to be true
end
it 'creates a report about the account' do
expect(sender.targeted_reports.unresolved.count).to eq 1
end
it 'attaches both matching statuses to the report' do
expect(sender.targeted_reports.first.status_ids).to include(status1.id, status2.id)
end
end
end

View File

@@ -25,15 +25,6 @@ describe StatusFinder do
end
end
context 'with a stream entry url' do
let(:stream_entry) { Fabricate(:stream_entry) }
let(:url) { account_stream_entry_url(stream_entry.account, stream_entry) }
it 'finds the stream entry' do
expect(subject.status).to eq(stream_entry.status)
end
end
context 'with a remote url even if id exists on local' do
let(:status) { Fabricate(:status) }
let(:url) { "https://example.com/users/test/statuses/#{status.id}" }

View File

@@ -119,46 +119,4 @@ RSpec.describe TagManager do
expect(TagManager.instance.same_acct?('username', 'incorrect@Cb6E6126.nGrOk.Io')).to eq false
end
end
describe '#url_for' do
let(:alice) { Fabricate(:account, username: 'alice') }
subject { TagManager.instance.url_for(target) }
context 'activity object' do
let(:target) { Fabricate(:status, account: alice, reblog: Fabricate(:status)).stream_entry }
it 'returns the unique tag for status' do
expect(target.object_type).to eq :activity
is_expected.to eq "https://cb6e6126.ngrok.io/@alice/#{target.id}"
end
end
context 'comment object' do
let(:target) { Fabricate(:status, account: alice, reply: true) }
it 'returns the unique tag for status' do
expect(target.object_type).to eq :comment
is_expected.to eq "https://cb6e6126.ngrok.io/@alice/#{target.id}"
end
end
context 'note object' do
let(:target) { Fabricate(:status, account: alice, reply: false, thread: nil) }
it 'returns the unique tag for status' do
expect(target.object_type).to eq :note
is_expected.to eq "https://cb6e6126.ngrok.io/@alice/#{target.id}"
end
end
context 'person object' do
let(:target) { alice }
it 'returns the URL for account' do
expect(target.object_type).to eq :person
is_expected.to eq 'https://cb6e6126.ngrok.io/@alice'
end
end
end
end

View File

@@ -450,7 +450,7 @@ RSpec.describe Account, type: :model do
describe '.domains' do
it 'returns domains' do
Fabricate(:account, domain: 'domain')
expect(Account.domains).to match_array(['domain'])
expect(Account.remote.domains).to match_array(['domain'])
end
end
@@ -665,7 +665,7 @@ RSpec.describe Account, type: :model do
{ username: 'b', domain: 'b' },
].map(&method(:Fabricate).curry(2).call(:account))
expect(Account.alphabetic).to eq matches
expect(Account.where('id > 0').alphabetic).to eq matches
end
end
@@ -732,7 +732,7 @@ RSpec.describe Account, type: :model do
2.times { Fabricate(:account, domain: 'example.com') }
Fabricate(:account, domain: 'example2.com')
results = Account.by_domain_accounts
results = Account.where('id > 0').by_domain_accounts
expect(results.length).to eq 2
expect(results.first.domain).to eq 'example.com'
expect(results.first.accounts_count).to eq 2
@@ -745,7 +745,7 @@ RSpec.describe Account, type: :model do
it 'returns an array of accounts who do not have a domain' do
account_1 = Fabricate(:account, domain: nil)
account_2 = Fabricate(:account, domain: 'example.com')
expect(Account.local).to match_array([account_1])
expect(Account.where('id > 0').local).to match_array([account_1])
end
end
@@ -756,14 +756,14 @@ RSpec.describe Account, type: :model do
matches[index] = Fabricate(:account, domain: matches[index])
end
expect(Account.partitioned).to match_array(matches)
expect(Account.where('id > 0').partitioned).to match_array(matches)
end
end
describe 'recent' do
it 'returns a relation of accounts sorted by recent creation' do
matches = 2.times.map { Fabricate(:account) }
expect(Account.recent).to match_array(matches)
expect(Account.where('id > 0').recent).to match_array(matches)
end
end

View File

@@ -1,63 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Streamable do
class Parent
def title; end
def target; end
def thread; end
def self.has_one(*); end
def self.after_create; end
end
class Child < Parent
include Streamable
end
child = Child.new
describe '#title' do
it 'calls Parent#title' do
expect_any_instance_of(Parent).to receive(:title)
child.title
end
end
describe '#content' do
it 'calls #title' do
expect_any_instance_of(Parent).to receive(:title)
child.content
end
end
describe '#target' do
it 'calls Parent#target' do
expect_any_instance_of(Parent).to receive(:target)
child.target
end
end
describe '#object_type' do
it 'returns :activity' do
expect(child.object_type).to eq :activity
end
end
describe '#thread' do
it 'calls Parent#thread' do
expect_any_instance_of(Parent).to receive(:thread)
child.thread
end
end
describe '#hidden?' do
it 'returns false' do
expect(child.hidden?).to be false
end
end
end

View File

@@ -1,143 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe RemoteProfile do
let(:remote_profile) { RemoteProfile.new(body) }
let(:body) do
<<-XML
<feed xmlns="http://www.w3.org/2005/Atom">
<author>John</author>
XML
end
describe '.initialize' do
it 'calls Nokogiri::XML.parse' do
expect(Nokogiri::XML).to receive(:parse).with(body, nil, 'utf-8')
RemoteProfile.new(body)
end
it 'sets document' do
remote_profile = RemoteProfile.new(body)
expect(remote_profile).not_to be nil
end
end
describe '#root' do
let(:document) { remote_profile.document }
it 'callse document.at_xpath' do
expect(document).to receive(:at_xpath).with(
'/atom:feed|/atom:entry',
atom: OStatus::TagManager::XMLNS
)
remote_profile.root
end
end
describe '#author' do
let(:root) { remote_profile.root }
it 'calls root.at_xpath' do
expect(root).to receive(:at_xpath).with(
'./atom:author|./dfrn:owner',
atom: OStatus::TagManager::XMLNS,
dfrn: OStatus::TagManager::DFRN_XMLNS
)
remote_profile.author
end
end
describe '#hub_link' do
let(:root) { remote_profile.root }
it 'calls #link_href_from_xml' do
expect(remote_profile).to receive(:link_href_from_xml).with(root, 'hub')
remote_profile.hub_link
end
end
describe '#display_name' do
let(:author) { remote_profile.author }
it 'calls author.at_xpath.content' do
expect(author).to receive_message_chain(:at_xpath, :content).with(
'./poco:displayName',
poco: OStatus::TagManager::POCO_XMLNS
).with(no_args)
remote_profile.display_name
end
end
describe '#note' do
let(:author) { remote_profile.author }
it 'calls author.at_xpath.content' do
expect(author).to receive_message_chain(:at_xpath, :content).with(
'./atom:summary|./poco:note',
atom: OStatus::TagManager::XMLNS,
poco: OStatus::TagManager::POCO_XMLNS
).with(no_args)
remote_profile.note
end
end
describe '#scope' do
let(:author) { remote_profile.author }
it 'calls author.at_xpath.content' do
expect(author).to receive_message_chain(:at_xpath, :content).with(
'./mastodon:scope',
mastodon: OStatus::TagManager::MTDN_XMLNS
).with(no_args)
remote_profile.scope
end
end
describe '#avatar' do
let(:author) { remote_profile.author }
it 'calls #link_href_from_xml' do
expect(remote_profile).to receive(:link_href_from_xml).with(author, 'avatar')
remote_profile.avatar
end
end
describe '#header' do
let(:author) { remote_profile.author }
it 'calls #link_href_from_xml' do
expect(remote_profile).to receive(:link_href_from_xml).with(author, 'header')
remote_profile.header
end
end
describe '#locked?' do
before do
allow(remote_profile).to receive(:scope).and_return(scope)
end
subject { remote_profile.locked? }
context 'scope is private' do
let(:scope) { 'private' }
it 'returns true' do
is_expected.to be true
end
end
context 'scope is not private' do
let(:scope) { 'public' }
it 'returns false' do
is_expected.to be false
end
end
end
end

View File

@@ -1,192 +0,0 @@
require 'rails_helper'
RSpec.describe StreamEntry, type: :model do
let(:alice) { Fabricate(:account, username: 'alice') }
let(:bob) { Fabricate(:account, username: 'bob') }
let(:status) { Fabricate(:status, account: alice) }
let(:reblog) { Fabricate(:status, account: bob, reblog: status) }
let(:reply) { Fabricate(:status, account: bob, thread: status) }
let(:stream_entry) { Fabricate(:stream_entry, activity: activity) }
let(:activity) { reblog }
describe '#object_type' do
before do
allow(stream_entry).to receive(:orphaned?).and_return(orphaned)
allow(stream_entry).to receive(:targeted?).and_return(targeted)
end
subject { stream_entry.object_type }
context 'orphaned? is true' do
let(:orphaned) { true }
let(:targeted) { false }
it 'returns :activity' do
is_expected.to be :activity
end
end
context 'targeted? is true' do
let(:orphaned) { false }
let(:targeted) { true }
it 'returns :activity' do
is_expected.to be :activity
end
end
context 'orphaned? and targeted? are false' do
let(:orphaned) { false }
let(:targeted) { false }
context 'activity is reblog' do
let(:activity) { reblog }
it 'returns :note' do
is_expected.to be :note
end
end
context 'activity is reply' do
let(:activity) { reply }
it 'returns :comment' do
is_expected.to be :comment
end
end
end
end
describe '#verb' do
before do
allow(stream_entry).to receive(:orphaned?).and_return(orphaned)
end
subject { stream_entry.verb }
context 'orphaned? is true' do
let(:orphaned) { true }
it 'returns :delete' do
is_expected.to be :delete
end
end
context 'orphaned? is false' do
let(:orphaned) { false }
context 'activity is reblog' do
let(:activity) { reblog }
it 'returns :share' do
is_expected.to be :share
end
end
context 'activity is reply' do
let(:activity) { reply }
it 'returns :post' do
is_expected.to be :post
end
end
end
end
describe '#mentions' do
before do
allow(stream_entry).to receive(:orphaned?).and_return(orphaned)
end
subject { stream_entry.mentions }
context 'orphaned? is true' do
let(:orphaned) { true }
it 'returns []' do
is_expected.to eq []
end
end
context 'orphaned? is false' do
before do
reblog.mentions << Fabricate(:mention, account: alice)
reblog.mentions << Fabricate(:mention, account: bob)
end
let(:orphaned) { false }
it 'returns [Account] includes alice and bob' do
is_expected.to eq [alice, bob]
end
end
end
describe '#targeted?' do
it 'returns true for a reblog' do
expect(reblog.stream_entry.targeted?).to be true
end
it 'returns false otherwise' do
expect(status.stream_entry.targeted?).to be false
end
end
describe '#threaded?' do
it 'returns true for a reply' do
expect(reply.stream_entry.threaded?).to be true
end
it 'returns false otherwise' do
expect(status.stream_entry.threaded?).to be false
end
end
describe 'delegated methods' do
context 'with a nil status' do
subject { described_class.new(status: nil) }
it 'returns nil for target' do
expect(subject.target).to be_nil
end
it 'returns nil for title' do
expect(subject.title).to be_nil
end
it 'returns nil for content' do
expect(subject.content).to be_nil
end
it 'returns nil for thread' do
expect(subject.thread).to be_nil
end
end
context 'with a real status' do
let(:original) { Fabricate(:status, text: 'Test status') }
let(:status) { Fabricate(:status, reblog: original, thread: original) }
subject { described_class.new(status: status) }
it 'delegates target' do
expect(status.target).not_to be_nil
expect(subject.target).to eq(status.target)
end
it 'delegates title' do
expect(status.title).not_to be_nil
expect(subject.title).to eq(status.title)
end
it 'delegates content' do
expect(status.content).not_to be_nil
expect(subject.content).to eq(status.content)
end
it 'delegates thread' do
expect(status.thread).not_to be_nil
expect(subject.thread).to eq(status.thread)
end
end
end
end

View File

@@ -31,7 +31,43 @@ RSpec.describe Tag, type: :model do
end
it 'matches #' do
expect(subject.match('this is #')).to_not be_nil
expect(subject.match('this is #').to_s).to eq ' #'
end
it 'matches digits at the start' do
expect(subject.match('hello #3d').to_s).to eq ' #3d'
end
it 'matches digits in the middle' do
expect(subject.match('hello #l33ts35k').to_s).to eq ' #l33ts35k'
end
it 'matches digits at the end' do
expect(subject.match('hello #world2016').to_s).to eq ' #world2016'
end
it 'matches underscores at the beginning' do
expect(subject.match('hello #_test').to_s).to eq ' #_test'
end
it 'matches underscores at the end' do
expect(subject.match('hello #test_').to_s).to eq ' #test_'
end
it 'matches underscores in the middle' do
expect(subject.match('hello #one_two_three').to_s).to eq ' #one_two_three'
end
it 'matches middle dots' do
expect(subject.match('hello #one·two·three').to_s).to eq ' #one·two·three'
end
it 'does not match middle dots at the start' do
expect(subject.match('hello #·one·two·three')).to be_nil
end
it 'does not match middle dots at the end' do
expect(subject.match('hello #one·two·three·').to_s).to eq ' #one·two·three'
end
end

View File

@@ -11,16 +11,16 @@ describe 'Link headers' do
end
it 'contains webfinger url in link header' do
link_header = link_header_with_type('application/xrd+xml')
link_header = link_header_with_type('application/jrd+json')
expect(link_header.href).to match 'http://www.example.com/.well-known/webfinger?resource=acct%3Atest%40cb6e6126.ngrok.io'
expect(link_header.attr_pairs.first).to eq %w(rel lrdd)
end
it 'contains atom url in link header' do
link_header = link_header_with_type('application/atom+xml')
it 'contains activitypub url in link header' do
link_header = link_header_with_type('application/activity+json')
expect(link_header.href).to eq 'http://www.example.com/users/test.atom'
expect(link_header.href).to eq 'https://cb6e6126.ngrok.io/users/test'
expect(link_header.attr_pairs.first).to eq %w(rel alternate)
end

View File

@@ -38,13 +38,6 @@ RSpec.describe AuthorizeFollowService, type: :service do
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

View File

@@ -49,19 +49,6 @@ RSpec.describe BatchedRemoveStatusService, type: :service 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

View File

@@ -28,13 +28,6 @@ RSpec.describe BlockService, type: :service do
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

View File

@@ -30,13 +30,6 @@ RSpec.describe FavouriteService, type: :service do
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

View File

@@ -4,6 +4,7 @@ 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
@@ -36,36 +37,6 @@ RSpec.describe FetchRemoteAccountService, type: :service do
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
@@ -75,15 +46,5 @@ RSpec.describe FetchRemoteAccountService, type: :service do
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

View File

@@ -1,73 +1,80 @@
require 'rails_helper'
RSpec.describe FetchAtomService, type: :service do
RSpec.describe FetchResourceService, type: :service do
describe '#call' do
let(:url) { 'http://example.com' }
subject { FetchAtomService.new.call(url) }
context 'url is blank' do
subject { described_class.new.call(url) }
context 'with blank url' do
let(:url) { '' }
it { is_expected.to be_nil }
end
context 'request failed' do
context 'when request fails' do
before do
WebMock.stub_request(:get, url).to_return(status: 500, body: '', headers: {})
stub_request(:get, url).to_return(status: 500, body: '', headers: {})
end
it { is_expected.to be_nil }
end
context 'raise OpenSSL::SSL::SSLError' do
context 'when OpenSSL::SSL::SSLError is raised' do
before do
allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(OpenSSL::SSL::SSLError)
allow(Request).to receive_message_chain(:new, :add_headers, :on_behalf_of, :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
it { is_expected.to be_nil }
end
context 'raise HTTP::ConnectionError' do
context 'when HTTP::ConnectionError is raised' do
before do
allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(HTTP::ConnectionError)
allow(Request).to receive_message_chain(:new, :add_headers, :on_behalf_of, :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
it { is_expected.to be_nil }
end
context 'response success' do
context 'when request succeeds' do
let(:body) { '' }
let(:headers) { { 'Content-Type' => content_type } }
let(:json) {
{ id: 1,
let(:content_type) { 'application/json' }
let(:headers) do
{ 'Content-Type' => content_type }
end
let(:json) do
{
id: 1,
'@context': ActivityPub::TagManager::CONTEXT,
type: 'Note',
}.to_json
}
end
before do
WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers)
stub_request(:get, url).to_return(status: 200, body: body, headers: headers)
end
context 'content type is application/atom+xml' do
it 'signs request' do
subject
expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.uri_for(Account.representative) + '#main-key')}"/ })).to have_been_made
end
context 'when content type is application/atom+xml' do
let(:content_type) { 'application/atom+xml' }
it { is_expected.to eq [url, { :prefetched_body => "" }, :ostatus] }
it { is_expected.to eq nil }
end
context 'content_type is activity+json' do
context 'when 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
context 'when content type is ld+json with profile' do
let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }
let(:body) { json }
@@ -75,17 +82,17 @@ RSpec.describe FetchAtomService, type: :service do
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' })
stub_request(:get, url).to_return(status: 200, body: body, headers: headers)
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
context 'when link header is present' 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
context 'when 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>' }

View File

@@ -96,74 +96,6 @@ RSpec.describe FollowService, type: :service do
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 }

View File

@@ -3,7 +3,11 @@ 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) }
let!(:eve) { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false, protocol: :activitypub, inbox_url: 'https://example.com/inbox') }
before do
stub_request(:post, "https://example.com/inbox").to_return(status: 200)
end
context 'import old-style list of muted users' do
subject { ImportService.new }
@@ -95,7 +99,8 @@ RSpec.describe ImportService, type: :service do
let(:import) { Import.create(account: account, type: 'following', data: csv) }
it 'follows the listed accounts, including boosts' do
subject.call(import)
expect(account.following.count).to eq 2
expect(account.following.count).to eq 1
expect(account.follow_requests.count).to eq 1
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
end
end
@@ -106,7 +111,8 @@ RSpec.describe ImportService, type: :service do
it 'follows the listed accounts, including notifications' do
account.follow!(bob, reblogs: false)
subject.call(import)
expect(account.following.count).to eq 2
expect(account.following.count).to eq 1
expect(account.follow_requests.count).to eq 1
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
end
end
@@ -117,7 +123,8 @@ RSpec.describe ImportService, type: :service do
it 'mutes the listed accounts, including notifications' do
account.follow!(bob, reblogs: false)
subject.call(import)
expect(account.following.count).to eq 2
expect(account.following.count).to eq 1
expect(account.follow_requests.count).to eq 1
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
end
end
@@ -136,9 +143,10 @@ RSpec.describe ImportService, type: :service do
let(:import) { Import.create(account: account, type: 'following', data: csv) }
it 'follows the listed accounts, respecting boosts' do
subject.call(import)
expect(account.following.count).to eq 2
expect(account.following.count).to eq 1
expect(account.follow_requests.count).to eq 1
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
expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
end
end
@@ -148,9 +156,10 @@ RSpec.describe ImportService, type: :service do
it 'mutes the listed accounts, respecting notifications' do
account.follow!(bob, reblogs: true)
subject.call(import)
expect(account.following.count).to eq 2
expect(account.following.count).to eq 1
expect(account.follow_requests.count).to eq 1
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
expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
end
end
@@ -160,9 +169,10 @@ RSpec.describe ImportService, type: :service do
it 'mutes the listed accounts, respecting notifications' do
account.follow!(bob, reblogs: true)
subject.call(import)
expect(account.following.count).to eq 2
expect(account.following.count).to eq 1
expect(account.follow_requests.count).to eq 1
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
expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
end
end
end

View File

@@ -144,7 +144,6 @@ RSpec.describe PostStatusService, type: :service do
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)
@@ -152,7 +151,6 @@ RSpec.describe PostStatusService, type: :service do
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

View File

@@ -1,252 +0,0 @@
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', 'mastodon.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:mastodon="http://mastodon.social/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:mastodon="http://mastodon.social/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:mastodon="http://mastodon.social/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:mastodon="http://mastodon.social/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:mastodon="http://mastodon.social/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

View File

@@ -1,151 +0,0 @@
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

View File

@@ -15,12 +15,8 @@ RSpec.describe ProcessMentionsService, type: :service do
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
it 'does not create a mention' do
expect(remote_user.mentions.where(status: status).count).to eq 0
end
end

View File

@@ -1,71 +0,0 @@
# 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

View File

@@ -1,46 +0,0 @@
# 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

View File

@@ -46,10 +46,6 @@ RSpec.describe ReblogService, type: :service do
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

View File

@@ -38,13 +38,6 @@ RSpec.describe RejectFollowService, type: :service do
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

View File

@@ -32,23 +32,10 @@ RSpec.describe RemoveStatusService, type: :service 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

View File

@@ -6,19 +6,13 @@ RSpec.describe ResolveAccountService, type: :service do
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:gargron@quitter.no").to_return(request_fixture('webfinger.txt'))
stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:gargron@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'))
stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no").to_return(status: 404)
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 'raises error if no such user can be resolved via webfinger' do
@@ -29,74 +23,7 @@ RSpec.describe ResolveAccountService, type: :service 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: 'gargron', domain: 'quitter.no')
returned_account = subject.call('gargron@quitter.no')
expect(old_account.id).to eq returned_account.id
end
it 'returns a new remote account' do
account = subject.call('gargron@quitter.no')
expect(account.username).to eq 'gargron'
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('gargron@redirected.com')
expect(account.username).to eq 'gargron'
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')
@@ -124,13 +51,19 @@ RSpec.describe ResolveAccountService, type: :service do
it 'processes one remote account at a time using locks' do
wait_for_start = true
fail_occurred = false
return_values = []
return_values = Concurrent::Array.new
# Preload classes that throw circular dependency errors in threads
Account
TagManager
DomainBlock
threads = Array.new(5) do
Thread.new do
true while wait_for_start
begin
return_values << described_class.new.call('foo@localdomain.com')
return_values << described_class.new.call('foo@ap.example.com')
rescue ActiveRecord::RecordNotUnique
fail_occurred = true
end

View File

@@ -6,48 +6,14 @@ 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'
it 'returns nil when there is no resource url' do
url = 'http://example.com/missing-resource'
service = double
allow(FetchAtomService).to receive(:new).and_return service
allow(FetchResourceService).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)
expect(subject.call(url)).to be_nil
end
end
end

View File

@@ -1,7 +0,0 @@
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

View File

@@ -1,43 +0,0 @@
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 Mastodon::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 Mastodon::UnexpectedResponseError
end
end

View File

@@ -27,14 +27,13 @@ RSpec.describe SuspendAccountService, type: :service do
[
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])
}.from([1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0])
end
it 'sends a delete actor activity to all known inboxes' do
@@ -70,14 +69,13 @@ RSpec.describe SuspendAccountService, type: :service do
[
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])
}.from([1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0])
end
it 'sends a reject follow to follwer inboxes' do

View File

@@ -30,13 +30,6 @@ RSpec.describe UnblockService, type: :service do
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

View File

@@ -30,13 +30,6 @@ RSpec.describe UnfollowService, type: :service do
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

View File

@@ -1,37 +0,0 @@
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

View File

@@ -1,84 +0,0 @@
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 ' '
end
it 'sets note' do
expect(remote_account.reload.note).to eq 'Software engineer, free time musician and 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: ' ', note: 'Software engineer, free time musician and 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 ' '
end
it 'sets note' do
expect(remote_account.reload.note).to eq 'Software engineer, free time musician and 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 ' '
end
it 'sets note' do
expect(remote_account.reload.note).to eq 'Software engineer, free time musician and 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

View File

@@ -27,6 +27,7 @@ RSpec.configure do |config|
end
config.before :suite do
Rails.application.load_seed
Chewy.strategy(:bypass)
end

View File

@@ -2,10 +2,9 @@
require 'rails_helper'
describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true do
describe 'statuses/show.html.haml', without_verify_partial_doubles: true do
before do
double(:api_oembed_url => '')
double(:account_stream_entry_url => '')
allow(view).to receive(:show_landing_strip?).and_return(true)
allow(view).to receive(:site_title).and_return('example site')
allow(view).to receive(:site_hostname).and_return('example.com')
@@ -23,9 +22,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d
reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice')
assign(:status, status)
assign(:stream_entry, status.stream_entry)
assign(:account, alice)
assign(:type, status.stream_entry.activity_type.downcase)
assign(:descendant_threads, [])
render
@@ -46,11 +43,9 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d
comment = Fabricate(:status, account: carl, thread: reply, text: 'Hello Bob')
assign(:status, reply)
assign(:stream_entry, reply.stream_entry)
assign(:account, alice)
assign(:type, reply.stream_entry.activity_type.downcase)
assign(:ancestors, reply.stream_entry.activity.ancestors(1, bob))
assign(:descendant_threads, [{ statuses: reply.stream_entry.activity.descendants(1) }])
assign(:ancestors, reply.ancestors(1, bob))
assign(:descendant_threads, [{ statuses: reply.descendants(1) }])
render
@@ -71,9 +66,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d
status = Fabricate(:status, account: alice, text: 'Hello World')
assign(:status, status)
assign(:stream_entry, status.stream_entry)
assign(:account, alice)
assign(:type, status.stream_entry.activity_type.downcase)
assign(:descendant_threads, [])
render

View File

@@ -1,59 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe AfterRemoteFollowRequestWorker do
subject { described_class.new }
let(:follow_request) { Fabricate(:follow_request) }
describe 'perform' do
context 'when the follow_request does not exist' do
it 'catches a raise and returns true' do
allow(FollowService).to receive(:new)
result = subject.perform('aaa')
expect(result).to eq(true)
expect(FollowService).not_to have_received(:new)
end
end
context 'when the account cannot be updated' do
it 'returns nil and does not call service when account is nil' do
allow(FollowService).to receive(:new)
service = double(call: nil)
allow(FetchRemoteAccountService).to receive(:new).and_return(service)
result = subject.perform(follow_request.id)
expect(result).to be_nil
expect(FollowService).not_to have_received(:new)
end
it 'returns nil and does not call service when account is locked' do
allow(FollowService).to receive(:new)
service = double(call: double(locked?: true))
allow(FetchRemoteAccountService).to receive(:new).and_return(service)
result = subject.perform(follow_request.id)
expect(result).to be_nil
expect(FollowService).not_to have_received(:new)
end
end
context 'when the account is updated' do
it 'calls the follow service and destroys the follow' do
follow_service = double(call: nil)
allow(FollowService).to receive(:new).and_return(follow_service)
account = Fabricate(:account, locked: false)
service = double(call: account)
allow(FetchRemoteAccountService).to receive(:new).and_return(service)
result = subject.perform(follow_request.id)
expect(result).to be_nil
expect(follow_service).to have_received(:call).with(follow_request.account, account.acct)
expect { follow_request.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
end

View File

@@ -1,59 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe AfterRemoteFollowWorker do
subject { described_class.new }
let(:follow) { Fabricate(:follow) }
describe 'perform' do
context 'when the follow does not exist' do
it 'catches a raise and returns true' do
allow(FollowService).to receive(:new)
result = subject.perform('aaa')
expect(result).to eq(true)
expect(FollowService).not_to have_received(:new)
end
end
context 'when the account cannot be updated' do
it 'returns nil and does not call service when account is nil' do
allow(FollowService).to receive(:new)
service = double(call: nil)
allow(FetchRemoteAccountService).to receive(:new).and_return(service)
result = subject.perform(follow.id)
expect(result).to be_nil
expect(FollowService).not_to have_received(:new)
end
it 'returns nil and does not call service when account is not locked' do
allow(FollowService).to receive(:new)
service = double(call: double(locked?: false))
allow(FetchRemoteAccountService).to receive(:new).and_return(service)
result = subject.perform(follow.id)
expect(result).to be_nil
expect(FollowService).not_to have_received(:new)
end
end
context 'when the account is updated' do
it 'calls the follow service and destroys the follow' do
follow_service = double(call: nil)
allow(FollowService).to receive(:new).and_return(follow_service)
account = Fabricate(:account, locked: true)
service = double(call: account)
allow(FetchRemoteAccountService).to receive(:new).and_return(service)
result = subject.perform(follow.id)
expect(result).to be_nil
expect(follow_service).to have_received(:call).with(follow.account, account.acct)
expect { follow.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
end

View File

@@ -1,88 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe Pubsubhubbub::ConfirmationWorker do
include RoutingHelper
subject { described_class.new }
let!(:alice) { Fabricate(:account, username: 'alice') }
let!(:subscription) { Fabricate(:subscription, account: alice, callback_url: 'http://example.com/api', confirmed: false, expires_at: 3.days.from_now, secret: nil) }
describe 'perform' do
describe 'with subscribe mode' do
it 'confirms and updates subscription when challenge matches' do
stub_random_value
stub_request(:get, url_for_mode('subscribe'))
.with(headers: http_headers)
.to_return(status: 200, body: challenge_value, headers: {})
seconds = 10.days.seconds.to_i
subject.perform(subscription.id, 'subscribe', 'asdf', seconds)
subscription.reload
expect(subscription.secret).to eq 'asdf'
expect(subscription.confirmed).to eq true
expect(subscription.expires_at).to be_within(5).of(10.days.from_now)
end
it 'does not update subscription when challenge does not match' do
stub_random_value
stub_request(:get, url_for_mode('subscribe'))
.with(headers: http_headers)
.to_return(status: 200, body: 'wrong value', headers: {})
seconds = 10.days.seconds.to_i
subject.perform(subscription.id, 'subscribe', 'asdf', seconds)
subscription.reload
expect(subscription.secret).to be_blank
expect(subscription.confirmed).to eq false
expect(subscription.expires_at).to be_within(5).of(3.days.from_now)
end
end
describe 'with unsubscribe mode' do
it 'confirms and destroys subscription when challenge matches' do
stub_random_value
stub_request(:get, url_for_mode('unsubscribe'))
.with(headers: http_headers)
.to_return(status: 200, body: challenge_value, headers: {})
seconds = 10.days.seconds.to_i
subject.perform(subscription.id, 'unsubscribe', 'asdf', seconds)
expect { subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'does not destroy subscription when challenge does not match' do
stub_random_value
stub_request(:get, url_for_mode('unsubscribe'))
.with(headers: http_headers)
.to_return(status: 200, body: 'wrong value', headers: {})
seconds = 10.days.seconds.to_i
subject.perform(subscription.id, 'unsubscribe', 'asdf', seconds)
expect { subscription.reload }.not_to raise_error
end
end
end
def url_for_mode(mode)
"http://example.com/api?hub.challenge=#{challenge_value}&hub.lease_seconds=863999&hub.mode=#{mode}&hub.topic=https://#{Rails.configuration.x.local_domain}/users/alice.atom"
end
def stub_random_value
allow(SecureRandom).to receive(:hex).and_return(challenge_value)
end
def challenge_value
'1a2s3d4f'
end
def http_headers
{ 'Connection' => 'close', 'Host' => 'example.com' }
end
end

View File

@@ -1,68 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe Pubsubhubbub::DeliveryWorker do
include RoutingHelper
subject { described_class.new }
let(:payload) { 'test' }
describe 'perform' do
it 'raises when subscription does not exist' do
expect { subject.perform 123, payload }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'does not attempt to deliver when domain blocked' do
_domain_block = Fabricate(:domain_block, domain: 'example.com', severity: :suspend)
subscription = Fabricate(:subscription, callback_url: 'https://example.com/api', last_successful_delivery_at: 2.days.ago)
subject.perform(subscription.id, payload)
expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(2.days.ago)
end
it 'raises when request fails' do
subscription = Fabricate(:subscription)
stub_request_to_respond_with(subscription, 500)
expect { subject.perform(subscription.id, payload) }.to raise_error Mastodon::UnexpectedResponseError
end
it 'updates subscriptions when delivery succeeds' do
subscription = Fabricate(:subscription)
stub_request_to_respond_with(subscription, 200)
subject.perform(subscription.id, payload)
expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(Time.now.utc)
end
it 'updates subscription without a secret when delivery succeeds' do
subscription = Fabricate(:subscription, secret: nil)
stub_request_to_respond_with(subscription, 200)
subject.perform(subscription.id, payload)
expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(Time.now.utc)
end
def stub_request_to_respond_with(subscription, code)
stub_request(:post, 'http://example.com/callback')
.with(body: payload, headers: expected_headers(subscription))
.to_return(status: code, body: '', headers: {})
end
def expected_headers(subscription)
{
'Connection' => 'close',
'Content-Type' => 'application/atom+xml',
'Host' => 'example.com',
'Link' => "<https://#{Rails.configuration.x.local_domain}/api/push>; rel=\"hub\", <https://#{Rails.configuration.x.local_domain}/users/#{subscription.account.username}.atom>; rel=\"self\"",
}.tap do |basic|
known_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), subscription.secret.to_s, payload)
basic.merge('X-Hub-Signature' => "sha1=#{known_digest}") if subscription.secret?
end
end
end
end

View File

@@ -1,46 +0,0 @@
require 'rails_helper'
describe Pubsubhubbub::DistributionWorker do
subject { Pubsubhubbub::DistributionWorker.new }
let!(:alice) { Fabricate(:account, username: 'alice') }
let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example2.com') }
let!(:anonymous_subscription) { Fabricate(:subscription, account: alice, callback_url: 'http://example1.com', confirmed: true, lease_seconds: 3600) }
let!(:subscription_with_follower) { Fabricate(:subscription, account: alice, callback_url: 'http://example2.com', confirmed: true, lease_seconds: 3600) }
before do
bob.follow!(alice)
end
describe 'with public status' do
let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :public) }
it 'delivers payload to all subscriptions' do
allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk)
subject.perform(status.stream_entry.id)
expect(Pubsubhubbub::DeliveryWorker).to have_received(:push_bulk).with([anonymous_subscription.id, subscription_with_follower.id])
end
end
context 'when OStatus privacy is not used' do
describe 'with private status' do
let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :private) }
it 'does not deliver anything' do
allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk)
subject.perform(status.stream_entry.id)
expect(Pubsubhubbub::DeliveryWorker).to_not have_received(:push_bulk)
end
end
describe 'with direct status' do
let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :direct) }
it 'does not deliver payload' do
allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk)
subject.perform(status.stream_entry.id)
expect(Pubsubhubbub::DeliveryWorker).to_not have_received(:push_bulk)
end
end
end
end

View File

@@ -1,19 +0,0 @@
require 'rails_helper'
describe Scheduler::SubscriptionsScheduler do
subject { Scheduler::SubscriptionsScheduler.new }
let!(:expiring_account1) { Fabricate(:account, subscription_expires_at: 20.minutes.from_now, domain: 'example.com', followers_count: 1, hub_url: 'http://hub.example.com') }
let!(:expiring_account2) { Fabricate(:account, subscription_expires_at: 4.hours.from_now, domain: 'example.org', followers_count: 1, hub_url: 'http://hub.example.org') }
before do
stub_request(:post, 'http://hub.example.com/').to_return(status: 202)
stub_request(:post, 'http://hub.example.org/').to_return(status: 202)
end
it 're-subscribes for all expiring accounts' do
subject.perform
expect(a_request(:post, 'http://hub.example.com/')).to have_been_made.once
expect(a_request(:post, 'http://hub.example.org/')).to have_been_made.once
end
end